merge mozilla-central to autoland. r=merge a=merge
authorSebastian Hengst <archaeopteryx@coole-files.de>
Fri, 08 Sep 2017 11:03:59 +0200
changeset 429171 2af9cfda0d5c1a8821d1640d8185ae0985383fd3
parent 429170 81a4056d20310ffdb086d6e65c8eaff11aa0e6fa (current diff)
parent 429143 50857982881ae7803ceb438fee90650a282f7f05 (diff)
child 429172 735a60b17ee4342b41529f581b87fe36a013e1aa
push id7761
push userjlund@mozilla.com
push dateFri, 15 Sep 2017 00:19:52 +0000
treeherdermozilla-beta@c38455951db4 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge, merge
milestone57.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
merge mozilla-central to autoland. r=merge a=merge
browser/components/nsBrowserGlue.js
mobile/android/app/mobile.js
toolkit/components/extensions/Extension.jsm
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -157,22 +157,18 @@ pref("app.update.elevation.promptMaxAtte
 pref("app.update.auto", true);
 
 // If set to true, the Update Service will present no UI for any event.
 pref("app.update.silent", false);
 
 // app.update.badgeWaitTime is in branding section
 
 // If set to true, the Update Service will apply updates in the background
-// when it finishes downloading them.
-#ifdef XP_WIN
+// when it finishes downloading them. Disabled in bug 1397562.
 pref("app.update.staging.enabled", false);
-#else
-pref("app.update.staging.enabled", true);
-#endif
 
 // Update service URL:
 pref("app.update.url", "https://aus5.mozilla.org/update/6/%PRODUCT%/%VERSION%/%BUILD_ID%/%BUILD_TARGET%/%LOCALE%/%CHANNEL%/%OS_VERSION%/%SYSTEM_CAPABILITIES%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%/update.xml");
 // app.update.url.manual is in branding section
 // app.update.url.details is in branding section
 
 // app.update.interval is in branding section
 // app.update.promptWaitTime is in branding section
@@ -271,17 +267,16 @@ pref("browser.slowStartup.timeThreshold"
 pref("browser.slowStartup.maxSamples", 5);
 
 // This url, if changed, MUST continue to point to an https url. Pulling arbitrary content to inject into
 // this page over http opens us up to a man-in-the-middle attack that we'd rather not face. If you are a downstream
 // repackager of this code using an alternate snippet url, please keep your users safe
 pref("browser.aboutHomeSnippets.updateUrl", "https://snippets.cdn.mozilla.net/%STARTPAGE_VERSION%/%NAME%/%VERSION%/%APPBUILDID%/%BUILD_TARGET%/%LOCALE%/%CHANNEL%/%OS_VERSION%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%/");
 
 pref("browser.enable_automatic_image_resizing", true);
-pref("browser.casting.enabled", false);
 pref("browser.chrome.site_icons", true);
 pref("browser.chrome.favicons", true);
 // browser.warnOnQuit == false will override all other possible prompts when quitting or restarting
 pref("browser.warnOnQuit", true);
 // browser.showQuitWarning specifically controls the quit warning dialog. We
 // might still show the window closing dialog with showQuitWarning == false.
 pref("browser.showQuitWarning", false);
 pref("browser.fullscreen.autohide", true);
--- a/browser/base/content/browser-context.inc
+++ b/browser/base/content/browser-context.inc
@@ -236,21 +236,16 @@
       <menuitem id="context-video-saveimage"
                 accesskey="&videoSaveImage.accesskey;"
                 label="&videoSaveImage.label;"
                 oncommand="gContextMenu.saveVideoFrameAsImage();"/>
       <menuitem id="context-sendvideo"
                 label="&emailVideoCmd.label;"
                 accesskey="&emailVideoCmd.accesskey;"
                 oncommand="gContextMenu.sendMedia();"/>
-      <menu id="context-castvideo"
-                label="&castVideoCmd.label;"
-                accesskey="&castVideoCmd.accesskey;">
-        <menupopup id="context-castvideo-popup" onpopupshowing="gContextMenu.populateCastVideoMenu(this)"/>
-      </menu>
       <menuitem id="context-sendaudio"
                 label="&emailAudioCmd.label;"
                 accesskey="&emailAudioCmd.accesskey;"
                 oncommand="gContextMenu.sendMedia();"/>
       <menuitem id="context-ctp-play"
                 label="&playPluginCmd.label;"
                 accesskey="&playPluginCmd.accesskey;"
                 oncommand="gContextMenu.playPlugin();"/>
--- a/browser/base/content/browser-menubar.inc
+++ b/browser/base/content/browser-menubar.inc
@@ -468,18 +468,17 @@
       </menu>
       <menuseparator id="bookmarksMenuItemsSeparator"/>
       <!-- Bookmarks menu items -->
     </menupopup>
   </menu>
 
             <menu id="tools-menu"
                   label="&toolsMenu.label;"
-                  accesskey="&toolsMenu.accesskey;"
-                  onpopupshowing="mirrorShow(this)">
+                  accesskey="&toolsMenu.accesskey;">
               <menupopup id="menu_ToolsPopup">
               <menuitem id="menu_openDownloads"
                         label="&downloads.label;"
                         accesskey="&downloads.accesskey;"
                         key="key_openDownloads"
                         command="Tools:Downloads"/>
               <menuitem id="menu_openAddons"
                         label="&addons.label;"
@@ -515,23 +514,16 @@
               </menu>
               <menuitem id="menu_pageInfo"
                         accesskey="&pageInfoCmd.accesskey;"
                         label="&pageInfoCmd.label;"
 #ifndef XP_WIN
                         key="key_viewInfo"
 #endif
                         command="View:PageInfo"/>
-              <menu id="menu_mirrorTabCmd"
-                    hidden="true"
-                    accesskey="&mirrorTabCmd.accesskey;"
-                    label="&mirrorTabCmd.label;">
-                <menupopup id="menu_mirrorTab-popup"
-                           onpopupshowing="populateMirrorTabMenu(this)"/>
-              </menu>
 #ifndef XP_UNIX
               <menuseparator id="prefSep"/>
               <menuitem id="menu_preferences"
                         label="&preferencesCmd2.label;"
                         accesskey="&preferencesCmd2.accesskey;"
                         oncommand="openPreferences(undefined, {origin: 'menubar'});"/>
 #endif
               </menupopup>
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -21,17 +21,16 @@ XPCOMUtils.defineLazyGetter(this, "exten
 
 // lazy module getters
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   AboutHome: "resource:///modules/AboutHome.jsm",
   BrowserUITelemetry: "resource:///modules/BrowserUITelemetry.jsm",
   BrowserUsageTelemetry: "resource:///modules/BrowserUsageTelemetry.jsm",
   BrowserUtils: "resource://gre/modules/BrowserUtils.jsm",
-  CastingApps: "resource:///modules/CastingApps.jsm",
   CharsetMenu: "resource://gre/modules/CharsetMenu.jsm",
   Color: "resource://gre/modules/Color.jsm",
   ContentSearch: "resource:///modules/ContentSearch.jsm",
   ContextualIdentityService: "resource://gre/modules/ContextualIdentityService.jsm",
   CustomizableUI: "resource:///modules/CustomizableUI.jsm",
   Deprecated: "resource://gre/modules/Deprecated.jsm",
   DownloadsCommon: "resource:///modules/DownloadsCommon.jsm",
   E10SUtils: "resource:///modules/E10SUtils.jsm",
@@ -3305,45 +3304,16 @@ function getDefaultHomePage() {
   }
   return url;
 }
 
 function BrowserFullScreen() {
   window.fullScreen = !window.fullScreen;
 }
 
-function mirrorShow(popup) {
-  let services = [];
-  if (Services.prefs.getBoolPref("browser.casting.enabled")) {
-    services = CastingApps.getServicesForMirroring();
-  }
-  popup.ownerDocument.getElementById("menu_mirrorTabCmd").hidden = !services.length;
-}
-
-function mirrorMenuItemClicked(event) {
-  gBrowser.selectedBrowser.messageManager.sendAsyncMessage("SecondScreen:tab-mirror",
-                                                           {service: event.originalTarget._service});
-}
-
-function populateMirrorTabMenu(popup) {
-  popup.innerHTML = null;
-  if (!Services.prefs.getBoolPref("browser.casting.enabled")) {
-    return;
-  }
-  let doc = popup.ownerDocument;
-  let services = CastingApps.getServicesForMirroring();
-  services.forEach(service => {
-    let item = doc.createElement("menuitem");
-    item.setAttribute("label", service.friendlyName);
-    item._service = service;
-    item.addEventListener("command", mirrorMenuItemClicked);
-    popup.appendChild(item);
-  });
-}
-
 function getWebNavigation() {
   return gBrowser.webNavigation;
 }
 
 function BrowserReloadWithFlags(reloadFlags) {
   let url = gBrowser.currentURI.spec;
   if (gBrowser.updateBrowserRemotenessByURL(gBrowser.selectedBrowser, url)) {
     // If the remoteness has changed, the new browser doesn't have any
@@ -3967,18 +3937,17 @@ const BrowserSearch = {
     openUILinkIn(this.searchEnginesURL, where);
   },
 
   _getSearchEngineId(engine) {
     if (engine && engine.identifier) {
       return engine.identifier;
     }
 
-    if (!engine || (engine.name === undefined) ||
-        !Services.prefs.getBoolPref("toolkit.telemetry.enabled"))
+    if (!engine || (engine.name === undefined))
       return "other";
 
     return "other-" + engine.name;
   },
 
   /**
    * Helper to record a search with Telemetry.
    *
--- a/browser/base/content/content.js
+++ b/browser/base/content/content.js
@@ -9,58 +9,46 @@
 /* eslint-env mozilla/frame-script */
 
 var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
 XPCOMUtils.defineLazyModuleGetters(this, {
-  E10SUtils: "resource:///modules/E10SUtils.jsm",
   BrowserUtils: "resource://gre/modules/BrowserUtils.jsm",
   ContentLinkHandler: "resource:///modules/ContentLinkHandler.jsm",
   ContentMetaHandler: "resource:///modules/ContentMetaHandler.jsm",
   ContentWebRTC: "resource:///modules/ContentWebRTC.jsm",
-  SpellCheckHelper: "resource://gre/modules/InlineSpellChecker.jsm",
   InlineSpellCheckerContent: "resource://gre/modules/InlineSpellCheckerContent.jsm",
   LoginManagerContent: "resource://gre/modules/LoginManagerContent.jsm",
   LoginFormFactory: "resource://gre/modules/LoginManagerContent.jsm",
   InsecurePasswordUtils: "resource://gre/modules/InsecurePasswordUtils.jsm",
   PluginContent: "resource:///modules/PluginContent.jsm",
   PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
   FormSubmitObserver: "resource:///modules/FormSubmitObserver.jsm",
   PageMetadata: "resource://gre/modules/PageMetadata.jsm",
   PlacesUIUtils: "resource:///modules/PlacesUIUtils.jsm",
   Utils: "resource://gre/modules/sessionstore/Utils.jsm",
   WebNavigationFrames: "resource://gre/modules/WebNavigationFrames.jsm",
   Feeds: "resource:///modules/Feeds.jsm",
-  findCssSelector: "resource://gre/modules/css-selector.js",
+  ContextMenu: "resource:///modules/ContextMenu.jsm",
 });
 
-XPCOMUtils.defineLazyGetter(this, "PageMenuChild", function() {
-  let tmp = {};
-  Cu.import("resource://gre/modules/PageMenu.jsm", tmp);
-  return new tmp.PageMenuChild();
-});
-
-Cu.importGlobalProperties(["URL"]);
-
 // TabChildGlobal
 var global = this;
 
+var contextMenu = this.contextMenu = new ContextMenu(global);
+
 // Load the form validation popup handler
 var formSubmitObserver = new FormSubmitObserver(content, this);
 
-addMessageListener("ContextMenu:DoCustomCommand", function(message) {
-  E10SUtils.wrapHandlingUserInput(
-    content, message.data.handlingUserInput,
-    () => PageMenuChild.executeMenu(message.data.generatedItemId));
-});
-
 addMessageListener("RemoteLogins:fillForm", function(message) {
+  // intercept if ContextMenu.jsm had sent a plain object for remote targets
+  message.objects.inputElement = contextMenu.getTarget(message, "inputElement");
   LoginManagerContent.receiveMessage(message, content);
 });
 addEventListener("DOMFormHasPassword", function(event) {
   LoginManagerContent.onDOMFormHasPassword(event, content);
   let formLike = LoginFormFactory.createFromForm(event.target);
   InsecurePasswordUtils.reportInsecurePasswords(formLike);
 });
 addEventListener("DOMInputPasswordAdded", function(event) {
@@ -73,154 +61,16 @@ addEventListener("pageshow", function(ev
 });
 addEventListener("DOMAutoComplete", function(event) {
   LoginManagerContent.onUsernameInput(event);
 });
 addEventListener("blur", function(event) {
   LoginManagerContent.onUsernameInput(event);
 });
 
-var handleContentContextMenu = function(event) {
-  let defaultPrevented = event.defaultPrevented;
-  if (!Services.prefs.getBoolPref("dom.event.contextmenu.enabled")) {
-    let plugin = null;
-    try {
-      plugin = event.target.QueryInterface(Ci.nsIObjectLoadingContent);
-    } catch (e) {}
-    if (plugin && plugin.displayedType == Ci.nsIObjectLoadingContent.TYPE_PLUGIN) {
-      // Don't open a context menu for plugins.
-      return;
-    }
-
-    defaultPrevented = false;
-  }
-
-  if (defaultPrevented)
-    return;
-
-  let addonInfo = {};
-  let subject = {
-    event,
-    addonInfo,
-  };
-  subject.wrappedJSObject = subject;
-  Services.obs.notifyObservers(subject, "content-contextmenu");
-
-  let doc = event.target.ownerDocument;
-  let docLocation = doc.mozDocumentURIIfNotForErrorPages;
-  docLocation = docLocation && docLocation.spec;
-  let charSet = doc.characterSet;
-  let baseURI = doc.baseURI;
-  let referrer = doc.referrer;
-  let referrerPolicy = doc.referrerPolicy;
-  let frameOuterWindowID = WebNavigationFrames.getFrameId(doc.defaultView);
-  let loginFillInfo = LoginManagerContent.getFieldContext(event.target);
-
-  // The same-origin check will be done in nsContextMenu.openLinkInTab.
-  let parentAllowsMixedContent = !!docShell.mixedContentChannel;
-
-  // get referrer attribute from clicked link and parse it
-  let referrerAttrValue = Services.netUtils.parseAttributePolicyString(event.target.
-                          getAttribute("referrerpolicy"));
-  if (referrerAttrValue !== Ci.nsIHttpChannel.REFERRER_POLICY_UNSET) {
-    referrerPolicy = referrerAttrValue;
-  }
-
-  let disableSetDesktopBg = null;
-  // Media related cache info parent needs for saving
-  let contentType = null;
-  let contentDisposition = null;
-  if (event.target.nodeType == Ci.nsIDOMNode.ELEMENT_NODE &&
-      event.target instanceof Ci.nsIImageLoadingContent &&
-      event.target.currentURI) {
-    disableSetDesktopBg = disableSetDesktopBackground(event.target);
-
-    try {
-      let imageCache =
-        Cc["@mozilla.org/image/tools;1"].getService(Ci.imgITools)
-                                        .getImgCacheForDocument(doc);
-      let props =
-        imageCache.findEntryProperties(event.target.currentURI, doc);
-      try {
-        contentType = props.get("type", Ci.nsISupportsCString).data;
-      } catch (e) {}
-      try {
-        contentDisposition =
-          props.get("content-disposition", Ci.nsISupportsCString).data;
-      } catch (e) {}
-    } catch (e) {}
-  }
-
-  let selectionInfo = BrowserUtils.getSelectionDetails(content);
-
-  let loadContext = docShell.QueryInterface(Ci.nsILoadContext);
-  let userContextId = loadContext.originAttributes.userContextId;
-  let popupNodeSelectors = getNodeSelectors(event.target);
-
-  if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
-    let editFlags = SpellCheckHelper.isEditable(event.target, content);
-    let spellInfo;
-    if (editFlags &
-        (SpellCheckHelper.EDITABLE | SpellCheckHelper.CONTENTEDITABLE)) {
-      spellInfo =
-        InlineSpellCheckerContent.initContextMenu(event, editFlags, this);
-    }
-
-    // Set the event target first as the copy image command needs it to
-    // determine what was context-clicked on. Then, update the state of the
-    // commands on the context menu.
-    docShell.contentViewer.QueryInterface(Ci.nsIContentViewerEdit)
-            .setCommandNode(event.target);
-    event.target.ownerGlobal.updateCommands("contentcontextmenu");
-
-    let customMenuItems = PageMenuChild.build(event.target);
-    let principal = doc.nodePrincipal;
-
-    sendRpcMessage("contextmenu",
-                   { editFlags, spellInfo, customMenuItems, addonInfo,
-                     principal, docLocation, charSet, baseURI, referrer,
-                     referrerPolicy, contentType, contentDisposition,
-                     frameOuterWindowID, selectionInfo, disableSetDesktopBg,
-                     loginFillInfo, parentAllowsMixedContent, userContextId,
-                     popupNodeSelectors,
-                   }, {
-                     event,
-                     popupNode: event.target,
-                   });
-  } else {
-    // Break out to the parent window and pass the add-on info along
-    let browser = docShell.chromeEventHandler;
-    let mainWin = browser.ownerGlobal;
-    mainWin.setContextMenuContentData({
-      isRemote: false,
-      event,
-      popupNode: event.target,
-      popupNodeSelectors,
-      browser,
-      addonInfo,
-      documentURIObject: doc.documentURIObject,
-      docLocation,
-      charSet,
-      referrer,
-      referrerPolicy,
-      contentType,
-      contentDisposition,
-      selectionInfo,
-      disableSetDesktopBackground: disableSetDesktopBg,
-      loginFillInfo,
-      parentAllowsMixedContent,
-      userContextId,
-    });
-  }
-}
-
-Cc["@mozilla.org/eventlistenerservice;1"]
-  .getService(Ci.nsIEventListenerService)
-  .addSystemEventListener(global, "contextmenu", handleContentContextMenu, false);
-
 // Values for telemtery bins: see TLS_ERROR_REPORT_UI in Histograms.json
 const TLS_ERROR_REPORT_TELEMETRY_UI_SHOWN = 0;
 const TLS_ERROR_REPORT_TELEMETRY_EXPANDED = 1;
 const TLS_ERROR_REPORT_TELEMETRY_SUCCESS  = 6;
 const TLS_ERROR_REPORT_TELEMETRY_FAILURE  = 7;
 
 const SEC_ERROR_BASE          = Ci.nsINSSErrorsService.NSS_SEC_ERROR_BASE;
 const MOZILLA_PKIX_ERROR_BASE = Ci.nsINSSErrorsService.MOZILLA_PKIX_ERROR_BASE;
@@ -233,39 +83,16 @@ const SEC_ERROR_OCSP_OLD_RESPONSE       
 const MOZILLA_PKIX_ERROR_NOT_YET_VALID_CERTIFICATE = MOZILLA_PKIX_ERROR_BASE + 5;
 const MOZILLA_PKIX_ERROR_NOT_YET_VALID_ISSUER_CERTIFICATE = MOZILLA_PKIX_ERROR_BASE + 6;
 
 const PREF_BLOCKLIST_CLOCK_SKEW_SECONDS = "services.blocklist.clock_skew_seconds";
 
 const PREF_SSL_IMPACT_ROOTS = ["security.tls.version.", "security.ssl3."];
 
 
-/**
- * Retrieve the array of CSS selectors corresponding to the provided node. The first item
- * of the array is the selector of the node in its owner document. Additional items are
- * used if the node is inside a frame, each representing the CSS selector for finding the
- * frame element in its parent document.
- *
- * This format is expected by DevTools in order to handle the Inspect Node context menu
- * item.
- *
- * @param  {Node}
- *         The node for which the CSS selectors should be computed
- * @return {Array} array of css selectors (strings).
- */
-function getNodeSelectors(node) {
-  let selectors = [];
-  while (node) {
-    selectors.push(findCssSelector(node));
-    node = node.ownerGlobal.frameElement;
-  }
-
-  return selectors;
-}
-
 function getSerializedSecurityInfo(docShell) {
   let serhelper = Cc["@mozilla.org/network/serialization-helper;1"]
                     .getService(Ci.nsISerializationHelper);
 
   let securityInfo = docShell.failedChannel && docShell.failedChannel.securityInfo;
   if (!securityInfo) {
     return "";
   }
@@ -807,175 +634,32 @@ addEventListener("pagehide", function(ev
 var PageMetadataMessenger = {
   init() {
     addMessageListener("PageMetadata:GetPageData", this);
     addMessageListener("PageMetadata:GetMicroformats", this);
   },
   receiveMessage(message) {
     switch (message.name) {
       case "PageMetadata:GetPageData": {
-        let target = message.objects.target;
+        let target = contextMenu.getTarget(message);
         let result = PageMetadata.getData(content.document, target);
         sendAsyncMessage("PageMetadata:PageDataResult", result);
         break;
       }
       case "PageMetadata:GetMicroformats": {
-        let target = message.objects.target;
+        let target = contextMenu.getTarget(message);
         let result = PageMetadata.getMicroformats(content.document, target);
         sendAsyncMessage("PageMetadata:MicroformatsResult", result);
         break;
       }
     }
   }
 }
 PageMetadataMessenger.init();
 
-addMessageListener("ContextMenu:SaveVideoFrameAsImage", (message) => {
-  let video = message.objects.target;
-  let canvas = content.document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
-  canvas.width = video.videoWidth;
-  canvas.height = video.videoHeight;
-
-  let ctxDraw = canvas.getContext("2d");
-  ctxDraw.drawImage(video, 0, 0);
-  sendAsyncMessage("ContextMenu:SaveVideoFrameAsImage:Result", {
-    dataURL: canvas.toDataURL("image/jpeg", ""),
-  });
-});
-
-addMessageListener("ContextMenu:MediaCommand", (message) => {
-  E10SUtils.wrapHandlingUserInput(
-    content, message.data.handlingUserInput,
-    () => {
-      let media = message.objects.element;
-      switch (message.data.command) {
-        case "play":
-          media.play();
-          break;
-        case "pause":
-          media.pause();
-          break;
-        case "loop":
-          media.loop = !media.loop;
-          break;
-        case "mute":
-          media.muted = true;
-          break;
-        case "unmute":
-          media.muted = false;
-          break;
-        case "playbackRate":
-          media.playbackRate = message.data.data;
-          break;
-        case "hidecontrols":
-          media.removeAttribute("controls");
-          break;
-        case "showcontrols":
-          media.setAttribute("controls", "true");
-          break;
-        case "fullscreen":
-          if (content.document.fullscreenEnabled)
-            media.requestFullscreen();
-          break;
-      }
-    });
-});
-
-addMessageListener("ContextMenu:Canvas:ToBlobURL", (message) => {
-  message.objects.target.toBlob((blob) => {
-    let blobURL = URL.createObjectURL(blob);
-    sendAsyncMessage("ContextMenu:Canvas:ToBlobURL:Result", { blobURL });
-  });
-});
-
-addMessageListener("ContextMenu:ReloadFrame", (message) => {
-  message.objects.target.ownerDocument.location.reload();
-});
-
-addMessageListener("ContextMenu:ReloadImage", (message) => {
-  let image = message.objects.target;
-  if (image instanceof Ci.nsIImageLoadingContent)
-    image.forceReload();
-});
-
-addMessageListener("ContextMenu:BookmarkFrame", (message) => {
-  let frame = message.objects.target.ownerDocument;
-  sendAsyncMessage("ContextMenu:BookmarkFrame:Result",
-                   { title: frame.title,
-                     description: PlacesUIUtils.getDescriptionFromDocument(frame) });
-});
-
-addMessageListener("ContextMenu:SearchFieldBookmarkData", (message) => {
-  let node = message.objects.target;
-
-  let charset = node.ownerDocument.characterSet;
-
-  let formBaseURI = Services.io.newURI(node.form.baseURI, charset);
-
-  let formURI = Services.io.newURI(node.form.getAttribute("action"), charset,
-                                   formBaseURI);
-
-  let spec = formURI.spec;
-
-  let isURLEncoded =
-               (node.form.method.toUpperCase() == "POST"
-                && (node.form.enctype == "application/x-www-form-urlencoded" ||
-                    node.form.enctype == ""));
-
-  let title = node.ownerDocument.title;
-  let description = PlacesUIUtils.getDescriptionFromDocument(node.ownerDocument);
-
-  let formData = [];
-
-  function escapeNameValuePair(aName, aValue, aIsFormUrlEncoded) {
-    if (aIsFormUrlEncoded) {
-      return escape(aName + "=" + aValue);
-    }
-    return escape(aName) + "=" + escape(aValue);
-  }
-
-  for (let el of node.form.elements) {
-    if (!el.type) // happens with fieldsets
-      continue;
-
-    if (el == node) {
-      formData.push((isURLEncoded) ? escapeNameValuePair(el.name, "%s", true) :
-                                     // Don't escape "%s", just append
-                                     escapeNameValuePair(el.name, "", false) + "%s");
-      continue;
-    }
-
-    let type = el.type.toLowerCase();
-
-    if (((el instanceof content.HTMLInputElement && el.mozIsTextField(true)) ||
-        type == "hidden" || type == "textarea") ||
-        ((type == "checkbox" || type == "radio") && el.checked)) {
-      formData.push(escapeNameValuePair(el.name, el.value, isURLEncoded));
-    } else if (el instanceof content.HTMLSelectElement && el.selectedIndex >= 0) {
-      for (let j = 0; j < el.options.length; j++) {
-        if (el.options[j].selected)
-          formData.push(escapeNameValuePair(el.name, el.options[j].value,
-                                            isURLEncoded));
-      }
-    }
-  }
-
-  let postData;
-
-  if (isURLEncoded)
-    postData = formData.join("&");
-  else {
-    let separator = spec.includes("?") ? "&" : "?";
-    spec += separator + formData.join("&");
-  }
-
-  sendAsyncMessage("ContextMenu:SearchFieldBookmarkData:Result",
-                   { spec, title, description, postData, charset });
-});
-
 addMessageListener("Bookmarks:GetPageDetails", (message) => {
   let doc = content.document;
   let isErrorPage = /^about:(neterror|certerror|blocked)/.test(doc.documentURI);
   sendAsyncMessage("Bookmarks:GetPageDetails:Result",
                    { isErrorPage,
                      description: PlacesUIUtils.getDescriptionFromDocument(doc) });
 });
 
@@ -1025,66 +709,16 @@ var LightWeightThemeWebInstallListener =
   _resetPreviewWindow() {
     this._previewWindow.removeEventListener("pagehide", this, true);
     this._previewWindow = null;
   }
 };
 
 LightWeightThemeWebInstallListener.init();
 
-function disableSetDesktopBackground(aTarget) {
-  // Disable the Set as Desktop Background menu item if we're still trying
-  // to load the image or the load failed.
-  if (!(aTarget instanceof Ci.nsIImageLoadingContent))
-    return true;
-
-  if (("complete" in aTarget) && !aTarget.complete)
-    return true;
-
-  if (aTarget.currentURI.schemeIs("javascript"))
-    return true;
-
-  let request = aTarget.QueryInterface(Ci.nsIImageLoadingContent)
-                       .getRequest(Ci.nsIImageLoadingContent.CURRENT_REQUEST);
-  if (!request)
-    return true;
-
-  return false;
-}
-
-addMessageListener("ContextMenu:SetAsDesktopBackground", (message) => {
-  let target = message.objects.target;
-
-  // Paranoia: check disableSetDesktopBackground again, in case the
-  // image changed since the context menu was initiated.
-  let disable = disableSetDesktopBackground(target);
-
-  if (!disable) {
-    try {
-      BrowserUtils.urlSecurityCheck(target.currentURI.spec, target.ownerDocument.nodePrincipal);
-      let canvas = content.document.createElement("canvas");
-      canvas.width = target.naturalWidth;
-      canvas.height = target.naturalHeight;
-      let ctx = canvas.getContext("2d");
-      ctx.drawImage(target, 0, 0);
-      let dataUrl = canvas.toDataURL();
-      let url = (new URL(target.ownerDocument.location.href)).pathname;
-      let imageName = url.substr(url.lastIndexOf("/") + 1);
-      sendAsyncMessage("ContextMenu:SetAsDesktopBackground:Result",
-                       { dataUrl, imageName });
-    } catch (e) {
-      Cu.reportError(e);
-      disable = true;
-    }
-  }
-
-  if (disable)
-    sendAsyncMessage("ContextMenu:SetAsDesktopBackground:Result", { disable });
-});
-
 var PageInfoListener = {
 
   init() {
     addMessageListener("PageInfo:getData", this);
   },
 
   receiveMessage(message) {
     let strings = message.data.strings;
@@ -1097,43 +731,27 @@ var PageInfoListener = {
     if (frameOuterWindowID != undefined) {
       window = Services.wm.getOuterWindowWithId(frameOuterWindowID);
       document = window.document;
     } else {
       window = content.window;
       document = content.document;
     }
 
-    let imageElement = message.objects.imageElement;
-
     let pageInfoData = {metaViewRows: this.getMetaInfo(document),
                         docInfo: this.getDocumentInfo(document),
                         feeds: this.getFeedsInfo(document, strings),
-                        windowInfo: this.getWindowInfo(window),
-                        imageInfo: this.getImageInfo(imageElement)};
+                        windowInfo: this.getWindowInfo(window)};
 
     sendAsyncMessage("PageInfo:data", pageInfoData);
 
     // Separate step so page info dialog isn't blank while waiting for this to finish.
     this.getMediaInfo(document, window, strings);
   },
 
-  getImageInfo(imageElement) {
-    let imageInfo = null;
-    if (imageElement) {
-      imageInfo = {
-        currentSrc: imageElement.currentSrc,
-        width: imageElement.width,
-        height: imageElement.height,
-        imageText: imageElement.title || imageElement.alt
-      };
-    }
-    return imageInfo;
-  },
-
   getMetaInfo(document) {
     let metaViewRows = [];
 
     // Get the meta tags from the page.
     let metaNodes = document.getElementsByTagName("meta");
 
     for (let metaNode of metaNodes) {
       metaViewRows.push([metaNode.name || metaNode.httpEquiv || metaNode.getAttribute("property"),
--- a/browser/base/content/nsContextMenu.js
+++ b/browser/base/content/nsContextMenu.js
@@ -22,26 +22,27 @@ var gContextMenuContentData = null;
 
 function setContextMenuContentData(data) {
   gContextMenuContentData = data;
 }
 
 function openContextMenu(aMessage) {
   let data = aMessage.data;
   let browser = aMessage.target;
+  let spellInfo = data.spellInfo;
 
-  let spellInfo = data.spellInfo;
-  if (spellInfo)
+  if (spellInfo) {
     spellInfo.target = aMessage.target.messageManager;
+  }
+
   let documentURIObject = makeURI(data.docLocation,
                                   data.charSet,
                                   makeURI(data.baseURI));
-  gContextMenuContentData = { isRemote: true,
-                              event: aMessage.objects.event,
-                              popupNode: aMessage.objects.popupNode,
+  gContextMenuContentData = { context: data.context,
+                              isRemote: data.isRemote,
                               popupNodeSelectors: data.popupNodeSelectors,
                               browser,
                               editFlags: data.editFlags,
                               spellInfo,
                               principal: data.principal,
                               customMenuItems: data.customMenuItems,
                               addonInfo: data.addonInfo,
                               documentURIObject,
@@ -53,40 +54,41 @@ function openContextMenu(aMessage) {
                               contentDisposition: data.contentDisposition,
                               frameOuterWindowID: data.frameOuterWindowID,
                               selectionInfo: data.selectionInfo,
                               disableSetDesktopBackground: data.disableSetDesktopBg,
                               loginFillInfo: data.loginFillInfo,
                               parentAllowsMixedContent: data.parentAllowsMixedContent,
                               userContextId: data.userContextId,
                             };
+
   let popup = browser.ownerDocument.getElementById("contentAreaContextMenu");
-  let event = gContextMenuContentData.event;
+  let context = gContextMenuContentData.context;
 
   // The event is a CPOW that can't be passed into the native openPopupAtScreen
   // function. Therefore we synthesize a new MouseEvent to propagate the
   // inputSource to the subsequently triggered popupshowing event.
   var newEvent = document.createEvent("MouseEvent");
-  newEvent.initNSMouseEvent("contextmenu", true, true, null, 0, event.screenX, event.screenY,
-                            0, 0, false, false, false, false, 0, null, 0, event.mozInputSource);
+  newEvent.initNSMouseEvent("contextmenu", true, true, null, 0, context.screenX, context.screenY,
+                            0, 0, false, false, false, false, 0, null, 0, context.mozInputSource);
 
   popup.openPopupAtScreen(newEvent.screenX, newEvent.screenY, true, newEvent);
 }
 
 function nsContextMenu(aXulMenu, aIsShift) {
   this.shouldDisplay = true;
   this.initMenu(aXulMenu, aIsShift);
 }
 
 // Prototype for nsContextMenu "class."
 nsContextMenu.prototype = {
   initMenu: function CM_initMenu(aXulMenu, aIsShift) {
     // Get contextual info.
-    this.setTarget(document.popupNode, document.popupRangeParent,
-                   document.popupRangeOffset);
+    this.setContext();
+
     if (!this.shouldDisplay)
       return;
 
     this.hasPageMenu = false;
     this.isContentSelected = !this.selectionInfo.docSelectionIsCollapsed;
     if (!aIsShift) {
       if (this.isRemote) {
         this.hasPageMenu =
@@ -140,17 +142,144 @@ nsContextMenu.prototype = {
 
     // Initialize (disable/remove) menu items.
     this.initItems();
 
     // Register this opening of the menu with telemetry:
     this._checkTelemetryForMenu(aXulMenu);
   },
 
+  setContext() {
+    let context = Object.create(null);
+    this.isRemote = false;
+
+    if (gContextMenuContentData) {
+      context = gContextMenuContentData.context;
+      gContextMenuContentData.context = null;
+      this.isRemote = gContextMenuContentData.isRemote;
+    }
+
+    this.shouldDisplay = context.shouldDisplay;
+
+    // Assign what's _possibly_ needed from `context` sent by ContextMenu.jsm
+    // Keep this consistent with the similar code in ContextMenu's _setContext
+    this.bgImageURL          = context.bgImageURL;
+    this.imageDescURL        = context.imageDescURL;
+    this.imageInfo           = context.imageInfo;
+    this.mediaURL            = context.mediaURL;
+    this.webExtBrowserType   = context.webExtBrowserType;
+
+    this.canSpellCheck       = context.canSpellCheck;
+    this.hasBGImage          = context.hasBGImage;
+    this.hasMultipleBGImages = context.hasMultipleBGImages;
+    this.isDesignMode        = context.isDesignMode;
+    this.inFrame             = context.inFrame;
+    this.inSrcdocFrame       = context.inSrcdocFrame;
+    this.inSyntheticDoc      = context.inSyntheticDoc;
+    this.inTabBrowser        = context.inTabBrowser;
+    this.inWebExtBrowser     = context.inWebExtBrowser;
+
+    this.link                = context.link;
+    this.linkDownload        = context.linkDownload;
+    this.linkHasNoReferrer   = context.linkHasNoReferrer;
+    this.linkProtocol        = context.linkProtocol;
+    this.linkTextStr         = context.linkTextStr;
+    this.linkURL             = context.linkURL;
+    this.linkURI             = this.getLinkURI();  // can't send; regenerate
+
+    this.onAudio             = context.onAudio;
+    this.onCanvas            = context.onCanvas;
+    this.onCompletedImage    = context.onCompletedImage;
+    this.onCTPPlugin         = context.onCTPPlugin;
+    this.onDRMMedia          = context.onDRMMedia;
+    this.onEditableArea      = context.onEditableArea;
+    this.onImage             = context.onImage;
+    this.onKeywordField      = context.onKeywordField;
+    this.onLink              = context.onLink;
+    this.onLoadedImage       = context.onLoadedImage;
+    this.onMailtoLink        = context.onMailtoLink;
+    this.onMathML            = context.onMathML;
+    this.onMozExtLink        = context.onMozExtLink;
+    this.onNumeric           = context.onNumeric;
+    this.onPassword          = context.onPassword;
+    this.onSaveableLink      = context.onSaveableLink;
+    this.onTextInput         = context.onTextInput;
+    this.onVideo             = context.onVideo;
+
+    this.target = this.isRemote ? context.target : document.popupNode;
+
+    this.principal = context.principal;
+    this.frameOuterWindowID = context.frameOuterWindowID;
+
+    this.inSyntheticDoc = context.inSyntheticDoc;
+
+    // Everything after this isn't sent directly from ContextMenu
+    this.ownerDoc = this.target.ownerDocument;
+
+    // Remember the CSS selectors corresponding to clicked node. gContextMenuContentData
+    // can be null if the menu was triggered by tests in which case use an empty array.
+    this.targetSelectors = gContextMenuContentData
+                           ? gContextMenuContentData.popupNodeSelectors
+                           : [];
+
+    if (this.isRemote) {
+      this.browser = gContextMenuContentData.browser;
+      this.selectionInfo = gContextMenuContentData.selectionInfo;
+    } else {
+      this.browser = this.ownerDoc.defaultView
+                         .QueryInterface(Ci.nsIInterfaceRequestor)
+                         .getInterface(Ci.nsIWebNavigation)
+                         .QueryInterface(Ci.nsIDocShell)
+                         .chromeEventHandler;
+      this.selectionInfo = BrowserUtils.getSelectionDetails(window);
+    }
+
+    this.textSelected      = this.selectionInfo.text;
+    this.isTextSelected    = this.textSelected.length != 0;
+    this.webExtBrowserType = this.browser.getAttribute("webextension-view-type");
+    this.inWebExtBrowser   = !!this.webExtBrowserType;
+    this.inTabBrowser      = this.browser.ownerGlobal.gBrowser ?
+      !!this.browser.ownerGlobal.gBrowser.getTabForBrowser(this.browser) : false;
+
+    if (context.shouldInitInlineSpellCheckerUINoChildren) {
+      if (this.isRemote) {
+        InlineSpellCheckerUI.initFromRemote(gContextMenuContentData.spellInfo);
+      } else {
+        InlineSpellCheckerUI.init(this.target.QueryInterface(Ci.nsIDOMNSEditableElement).editor);
+        InlineSpellCheckerUI.initFromEvent(document.popupRangeParent,
+                                           document.popupRangeOffset);
+      }
+    }
+
+    if (context.shouldInitInlineSpellCheckerUIWithChildren) {
+      if (this.isRemote) {
+        InlineSpellCheckerUI.initFromRemote(gContextMenuContentData.spellInfo);
+      } else {
+        var targetWin = this.ownerDoc.defaultView;
+        var editingSession = targetWin.QueryInterface(Ci.nsIInterfaceRequestor)
+                                      .getInterface(Ci.nsIWebNavigation)
+                                      .QueryInterface(Ci.nsIInterfaceRequestor)
+                                      .getInterface(Ci.nsIEditingSession);
+
+        InlineSpellCheckerUI.init(editingSession.getEditorForWindow(targetWin));
+        InlineSpellCheckerUI.initFromEvent(document.popupRangeParent,
+                                           document.popupRangeOffset);
+      }
+
+      let canSpell = InlineSpellCheckerUI.canSpellCheck && this.canSpellCheck;
+      this.showItem("spell-check-enabled", canSpell);
+      this.showItem("spell-separator", canSpell);
+    }
+  },  // setContext
+
   hiding: function CM_hiding() {
+    if (this.browser) {
+      this.browser.messageManager.sendAsyncMessage("ContextMenu:Hiding");
+    }
+
     gContextMenuContentData = null;
     InlineSpellCheckerUI.clearSuggestionsFromMenu();
     InlineSpellCheckerUI.clearDictionaryListFromMenu();
     InlineSpellCheckerUI.uninit();
     if (Cu.isModuleLoaded("resource://gre/modules/LoginManagerContextMenu.jsm")) {
       LoginManagerContextMenu.clearLoginsFromMenu(document);
     }
 
@@ -273,31 +402,20 @@ nsContextMenu.prototype = {
     this.showItem("context-savevideo", this.onVideo);
     this.showItem("context-saveaudio", this.onAudio);
     this.showItem("context-video-saveimage", this.onVideo);
     this.setItemAttr("context-savevideo", "disabled", !this.mediaURL);
     this.setItemAttr("context-saveaudio", "disabled", !this.mediaURL);
     // Send media URL (but not for canvas, since it's a big data: URL)
     this.showItem("context-sendimage", this.onImage);
     this.showItem("context-sendvideo", this.onVideo);
-    this.showItem("context-castvideo", this.onVideo);
     this.showItem("context-sendaudio", this.onAudio);
     let mediaIsBlob = this.mediaURL.startsWith("blob:");
     this.setItemAttr("context-sendvideo", "disabled", !this.mediaURL || mediaIsBlob);
     this.setItemAttr("context-sendaudio", "disabled", !this.mediaURL || mediaIsBlob);
-    let shouldShowCast = Services.prefs.getBoolPref("browser.casting.enabled");
-    // getServicesForVideo alone would be sufficient here (it depends on
-    // SimpleServiceDiscovery.services), but SimpleServiceDiscovery is guaranteed
-    // to be already loaded, since we load it on startup in nsBrowserGlue,
-    // and CastingApps isn't, so check SimpleServiceDiscovery.services first
-    // to avoid needing to load CastingApps.jsm if we don't need to.
-    shouldShowCast = shouldShowCast && this.mediaURL &&
-                     SimpleServiceDiscovery.services.length > 0 &&
-                     CastingApps.getServicesForVideo(this.target).length > 0;
-    this.setItemAttr("context-castvideo", "disabled", !shouldShowCast);
   },
 
   initViewItems: function CM_initViewItems() {
     // View source is always OK, unless in directory listing.
     this.showItem("context-viewpartialsource-selection",
                   this.isContentSelected);
     this.showItem("context-viewpartialsource-mathml",
                   this.onMathML && !this.isContentSelected);
@@ -349,20 +467,20 @@ nsContextMenu.prototype = {
 
     // View video depends on not having a standalone video.
     this.showItem("context-viewvideo", this.onVideo && (!this.inSyntheticDoc || this.inFrame));
     this.setItemAttr("context-viewvideo", "disabled", !this.mediaURL);
 
     // View background image depends on whether there is one, but don't make
     // background images of a stand-alone media document available.
     this.showItem("context-viewbgimage", shouldShow &&
-                                         !this._hasMultipleBGImages &&
+                                         !this.hasMultipleBGImages &&
                                          !this.inSyntheticDoc);
     this.showItem("context-sep-viewbgimage", shouldShow &&
-                                             !this._hasMultipleBGImages &&
+                                             !this.hasMultipleBGImages &&
                                              !this.inSyntheticDoc);
     document.getElementById("context-viewbgimage")
             .disabled = !this.hasBGImage;
 
     this.showItem("context-viewimageinfo", this.onImage);
     // The image info popup is broken for WebExtension popups, since the browser
     // is destroyed when the popup is closed.
     this.setItemAttr("context-viewimageinfo", "disabled", this.webExtBrowserType === "popup");
@@ -619,437 +737,16 @@ nsContextMenu.prototype = {
   openPasswordManager() {
     LoginHelper.openPasswordManager(window, gContextMenuContentData.documentURIObject.host);
   },
 
   inspectNode() {
     return DevToolsShim.inspectNode(gBrowser.selectedTab, this.targetSelectors);
   },
 
-  /**
-   * Set various context menu attributes based on the state of the world.
-   * Note: If the context menu is on a remote process the supplied parameters
-   * will be overwritten with data from gContextMenuContentData.
-   *
-   * @param {Object} aNode The node that this menu is being opened on.
-   * @param {nsIDOMNode} aRangeParent The parent node for where the selection ends.
-   * @param {Integer} aRangeOffset The end position of where the selction ends.
-   */
-  setTarget(aNode, aRangeParent, aRangeOffset) {
-    // gContextMenuContentData.isRemote tells us if the event came from a remote
-    // process. gContextMenuContentData can be null if something (like tests)
-    // opens the context menu directly.
-    this.isRemote = gContextMenuContentData && gContextMenuContentData.isRemote;
-    if (this.isRemote) {
-      aNode = gContextMenuContentData.event.target;
-      aRangeParent = gContextMenuContentData.event.rangeParent;
-      aRangeOffset = gContextMenuContentData.event.rangeOffset;
-    }
-
-    const xulNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
-    if (aNode.nodeType == Node.DOCUMENT_NODE ||
-        // Not display on XUL element but relax for <label class="text-link">
-        (aNode.namespaceURI == xulNS && !this._isXULTextLinkLabel(aNode))) {
-      this.shouldDisplay = false;
-      return;
-    }
-
-    // Initialize contextual info.
-    this.onImage           = false;
-    this.onLoadedImage     = false;
-    this.onCompletedImage  = false;
-    this.imageDescURL      = "";
-    this.onCanvas          = false;
-    this.onVideo           = false;
-    this.onAudio           = false;
-    this.onDRMMedia        = false;
-    this.onTextInput       = false;
-    this.onNumeric         = false;
-    this.onKeywordField    = false;
-    this.mediaURL          = "";
-    this.onLink            = false;
-    this.onMailtoLink      = false;
-    this.onSaveableLink    = false;
-    this.link              = null;
-    this.linkURL           = "";
-    this.linkURI           = null;
-    this.linkTextStr       = "";
-    this.linkProtocol      = "";
-    this.linkDownload      = "";
-    this.linkHasNoReferrer = false;
-    this.onMathML          = false;
-    this.inFrame           = false;
-    this.inSrcdocFrame     = false;
-    this.inSyntheticDoc    = false;
-    this.hasBGImage        = false;
-    this.bgImageURL        = "";
-    this.onEditableArea    = false;
-    this.isDesignMode      = false;
-    this.onCTPPlugin       = false;
-    this.canSpellCheck     = false;
-    this.onPassword        = false;
-    this.webExtBrowserType = "";
-    this.inWebExtBrowser   = false;
-    this.inTabBrowser      = true;
-    this.onMozExtLink      = false;
-
-    if (this.isRemote) {
-      this.selectionInfo = gContextMenuContentData.selectionInfo;
-    } else {
-      this.selectionInfo = BrowserUtils.getSelectionDetails(window);
-    }
-
-    this.textSelected      = this.selectionInfo.text;
-    this.isTextSelected    = this.textSelected.length != 0;
-
-    // Remember the node that was clicked.
-    this.target = aNode;
-
-    // Remember the CSS selectors corresponding to clicked node. gContextMenuContentData
-    // can be null if the menu was triggered by tests in which case use an empty array.
-    this.targetSelectors = gContextMenuContentData
-                              ? gContextMenuContentData.popupNodeSelectors
-                              : [];
-
-    let ownerDoc = this.target.ownerDocument;
-    this.ownerDoc = ownerDoc;
-
-    let editFlags;
-
-    // If this is a remote context menu event, use the information from
-    // gContextMenuContentData instead.
-    if (this.isRemote) {
-      this.browser = gContextMenuContentData.browser;
-      this.principal = gContextMenuContentData.principal;
-      this.frameOuterWindowID = gContextMenuContentData.frameOuterWindowID;
-      editFlags = gContextMenuContentData.editFlags;
-    } else {
-      editFlags = SpellCheckHelper.isEditable(this.target, window);
-      this.browser = ownerDoc.defaultView
-                             .QueryInterface(Ci.nsIInterfaceRequestor)
-                             .getInterface(Ci.nsIWebNavigation)
-                             .QueryInterface(Ci.nsIDocShell)
-                             .chromeEventHandler;
-      this.principal = ownerDoc.nodePrincipal;
-      this.frameOuterWindowID = WebNavigationFrames.getFrameId(ownerDoc.defaultView);
-    }
-    this.webExtBrowserType = this.browser.getAttribute("webextension-view-type");
-    this.inWebExtBrowser = !!this.webExtBrowserType;
-    this.inTabBrowser = this.browser.ownerGlobal.gBrowser ?
-      !!this.browser.ownerGlobal.gBrowser.getTabForBrowser(this.browser) : false;
-
-    // Check if we are in a synthetic document (stand alone image, video, etc.).
-    this.inSyntheticDoc = ownerDoc.mozSyntheticDocument;
-
-    this._setTargetForNodesNoChildren(editFlags, aRangeParent, aRangeOffset);
-
-    this._setTargetForNodesWithChildren(editFlags, aRangeParent, aRangeOffset);
-  },
-
-  /**
-   * Sets up the parts of the context menu for when when nodes have no children.
-   *
-   * @param {Integer} editFlags The edit flags for the node. See SpellCheckHelper
-   *                            for the details.
-   * @param {nsIDOMNode} rangeParent The parent node for where the selection ends.
-   * @param {Integer} rangeOffset The end position of where the selction ends.
-   */
-  _setTargetForNodesNoChildren(editFlags, rangeParent, rangeOffset) {
-    if (this.target.nodeType == Node.TEXT_NODE) {
-      // For text nodes, look at the parent node to determine the spellcheck attribute.
-      this.canSpellCheck = this.target.parentNode &&
-                           this._isSpellCheckEnabled(this.target);
-      return;
-    }
-
-    // We only deal with TEXT_NODE and ELEMENT_NODE in this function, so return
-    // early if we don't have one.
-    if (this.target.nodeType != Node.ELEMENT_NODE) {
-      return;
-    }
-    // See if the user clicked on an image. This check mirrors
-    // nsDocumentViewer::GetInImage. Make sure to update both if this is
-    // changed.
-    if (this.target instanceof Ci.nsIImageLoadingContent &&
-        this.target.currentURI) {
-      this.onImage = true;
-
-      var request =
-        this.target.getRequest(Ci.nsIImageLoadingContent.CURRENT_REQUEST);
-      if (request && (request.imageStatus & request.STATUS_SIZE_AVAILABLE))
-        this.onLoadedImage = true;
-      if (request &&
-          (request.imageStatus & request.STATUS_LOAD_COMPLETE) &&
-          !(request.imageStatus & request.STATUS_ERROR)) {
-        this.onCompletedImage = true;
-      }
-
-      this.mediaURL = this.target.currentURI.spec;
-
-      var descURL = this.target.getAttribute("longdesc");
-      if (descURL) {
-        this.imageDescURL = makeURLAbsolute(this.ownerDoc.body.baseURI, descURL);
-      }
-    } else if (this.target instanceof HTMLCanvasElement) {
-      this.onCanvas = true;
-    } else if (this.target instanceof HTMLVideoElement) {
-      let mediaURL = this.target.currentSrc || this.target.src;
-      if (this.isMediaURLReusable(mediaURL)) {
-        this.mediaURL = mediaURL;
-      }
-      if (this._isProprietaryDRM()) {
-        this.onDRMMedia = true;
-      }
-      // Firefox always creates a HTMLVideoElement when loading an ogg file
-      // directly. If the media is actually audio, be smarter and provide a
-      // context menu with audio operations.
-      if (this.target.readyState >= this.target.HAVE_METADATA &&
-          (this.target.videoWidth == 0 || this.target.videoHeight == 0)) {
-        this.onAudio = true;
-      } else {
-        this.onVideo = true;
-      }
-    } else if (this.target instanceof HTMLAudioElement) {
-      this.onAudio = true;
-      let mediaURL = this.target.currentSrc || this.target.src;
-      if (this.isMediaURLReusable(mediaURL)) {
-        this.mediaURL = mediaURL;
-      }
-      if (this._isProprietaryDRM()) {
-        this.onDRMMedia = true;
-      }
-    } else if (editFlags & (SpellCheckHelper.INPUT | SpellCheckHelper.TEXTAREA)) {
-      this.onTextInput = (editFlags & SpellCheckHelper.TEXTINPUT) !== 0;
-      this.onNumeric = (editFlags & SpellCheckHelper.NUMERIC) !== 0;
-      this.onEditableArea = (editFlags & SpellCheckHelper.EDITABLE) !== 0;
-      this.onPassword = (editFlags & SpellCheckHelper.PASSWORD) !== 0;
-      if (this.onEditableArea) {
-        if (this.isRemote) {
-          InlineSpellCheckerUI.initFromRemote(gContextMenuContentData.spellInfo);
-        } else {
-          InlineSpellCheckerUI.init(this.target.QueryInterface(Ci.nsIDOMNSEditableElement).editor);
-          InlineSpellCheckerUI.initFromEvent(rangeParent, rangeOffset);
-        }
-      }
-      this.onKeywordField = (editFlags & SpellCheckHelper.KEYWORD);
-    } else if (this.target instanceof HTMLHtmlElement) {
-      var bodyElt = this.ownerDoc.body;
-      if (bodyElt) {
-        let computedURL;
-        try {
-          computedURL = this.getComputedURL(bodyElt, "background-image");
-          this._hasMultipleBGImages = false;
-        } catch (e) {
-          this._hasMultipleBGImages = true;
-        }
-        if (computedURL) {
-          this.hasBGImage = true;
-          this.bgImageURL = makeURLAbsolute(bodyElt.baseURI,
-                                            computedURL);
-        }
-      }
-    } else if ((this.target instanceof HTMLEmbedElement ||
-              this.target instanceof HTMLObjectElement) &&
-             this.target.displayedType == HTMLObjectElement.TYPE_NULL &&
-             this.target.pluginFallbackType == HTMLObjectElement.PLUGIN_CLICK_TO_PLAY) {
-      this.onCTPPlugin = true;
-    }
-
-    this.canSpellCheck = this._isSpellCheckEnabled(this.target);
-  },
-
-  /**
-   * Sets up the parts of the context menu for when when nodes have children.
-   *
-   * @param {Integer} editFlags The edit flags for the node. See SpellCheckHelper
-   *                            for the details.
-   * @param {nsIDOMNode} rangeParent The parent node for where the selection ends.
-   * @param {Integer} rangeOffset The end position of where the selction ends.
-   */
-  _setTargetForNodesWithChildren(editFlags, rangeParent, rangeOffset) {
-    // Second, bubble out, looking for items of interest that can have childen.
-    // Always pick the innermost link, background image, etc.
-    var elem = this.target;
-    while (elem) {
-      if (elem.nodeType == Node.ELEMENT_NODE) {
-        // Link?
-        const XLINKNS = "http://www.w3.org/1999/xlink";
-        if (!this.onLink &&
-            // Be consistent with what hrefAndLinkNodeForClickEvent
-            // does in browser.js
-             (this._isXULTextLinkLabel(elem) ||
-              (elem instanceof HTMLAnchorElement && elem.href) ||
-              (elem instanceof SVGAElement &&
-               (elem.href || elem.hasAttributeNS(XLINKNS, "href"))) ||
-              (elem instanceof HTMLAreaElement && elem.href) ||
-              elem instanceof HTMLLinkElement ||
-              elem.getAttributeNS(XLINKNS, "type") == "simple")) {
-
-          // Target is a link or a descendant of a link.
-          this.onLink = true;
-
-          // Remember corresponding element.
-          this.link = elem;
-          this.linkURL = this.getLinkURL();
-          this.linkURI = this.getLinkURI();
-          this.linkTextStr = this.getLinkText();
-          this.linkProtocol = this.getLinkProtocol();
-          this.onMailtoLink = (this.linkProtocol == "mailto");
-          this.onMozExtLink = (this.linkProtocol == "moz-extension");
-          this.onSaveableLink = this.isLinkSaveable( this.link );
-          this.linkHasNoReferrer = BrowserUtils.linkHasNoReferrer(elem);
-          try {
-            if (elem.download) {
-              // Ignore download attribute on cross-origin links
-              this.principal.checkMayLoad(this.linkURI, false, true);
-              this.linkDownload = elem.download;
-            }
-          } catch (ex) {}
-        }
-
-        // Background image?  Don't bother if we've already found a
-        // background image further down the hierarchy.  Otherwise,
-        // we look for the computed background-image style.
-        if (!this.hasBGImage &&
-            !this._hasMultipleBGImages) {
-          let bgImgUrl;
-          try {
-            bgImgUrl = this.getComputedURL(elem, "background-image");
-            this._hasMultipleBGImages = false;
-          } catch (e) {
-            this._hasMultipleBGImages = true;
-          }
-          if (bgImgUrl) {
-            this.hasBGImage = true;
-            this.bgImageURL = makeURLAbsolute(elem.baseURI,
-                                              bgImgUrl);
-          }
-        }
-      }
-
-      elem = elem.parentNode;
-    }
-
-    // See if the user clicked on MathML
-    const NS_MathML = "http://www.w3.org/1998/Math/MathML";
-    if ((this.target.nodeType == Node.TEXT_NODE &&
-         this.target.parentNode.namespaceURI == NS_MathML)
-         || (this.target.namespaceURI == NS_MathML))
-      this.onMathML = true;
-
-    // See if the user clicked in a frame.
-    var docDefaultView = this.ownerDoc.defaultView;
-    if (docDefaultView != docDefaultView.top) {
-      this.inFrame = true;
-
-      if (this.ownerDoc.isSrcdocDocument) {
-          this.inSrcdocFrame = true;
-      }
-    }
-
-    // if the document is editable, show context menu like in text inputs
-    if (!this.onEditableArea) {
-      if (editFlags & SpellCheckHelper.CONTENTEDITABLE) {
-        // If this.onEditableArea is false but editFlags is CONTENTEDITABLE, then
-        // the document itself must be editable.
-        this.onTextInput       = true;
-        this.onKeywordField    = false;
-        this.onImage           = false;
-        this.onLoadedImage     = false;
-        this.onCompletedImage  = false;
-        this.onMathML          = false;
-        this.inFrame           = false;
-        this.inSrcdocFrame     = false;
-        this.hasBGImage        = false;
-        this.isDesignMode      = true;
-        this.onEditableArea = true;
-        if (this.isRemote) {
-          InlineSpellCheckerUI.initFromRemote(gContextMenuContentData.spellInfo);
-        } else {
-          var targetWin = this.ownerDoc.defaultView;
-          var editingSession = targetWin.QueryInterface(Ci.nsIInterfaceRequestor)
-                                        .getInterface(Ci.nsIWebNavigation)
-                                        .QueryInterface(Ci.nsIInterfaceRequestor)
-                                        .getInterface(Ci.nsIEditingSession);
-          InlineSpellCheckerUI.init(editingSession.getEditorForWindow(targetWin));
-          InlineSpellCheckerUI.initFromEvent(rangeParent, rangeOffset);
-        }
-        var canSpell = InlineSpellCheckerUI.canSpellCheck && this.canSpellCheck;
-        this.showItem("spell-check-enabled", canSpell);
-        this.showItem("spell-separator", canSpell);
-      }
-    }
-  },
-
-  /**
-   * Determines if a node is a XUL Text link.
-   *
-   * @param {Object} node The object to test.
-   * @returns {Boolean} true if the object is a XUL text link.
-   */
-  _isXULTextLinkLabel(node) {
-    const xulNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
-    return node.namespaceURI == xulNS &&
-           node.tagName == "label" &&
-           node.classList.contains("text-link") &&
-           node.href;
-  },
-
-  // Returns the computed style attribute for the given element.
-  getComputedStyle(aElem, aProp) {
-    return aElem.ownerGlobal
-                .getComputedStyle(aElem).getPropertyValue(aProp);
-  },
-
-  // Returns a "url"-type computed style attribute value, with the url() stripped.
-  getComputedURL(aElem, aProp) {
-    var url = aElem.ownerGlobal.getComputedStyle(aElem)
-                   .getPropertyCSSValue(aProp);
-    if (url instanceof CSSValueList) {
-      if (url.length != 1)
-        throw "found multiple URLs";
-      url = url[0];
-    }
-    return url.primitiveType == CSSPrimitiveValue.CSS_URI ?
-           url.getStringValue() : null;
-  },
-
-  // Returns true if clicked-on link targets a resource that can be saved.
-  isLinkSaveable(aLink) {
-    // We don't do the Right Thing for news/snews yet, so turn them off
-    // until we do.
-    return this.linkProtocol && !(
-             this.linkProtocol == "mailto" ||
-             this.linkProtocol == "javascript" ||
-             this.linkProtocol == "news" ||
-             this.linkProtocol == "snews");
-  },
-
-  _isSpellCheckEnabled(aNode) {
-    // We can always force-enable spellchecking on textboxes
-    if (this.isTargetATextBox(aNode)) {
-      return true;
-    }
-    // We can never spell check something which is not content editable
-    var editable = aNode.isContentEditable;
-    if (!editable && aNode.ownerDocument) {
-      editable = aNode.ownerDocument.designMode == "on";
-    }
-    if (!editable) {
-      return false;
-    }
-    // Otherwise make sure that nothing in the parent chain disables spellchecking
-    return aNode.spellcheck;
-  },
-
-  _isProprietaryDRM() {
-    return this.target.isEncrypted && this.target.mediaKeys &&
-           this.target.mediaKeys.keySystem != "org.w3.clearkey";
-  },
-
   _openLinkInParameters(extra) {
     let params = { charset: gContextMenuContentData.charSet,
                    originPrincipal: this.principal,
                    triggeringPrincipal: this.principal,
                    referrerURI: gContextMenuContentData.documentURIObject,
                    referrerPolicy: gContextMenuContentData.referrerPolicy,
                    frameOuterWindowID: gContextMenuContentData.frameOuterWindowID,
                    noReferrer: this.linkHasNoReferrer };
@@ -1189,17 +886,17 @@ nsContextMenu.prototype = {
   },
 
   viewInfo() {
     BrowserPageInfo(gContextMenuContentData.docLocation, null, null, null, this.browser);
   },
 
   viewImageInfo() {
     BrowserPageInfo(gContextMenuContentData.docLocation, "mediaTab",
-                    this.target, null, this.browser);
+                    this.imageInfo, null, this.browser);
   },
 
   viewImageDesc(e) {
     urlSecurityCheck(this.imageDescURL,
                      this.browser.contentPrincipal,
                      Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT);
     openUILink(this.imageDescURL, e, { disallowInheritPrincipal: true,
                                        referrerURI: gContextMenuContentData.documentURIObject });
@@ -1343,17 +1040,17 @@ nsContextMenu.prototype = {
   // Save URL of clicked-on frame.
   saveFrame() {
     saveBrowser(this.browser, false, this.frameOuterWindowID);
   },
 
   // Helper function to wait for appropriate MIME-type headers and
   // then prompt the user with a file picker
   saveHelper(linkURL, linkText, dialogTitle, bypassCache, doc, docURI,
-             windowID, linkDownload) {
+             windowID, linkDownload, isContentWindowPrivate) {
     // canonical def in nsURILoader.h
     const NS_ERROR_SAVE_LINK_AS_TIMEOUT = 0x805d0020;
 
     // an object to proxy the data through to
     // nsIExternalHelperAppService.doContent, which will wait for the
     // appropriate MIME-type headers and then prompt the user with a
     // file picker
     function saveAsListener() {}
@@ -1402,17 +1099,17 @@ nsContextMenu.prototype = {
       },
 
       onStopRequest: function saveLinkAs_onStopRequest(aRequest, aContext,
                                                        aStatusCode) {
         if (aStatusCode == NS_ERROR_SAVE_LINK_AS_TIMEOUT) {
           // do it the old fashioned way, which will pick the best filename
           // it can without waiting.
           saveURL(linkURL, linkText, dialogTitle, bypassCache, false, docURI,
-                  doc);
+                  doc, isContentWindowPrivate);
         }
         if (this.extListener)
           this.extListener.onStopRequest(aRequest, aContext, aStatusCode);
       },
 
       onDataAvailable: function saveLinkAs_onDataAvailable(aRequest, aContext,
                                                            aInputStream,
                                                            aOffset, aCount) {
@@ -1491,31 +1188,35 @@ nsContextMenu.prototype = {
 
     // kick off the channel with our proxy object as the listener
     channel.asyncOpen2(new saveAsListener());
   },
 
   // Save URL of clicked-on link.
   saveLink() {
     urlSecurityCheck(this.linkURL, this.principal);
+
+    let isContentWindowPrivate = this.isRemote ? this.ownerDoc.isPrivate : undefined;
     this.saveHelper(this.linkURL, this.linkTextStr, null, true, this.ownerDoc,
                     gContextMenuContentData.documentURIObject,
                     this.frameOuterWindowID,
-                    this.linkDownload);
+                    this.linkDownload,
+                    isContentWindowPrivate);
   },
 
   // Backwards-compatibility wrapper
   saveImage() {
     if (this.onCanvas || this.onImage)
         this.saveMedia();
   },
 
   // Save URL of the clicked upon image, video, or audio.
   saveMedia() {
     let doc = this.ownerDoc;
+    let isContentWindowPrivate = this.isRemote ? this.ownerDoc.isPrivate : undefined;
     let referrerURI = gContextMenuContentData.documentURIObject;
     let isPrivate = PrivateBrowsingUtils.isBrowserPrivate(this.browser);
     if (this.onCanvas) {
       // Bypass cache, since it's a data: URL.
       this._canvasToBlobURL(this.target).then(function(blobURL) {
         saveImageURL(blobURL, "canvas.png", "SaveImageTitle",
                      true, false, referrerURI, null, null, null,
                      isPrivate);
@@ -1524,49 +1225,30 @@ nsContextMenu.prototype = {
       urlSecurityCheck(this.mediaURL, this.principal);
       saveImageURL(this.mediaURL, null, "SaveImageTitle", false,
                    false, referrerURI, null, gContextMenuContentData.contentType,
                    gContextMenuContentData.contentDisposition, isPrivate);
     } else if (this.onVideo || this.onAudio) {
       urlSecurityCheck(this.mediaURL, this.principal);
       var dialogTitle = this.onVideo ? "SaveVideoTitle" : "SaveAudioTitle";
       this.saveHelper(this.mediaURL, null, dialogTitle, false, doc, referrerURI,
-                      this.frameOuterWindowID, "");
+                      this.frameOuterWindowID, "", isContentWindowPrivate);
     }
   },
 
   // Backwards-compatibility wrapper
   sendImage() {
     if (this.onCanvas || this.onImage)
         this.sendMedia();
   },
 
   sendMedia() {
     MailIntegration.sendMessage(this.mediaURL, "");
   },
 
-  castVideo() {
-    CastingApps.openExternal(this.target, window);
-  },
-
-  populateCastVideoMenu(popup) {
-    let videoEl = this.target;
-    popup.innerHTML = null;
-    let doc = popup.ownerDocument;
-    let services = CastingApps.getServicesForVideo(videoEl);
-    services.forEach(service => {
-      let item = doc.createElement("menuitem");
-      item.setAttribute("label", service.friendlyName);
-      item.addEventListener("command", event => {
-        CastingApps.sendVideoToService(videoEl, service);
-      });
-      popup.appendChild(item);
-    });
-  },
-
   playPlugin() {
     gPluginHandler.contextMenuCommand(this.browser, this.target, "play");
   },
 
   hidePlugin() {
     gPluginHandler.contextMenuCommand(this.browser, this.target, "hide");
   },
 
@@ -1662,99 +1344,31 @@ nsContextMenu.prototype = {
       var attr = attrs.item(i);
       node.setAttribute(attr.nodeName, attr.nodeValue);
     }
 
     // Voila!
     return node;
   },
 
-  // Generate fully qualified URL for clicked-on link.
-  getLinkURL() {
-    var href = this.link.href;
-    if (href) {
-      // Handle SVG links:
-      if (typeof href == "object" && href.animVal) {
-        return href.animVal;
-      }
-      return href;
-    }
-
-    href = this.link.getAttribute("href") ||
-           this.link.getAttributeNS("http://www.w3.org/1999/xlink", "href");
-
-    if (!href || !href.match(/\S/)) {
-      // Without this we try to save as the current doc,
-      // for example, HTML case also throws if empty
-      throw "Empty href";
-    }
-
-    return makeURLAbsolute(this.link.baseURI, href);
-  },
-
   getLinkURI() {
     try {
       return makeURI(this.linkURL);
     } catch (ex) {
      // e.g. empty URL string
     }
 
     return null;
   },
 
-  getLinkProtocol() {
-    if (this.linkURI)
-      return this.linkURI.scheme; // can be |undefined|
-
-    return null;
-  },
-
-  // Get text of link.
-  getLinkText() {
-    var text = gatherTextUnder(this.link);
-    if (!text || !text.match(/\S/)) {
-      text = this.link.getAttribute("title");
-      if (!text || !text.match(/\S/)) {
-        text = this.link.getAttribute("alt");
-        if (!text || !text.match(/\S/))
-          text = this.linkURL;
-      }
-    }
-
-    return text;
-  },
-
   // Kept for addon compat
   linkText() {
     return this.linkTextStr;
   },
 
-  isMediaURLReusable(aURL) {
-    if (aURL.startsWith("blob:")) {
-      return URL.isValidURL(aURL);
-    }
-    return true;
-  },
-
-  toString() {
-    return "contextMenu.target     = " + this.target + "\n" +
-           "contextMenu.onImage    = " + this.onImage + "\n" +
-           "contextMenu.onLink     = " + this.onLink + "\n" +
-           "contextMenu.link       = " + this.link + "\n" +
-           "contextMenu.inFrame    = " + this.inFrame + "\n" +
-           "contextMenu.hasBGImage = " + this.hasBGImage + "\n";
-  },
-
-  isTargetATextBox(node) {
-    if (node instanceof HTMLInputElement)
-      return node.mozIsTextField(false);
-
-    return (node instanceof HTMLTextAreaElement);
-  },
-
   // Determines whether or not the separator with the specified ID should be
   // shown or not by determining if there are any non-hidden items between it
   // and the previous separator.
   shouldShowSeparator(aSeparatorID) {
     var separator = document.getElementById(aSeparatorID);
     if (separator) {
       var sibling = separator.previousSibling;
       while (sibling && sibling.localName != "menuseparator") {
--- a/browser/base/content/pageinfo/pageInfo.js
+++ b/browser/base/content/pageinfo/pageInfo.js
@@ -343,34 +343,34 @@ function loadPageInfo(frameOuterWindowID
   let mm = browser.messageManager;
 
   gStrings["application/rss+xml"]  = gBundle.getString("feedRss");
   gStrings["application/atom+xml"] = gBundle.getString("feedAtom");
   gStrings["text/xml"]             = gBundle.getString("feedXML");
   gStrings["application/xml"]      = gBundle.getString("feedXML");
   gStrings["application/rdf+xml"]  = gBundle.getString("feedXML");
 
+  let imageInfo = imageElement;
+
   // Look for pageInfoListener in content.js. Sends message to listener with arguments.
-  mm.sendAsyncMessage("PageInfo:getData", {strings: gStrings,
-                      frameOuterWindowID},
-                      { imageElement });
+  mm.sendAsyncMessage("PageInfo:getData", {strings: gStrings, frameOuterWindowID});
 
   let pageInfoData;
 
   // Get initial pageInfoData needed to display the general, feeds, permission and security tabs.
   mm.addMessageListener("PageInfo:data", function onmessage(message) {
     mm.removeMessageListener("PageInfo:data", onmessage);
     pageInfoData = message.data;
     let docInfo = pageInfoData.docInfo;
     let windowInfo = pageInfoData.windowInfo;
     let uri = makeURI(docInfo.documentURIObject.spec);
     let principal = docInfo.principal;
     gDocInfo = docInfo;
 
-    gImageElement = pageInfoData.imageInfo;
+    gImageElement = imageInfo;
 
     var titleFormat = windowInfo.isTopWindow ? "pageInfo.page.title"
                                              : "pageInfo.frame.title";
     document.title = gBundle.getFormattedString(titleFormat, [docInfo.location]);
 
     document.getElementById("main-window").setAttribute("relatedUrl", docInfo.location);
 
     makeGeneralTab(pageInfoData.metaViewRows, docInfo);
--- a/browser/base/content/tab-content.js
+++ b/browser/base/content/tab-content.js
@@ -88,29 +88,16 @@ addMessageListener("Browser:Reload", fun
   } catch (e) {
   }
 });
 
 addMessageListener("MixedContent:ReenableProtection", function() {
   docShell.mixedContentChannel = null;
 });
 
-addMessageListener("SecondScreen:tab-mirror", function(message) {
-  if (!Services.prefs.getBoolPref("browser.casting.enabled")) {
-    return;
-  }
-  let app = SimpleServiceDiscovery.findAppForService(message.data.service);
-  if (app) {
-    let width = content.innerWidth;
-    let height = content.innerHeight;
-    let viewport = {cssWidth: width, cssHeight: height, width, height};
-    app.mirror(function() {}, content, viewport, function() {}, content);
-  }
-});
-
 var AboutHomeListener = {
   init(chromeGlobal) {
     chromeGlobal.addEventListener("AboutHomeLoad", this, false, true);
   },
 
   get isAboutHome() {
     return content.document.documentURI.toLowerCase() == "about:home";
   },
--- a/browser/base/content/test/forms/browser_selectpopup.js
+++ b/browser/base/content/test/forms/browser_selectpopup.js
@@ -6,34 +6,45 @@
 // single-process as a <menulist> is used to implement the dropdown list.
 
 requestLongerTimeout(2);
 
 const XHTML_DTD = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">';
 
 const PAGECONTENT =
   "<html xmlns='http://www.w3.org/1999/xhtml'>" +
-  "<body onload='gChangeEvents = 0;gInputEvents = 0; gClickEvents = 0; document.body.firstChild.focus()'>" +
-  "<select oninput='gInputEvents++' onchange='gChangeEvents++' onclick='if (event.target == this) gClickEvents++'>" +
+  "<body onload='gChangeEvents = 0;gInputEvents = 0; gClickEvents = 0; document.getElementById(\"select\").focus();'>" +
+  "<select id='select' oninput='gInputEvents++' onchange='gChangeEvents++' onclick='if (event.target == this) gClickEvents++'>" +
   "  <optgroup label='First Group'>" +
   "    <option value='One'>One</option>" +
   "    <option value='Two'>Two</option>" +
   "  </optgroup>" +
   "  <option value='Three'>Three</option>" +
   "  <optgroup label='Second Group' disabled='true'>" +
   "    <option value='Four'>Four</option>" +
   "    <option value='Five'>Five</option>" +
   "  </optgroup>" +
   "  <option value='Six' disabled='true'>Six</option>" +
   "  <optgroup label='Third Group'>" +
   "    <option value='Seven'>   Seven  </option>" +
   "    <option value='Eight'>&nbsp;&nbsp;Eight&nbsp;&nbsp;</option>" +
   "  </optgroup></select><input />Text" +
   "</body></html>";
 
+const PAGECONTENT_XSLT =
+  "<?xml-stylesheet type='text/xml' href='#style1'?>" +
+  "<xsl:stylesheet id='style1'" +
+  "                version='1.0'" +
+  "                xmlns:xsl='http://www.w3.org/1999/XSL/Transform'" +
+  "                xmlns:html='http://www.w3.org/1999/xhtml'>" +
+  "<xsl:template match='xsl:stylesheet'>" +
+  PAGECONTENT +
+  "</xsl:template>" +
+  "</xsl:stylesheet>";
+
 const PAGECONTENT_SMALL =
   "<html>" +
   "<body><select id='one'>" +
   "  <option value='One'>One</option>" +
   "  <option value='Two'>Two</option>" +
   "</select><select id='two'>" +
   "  <option value='Three'>Three</option>" +
   "  <option value='Four'>Four</option>" +
@@ -111,18 +122,18 @@ function getChangeEvents() {
 }
 
 function getClickEvents() {
   return ContentTask.spawn(gBrowser.selectedBrowser, {}, function() {
     return content.wrappedJSObject.gClickEvents;
   });
 }
 
-async function doSelectTests(contentType, dtd) {
-  const pageUrl = "data:" + contentType + "," + escape(dtd + "\n" + PAGECONTENT);
+async function doSelectTests(contentType, content) {
+  const pageUrl = "data:" + contentType + "," + encodeURIComponent(content);
   let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl);
 
   let menulist = document.getElementById("ContentSelectDropdown");
   let selectPopup = menulist.menupopup;
 
   await openSelectPopup(selectPopup);
 
   let isWindows = navigator.platform.indexOf("Win") >= 0;
@@ -216,21 +227,25 @@ add_task(async function setup() {
     "set": [
       ["dom.select_popup_in_parent.enabled", true],
       ["dom.forms.select.customstyling", true]
     ]
   });
 });
 
 add_task(async function() {
-  await doSelectTests("text/html", "");
+  await doSelectTests("text/html", PAGECONTENT);
 });
 
 add_task(async function() {
-  await doSelectTests("application/xhtml+xml", XHTML_DTD);
+  await doSelectTests("application/xhtml+xml", XHTML_DTD + "\n" + PAGECONTENT);
+});
+
+add_task(async function() {
+  await doSelectTests("application/xml", XHTML_DTD + "\n" + PAGECONTENT_XSLT);
 });
 
 // This test opens a select popup and removes the content node of a popup while
 // The popup should close if its node is removed.
 add_task(async function() {
   const pageUrl = "data:text/html," + escape(PAGECONTENT_SMALL);
   let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl);
 
--- a/browser/base/content/test/general/browser_addKeywordSearch.js
+++ b/browser/base/content/test/general/browser_addKeywordSearch.js
@@ -24,17 +24,17 @@ add_task(async function() {
   let count = 0;
   for (let method of ["GET", "POST"]) {
     for (let {desc, action, param } of testData) {
       info(`Running ${method} keyword test '${desc}'`);
       let id = `keyword-form-${count++}`;
       let contextMenu = document.getElementById("contentAreaContextMenu");
       let contextMenuPromise =
         BrowserTestUtils.waitForEvent(contextMenu, "popupshown")
-                        .then(() => gContextMenuContentData.popupNode);
+                        .then(() => gContextMenuContentData.target);
 
       await ContentTask.spawn(tab.linkedBrowser,
                               { action, param, method, id }, async function(args) {
         let doc = content.document;
         let form = doc.createElement("form");
         form.id = args.id;
         form.method = args.method;
         form.action = args.action;
--- a/browser/base/content/test/general/browser_contextmenu.js
+++ b/browser/base/content/test/general/browser_contextmenu.js
@@ -18,17 +18,22 @@ const chrome_base = "chrome://mochitests
 
 /* import-globals-from contextmenu_common.js */
 Services.scriptloader.loadSubScript(chrome_base + "contextmenu_common.js", this);
 
 // Below are test cases for XUL element
 add_task(async function test_xul_text_link_label() {
   let url = chrome_base + "subtst_contextmenu_xul.xul";
 
-  await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+  await BrowserTestUtils.openNewForegroundTab({
+    gBrowser,
+    url,
+    waitForLoad: true,
+    waitForStateStop: true,
+  });
 
   await test_contextmenu("#test-xul-text-link-label",
     ["context-openlinkintab", true,
      ...(hasContainers ? ["context-openlinkinusercontext-menu", true] : []),
      // We need a blank entry here because the containers submenu is
      // dynamically generated with no ids.
      ...(hasContainers ? ["", null] : []),
      "context-openlink",      true,
@@ -174,18 +179,16 @@ add_task(async function test_video_ok() 
      "context-video-fullscreen",   true,
      "---",                        null,
      "context-viewvideo",          true,
      "context-copyvideourl",       true,
      "---",                        null,
      "context-savevideo",          true,
      "context-video-saveimage",    true,
      "context-sendvideo",          true,
-     "context-castvideo",          null,
-       [], null
     ]
   );
 });
 
 add_task(async function test_audio_in_video() {
   await test_contextmenu("#test-audio-in-video",
     ["context-media-play",         true,
      "context-media-mute",         true,
@@ -221,18 +224,16 @@ add_task(async function test_video_bad()
      "context-video-fullscreen",   false,
      "---",                        null,
      "context-viewvideo",          true,
      "context-copyvideourl",       true,
      "---",                        null,
      "context-savevideo",          true,
      "context-video-saveimage",    false,
      "context-sendvideo",          true,
-     "context-castvideo",          null,
-       [], null
     ]
   );
 });
 
 add_task(async function test_video_bad2() {
   await test_contextmenu("#test-video-bad2",
     ["context-media-play",         false,
      "context-media-mute",         false,
@@ -247,18 +248,16 @@ add_task(async function test_video_bad2(
      "context-video-fullscreen",   false,
      "---",                        null,
      "context-viewvideo",          false,
      "context-copyvideourl",       false,
      "---",                        null,
      "context-savevideo",          false,
      "context-video-saveimage",    false,
      "context-sendvideo",          false,
-     "context-castvideo",          null,
-       [], null
     ]
   );
 });
 
 add_task(async function test_iframe() {
   await test_contextmenu("#test-iframe",
     ["context-navigation", null,
          ["context-back",         false,
@@ -309,18 +308,16 @@ add_task(async function test_video_in_if
      "context-video-fullscreen",   true,
      "---",                        null,
      "context-viewvideo",          true,
      "context-copyvideourl",       true,
      "---",                        null,
      "context-savevideo",          true,
      "context-video-saveimage",    true,
      "context-sendvideo",          true,
-     "context-castvideo",          null,
-       [], null,
      "frame",                null,
          ["context-showonlythisframe", true,
           "context-openframeintab",    true,
           "context-openframe",         true,
           "---",                       null,
           "context-reloadframe",       true,
           "---",                       null,
           "context-bookmarkframe",     true,
--- a/browser/base/content/test/pageinfo/browser_pageinfo_image_info.js
+++ b/browser/base/content/test/pageinfo/browser_pageinfo_image_info.js
@@ -1,20 +1,28 @@
 /* Make sure that "View Image Info" loads the correct image data */
+function getImageInfo(imageElement) {
+  return {
+    currentSrc: imageElement.currentSrc,
+    width: imageElement.width,
+    height: imageElement.height,
+    imageText: imageElement.title || imageElement.alt
+  };
+}
 
 function test() {
   waitForExplicitFinish();
 
   gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser);
 
   gBrowser.selectedBrowser.addEventListener("load", function() {
     var doc = gBrowser.contentDocument;
     var testImg = doc.getElementById("test-image");
     var pageInfo = BrowserPageInfo(gBrowser.selectedBrowser.currentURI.spec,
-                                   "mediaTab", testImg);
+                                   "mediaTab", getImageInfo(testImg));
 
     pageInfo.addEventListener("load", function() {
       pageInfo.onFinished.push(function() {
         var pageInfoImg = pageInfo.document.getElementById("thepreviewimage");
         pageInfoImg.addEventListener("loadend", function() {
 
           is(pageInfoImg.src, testImg.src, "selected image has the correct source");
           is(pageInfoImg.width, testImg.width, "selected image has the correct width");
--- a/browser/components/extensions/ext-browserAction.js
+++ b/browser/components/extensions/ext-browserAction.js
@@ -643,16 +643,19 @@ this.browserAction = class extends Exten
           let tab = getTab(details.tabId);
 
           // Note: Chrome resolves arguments to setIcon relative to the calling
           // context, but resolves arguments to setPopup relative to the extension
           // root.
           // For internal consistency, we currently resolve both relative to the
           // calling context.
           let url = details.popup && context.uri.resolve(details.popup);
+          if (url && !context.checkLoadURL(url)) {
+            return Promise.reject({message: `Access denied for URL ${url}`});
+          }
           browserAction.setProperty(tab, "popup", url);
         },
 
         getPopup: function(details) {
           let tab = getTab(details.tabId);
 
           let popup = browserAction.getProperty(tab, "popup");
           return Promise.resolve(popup);
--- a/browser/components/extensions/ext-pageAction.js
+++ b/browser/components/extensions/ext-pageAction.js
@@ -324,16 +324,19 @@ this.pageAction = class extends Extensio
           let tab = tabTracker.getTab(details.tabId);
 
           // Note: Chrome resolves arguments to setIcon relative to the calling
           // context, but resolves arguments to setPopup relative to the extension
           // root.
           // For internal consistency, we currently resolve both relative to the
           // calling context.
           let url = details.popup && context.uri.resolve(details.popup);
+          if (url && !context.checkLoadURL(url)) {
+            return Promise.reject({message: `Access denied for URL ${url}`});
+          }
           pageAction.setProperty(tab, "popup", url);
         },
 
         getPopup(details) {
           let tab = tabTracker.getTab(details.tabId);
 
           let popup = pageAction.getProperty(tab, "popup");
           return Promise.resolve(popup);
--- a/browser/components/extensions/ext-sidebarAction.js
+++ b/browser/components/extensions/ext-sidebarAction.js
@@ -412,16 +412,19 @@ this.sidebarAction = class extends Exten
           let nativeTab = getTab(details.tabId);
 
           let url;
           // Clear the tab-specific url when given a null string.
           if (nativeTab && details.panel === "") {
             url = null;
           } else if (details.panel !== "") {
             url = context.uri.resolve(details.panel);
+            if (!context.checkLoadURL(url)) {
+              return Promise.reject({message: `Access denied for URL ${url}`});
+            }
           } else {
             throw new ExtensionError("Invalid url for sidebar panel.");
           }
 
           sidebarAction.setProperty(nativeTab, "panel", url);
         },
 
         getPanel(details) {
--- a/browser/components/extensions/schemas/page_action.json
+++ b/browser/components/extensions/schemas/page_action.json
@@ -172,16 +172,17 @@
             "optional": true,
             "parameters": []
           }
         ]
       },
       {
         "name": "setPopup",
         "type": "function",
+        "async": true,
         "description": "Sets the html document to be opened as a popup when the user clicks on the page action's icon.",
         "parameters": [
           {
             "name": "details",
             "type": "object",
             "properties": {
               "tabId": {"type": "integer", "minimum": 0, "description": "The id of the tab for which you want to modify the page action."},
               "popup": {
--- a/browser/components/extensions/test/browser/browser_ext_browserAction_context.js
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_context.js
@@ -389,12 +389,21 @@ add_task(async function testDefaultTitle
         },
         async expect => {
           browser.test.log("Set default title to null string. Expect null string from API, extension title in UI.");
           browser.browserAction.setTitle({title: ""});
 
           await expectDefaults(details[3]);
           expect(details[3]);
         },
+        async expect => {
+          browser.test.assertRejects(
+            browser.browserAction.setPopup({popup: "about:addons"}),
+            /Access denied for URL about:addons/,
+            "unable to set popup to about:addons");
+
+          await expectDefaults(details[3]);
+          expect(details[3]);
+        },
       ];
     },
   });
 });
--- a/browser/components/extensions/test/browser/browser_ext_pageAction_context.js
+++ b/browser/components/extensions/test/browser/browser_ext_pageAction_context.js
@@ -165,12 +165,20 @@ add_task(async function testTabSwitchCon
           expect(details[1]);
         },
         async expect => {
           browser.test.log("Hide the icon. Expect hidden.");
 
           await browser.pageAction.hide(tabs[0]);
           expect(null);
         },
+        async expect => {
+          browser.test.assertRejects(
+            browser.pageAction.setPopup({tabId: tabs[0], popup: "about:addons"}),
+            /Access denied for URL about:addons/,
+            "unable to set popup to about:addons");
+
+          expect(null);
+        },
       ];
     },
   });
 });
--- a/browser/components/extensions/test/browser/browser_ext_sidebarAction_context.js
+++ b/browser/components/extensions/test/browser/browser_ext_sidebarAction_context.js
@@ -370,12 +370,21 @@ add_task(async function testDefaultTitle
         },
         async expect => {
           browser.test.log("Set default title to null string. Expect null string from API, extension title in UI.");
           browser.sidebarAction.setTitle({title: ""});
 
           await expectDefaults(details[3]);
           expect(details[3]);
         },
+        async expect => {
+          browser.test.assertRejects(
+            browser.sidebarAction.setPanel({panel: "about:addons"}),
+            /Access denied for URL about:addons/,
+            "unable to set panel to about:addons");
+
+          await expectDefaults(details[3]);
+          expect(details[3]);
+        },
       ];
     },
   });
 });
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -961,50 +961,26 @@ BrowserGlue.prototype = {
     BrowserUsageTelemetry.uninit();
 
     PageThumbs.uninit();
     NewTabUtils.uninit();
     AutoCompletePopup.uninit();
     DateTimePickerHelper.uninit();
   },
 
-  _initServiceDiscovery() {
-    if (!Services.prefs.getBoolPref("browser.casting.enabled")) {
-      return;
-    }
-    var rokuDevice = {
-      id: "roku:ecp",
-      target: "roku:ecp",
-      factory(aService) {
-        Cu.import("resource://gre/modules/RokuApp.jsm");
-        return new RokuApp(aService);
-      },
-      types: ["video/mp4"],
-      extensions: ["mp4"]
-    };
-
-    // Register targets
-    SimpleServiceDiscovery.registerDevice(rokuDevice);
-
-    // Search for devices continuously every 120 seconds
-    SimpleServiceDiscovery.search(120 * 1000);
-  },
-
   // All initial windows have opened.
   _onWindowsRestored: function BG__onWindowsRestored() {
     if (this._windowsWereRestored) {
       return;
     }
     this._windowsWereRestored = true;
 
     BrowserUsageTelemetry.init();
     BrowserUITelemetry.init();
 
-    this._initServiceDiscovery();
-
     // Show update notification, if needed.
     if (Services.prefs.prefHasUserValue("app.update.postupdate"))
       this._showUpdateNotification();
 
     ExtensionsUI.init();
 
     let signingRequired;
     if (AppConstants.MOZ_REQUIRE_SIGNING) {
--- a/browser/components/search/test/browser_oneOffContextMenu.js
+++ b/browser/components/search/test/browser_oneOffContextMenu.js
@@ -30,24 +30,17 @@ add_task(async function init() {
     searchbar, "anonid", "searchbar-search-button"
   );
 
   await promiseNewEngine(TEST_ENGINE_BASENAME, {
     setAsCurrent: false,
   });
 });
 
-add_task(async function extendedTelemetryDisabled() {
-  await SpecialPowers.pushPrefEnv({set: [["toolkit.telemetry.enabled", false]]});
-  await doTest();
-  checkTelemetry("other");
-});
-
-add_task(async function extendedTelemetryEnabled() {
-  await SpecialPowers.pushPrefEnv({set: [["toolkit.telemetry.enabled", true]]});
+add_task(async function telemetry() {
   await doTest();
   checkTelemetry("other-" + TEST_ENGINE_NAME);
 });
 
 async function doTest() {
   // Open the popup.
   let promise = promiseEvent(searchPopup, "popupshown");
   info("Opening search panel");
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -576,18 +576,16 @@ These should match what Safari and other
 <!ENTITY saveVideoCmd.label           "Save Video As…">
 <!ENTITY saveVideoCmd.accesskey       "v">
 <!ENTITY saveAudioCmd.label           "Save Audio As…">
 <!ENTITY saveAudioCmd.accesskey       "v">
 <!ENTITY emailImageCmd.label          "Email Image…">
 <!ENTITY emailImageCmd.accesskey      "g">
 <!ENTITY emailVideoCmd.label          "Email Video…">
 <!ENTITY emailVideoCmd.accesskey      "a">
-<!ENTITY castVideoCmd.label           "Send Video To Device">
-<!ENTITY castVideoCmd.accesskey       "e">
 <!ENTITY emailAudioCmd.label          "Email Audio…">
 <!ENTITY emailAudioCmd.accesskey      "a">
 <!ENTITY playPluginCmd.label          "Activate this plugin">
 <!ENTITY playPluginCmd.accesskey      "c">
 <!ENTITY hidePluginCmd.label          "Hide this plugin">
 <!ENTITY hidePluginCmd.accesskey      "H">
 <!ENTITY copyLinkCmd.label            "Copy Link Location">
 <!ENTITY copyLinkCmd.accesskey        "a">
--- a/browser/modules/BrowserUsageTelemetry.jsm
+++ b/browser/modules/BrowserUsageTelemetry.jsm
@@ -107,22 +107,17 @@ function getTabCount() {
   return getOpenTabsAndWinsCounts().tabCount;
 }
 
 function getSearchEngineId(engine) {
   if (engine) {
     if (engine.identifier) {
       return engine.identifier;
     }
-    // Due to bug 1222070, we can't directly check Services.telemetry.canRecordExtended
-    // here.
-    const extendedTelemetry = Services.prefs.getBoolPref("toolkit.telemetry.enabled");
-    if (engine.name && extendedTelemetry) {
-      // If it's a custom search engine only report the engine name
-      // if extended Telemetry is enabled.
+    if (engine.name) {
       return "other-" + engine.name;
     }
   }
   return "other";
 }
 
 let URICountListener = {
   // A set containing the visited domains, see bug 1271310.
new file mode 100644
--- /dev/null
+++ b/browser/modules/ContextMenu.jsm
@@ -0,0 +1,1037 @@
+/* -*- mode: js; indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 sw=2 sts=2 et 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/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["ContextMenu"];
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.importGlobalProperties(["URL"]);
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+  E10SUtils: "resource:///modules/E10SUtils.jsm",
+  CastingApps: "resource:///modules/CastingApps.jsm",
+  BrowserUtils: "resource://gre/modules/BrowserUtils.jsm",
+  PlacesUIUtils: "resource:///modules/PlacesUIUtils.jsm",
+  findCssSelector: "resource://gre/modules/css-selector.js",
+  SpellCheckHelper: "resource://gre/modules/InlineSpellChecker.jsm",
+  LoginManagerContent: "resource://gre/modules/LoginManagerContent.jsm",
+  WebNavigationFrames: "resource://gre/modules/WebNavigationFrames.jsm",
+  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
+  InlineSpellCheckerContent: "resource://gre/modules/InlineSpellCheckerContent.jsm",
+});
+
+XPCOMUtils.defineLazyGetter(this, "PageMenuChild", () => {
+  let tmp = {};
+  Cu.import("resource://gre/modules/PageMenu.jsm", tmp);
+  return new tmp.PageMenuChild();
+});
+
+const messageListeners = {
+  "ContextMenu:BookmarkFrame": function(aMessage) {
+    let frame = this.getTarget(aMessage).ownerDocument;
+
+    this.global.sendAsyncMessage("ContextMenu:BookmarkFrame:Result",
+                                 { title: frame.title,
+                                 description: PlacesUIUtils.getDescriptionFromDocument(frame) });
+  },
+
+  "ContextMenu:Canvas:ToBlobURL": function(aMessage) {
+    this.getTarget(aMessage).toBlob((blob) => {
+      let blobURL = URL.createObjectURL(blob);
+      this.global.sendAsyncMessage("ContextMenu:Canvas:ToBlobURL:Result", { blobURL });
+    });
+  },
+
+  "ContextMenu:DoCustomCommand": function(aMessage) {
+    E10SUtils.wrapHandlingUserInput(
+      this.content,
+      aMessage.data.handlingUserInput,
+      () => PageMenuChild.executeMenu(aMessage.data.generatedItemId)
+    );
+  },
+
+  "ContextMenu:Hiding": function() {
+    this.context = null;
+    this.target = null;
+  },
+
+  "ContextMenu:MediaCommand": function(aMessage) {
+    E10SUtils.wrapHandlingUserInput(
+      this.content, aMessage.data.handlingUserInput, () => {
+        let media = this.getTarget(aMessage, "element");
+
+        switch (aMessage.data.command) {
+          case "play":
+            media.play();
+            break;
+          case "pause":
+            media.pause();
+            break;
+          case "loop":
+            media.loop = !media.loop;
+            break;
+          case "mute":
+            media.muted = true;
+            break;
+          case "unmute":
+            media.muted = false;
+            break;
+          case "playbackRate":
+            media.playbackRate = aMessage.data.data;
+            break;
+          case "hidecontrols":
+            media.removeAttribute("controls");
+            break;
+          case "showcontrols":
+            media.setAttribute("controls", "true");
+            break;
+          case "fullscreen":
+            if (this.content.document.fullscreenEnabled) {
+              media.requestFullscreen();
+            }
+
+            break;
+        }
+      }
+    );
+  },
+
+  "ContextMenu:ReloadFrame": function(aMessage) {
+    this.getTarget(aMessage).ownerDocument.location.reload();
+  },
+
+  "ContextMenu:ReloadImage": function(aMessage) {
+    let image = this.getTarget(aMessage);
+
+    if (image instanceof Ci.nsIImageLoadingContent) {
+      image.forceReload();
+    }
+  },
+
+  "ContextMenu:SearchFieldBookmarkData": function(aMessage) {
+    let node = this.getTarget(aMessage);
+    let charset = node.ownerDocument.characterSet;
+    let formBaseURI = Services.io.newURI(node.form.baseURI, charset);
+    let formURI = Services.io.newURI(node.form.getAttribute("action"),
+                                     charset, formBaseURI);
+    let spec = formURI.spec;
+    let isURLEncoded =  (node.form.method.toUpperCase() == "POST" &&
+                         (node.form.enctype == "application/x-www-form-urlencoded" ||
+                          node.form.enctype == ""));
+    let title = node.ownerDocument.title;
+    let description = PlacesUIUtils.getDescriptionFromDocument(node.ownerDocument);
+    let formData = [];
+
+    function escapeNameValuePair(aName, aValue, aIsFormUrlEncoded) {
+      if (aIsFormUrlEncoded) {
+        return escape(aName + "=" + aValue);
+      }
+
+      return escape(aName) + "=" + escape(aValue);
+    }
+
+    for (let el of node.form.elements) {
+      if (!el.type) // happens with fieldsets
+        continue;
+
+      if (el == node) {
+        formData.push((isURLEncoded) ? escapeNameValuePair(el.name, "%s", true) :
+                                       // Don't escape "%s", just append
+                                       escapeNameValuePair(el.name, "", false) + "%s");
+        continue;
+      }
+
+      let type = el.type.toLowerCase();
+
+      if (((el instanceof this.content.HTMLInputElement && el.mozIsTextField(true)) ||
+          type == "hidden" || type == "textarea") ||
+          ((type == "checkbox" || type == "radio") && el.checked)) {
+        formData.push(escapeNameValuePair(el.name, el.value, isURLEncoded));
+      } else if (el instanceof this.content.HTMLSelectElement && el.selectedIndex >= 0) {
+        for (let j = 0; j < el.options.length; j++) {
+          if (el.options[j].selected)
+            formData.push(escapeNameValuePair(el.name, el.options[j].value,
+                                              isURLEncoded));
+        }
+      }
+    }
+
+    let postData;
+
+    if (isURLEncoded) {
+      postData = formData.join("&");
+    } else {
+      let separator = spec.includes("?") ? "&" : "?";
+      spec += separator + formData.join("&");
+    }
+
+    this.global.sendAsyncMessage("ContextMenu:SearchFieldBookmarkData:Result",
+                                 { spec, title, description, postData, charset });
+  },
+
+  "ContextMenu:SaveVideoFrameAsImage": function(aMessage) {
+    let video = this.getTarget(aMessage);
+    let canvas = this.content.document.createElementNS("http://www.w3.org/1999/xhtml",
+                                                       "canvas");
+    canvas.width = video.videoWidth;
+    canvas.height = video.videoHeight;
+
+    let ctxDraw = canvas.getContext("2d");
+    ctxDraw.drawImage(video, 0, 0);
+
+    this.global.sendAsyncMessage("ContextMenu:SaveVideoFrameAsImage:Result", {
+      dataURL: canvas.toDataURL("image/jpeg", ""),
+    });
+  },
+
+  "ContextMenu:SetAsDesktopBackground": function(aMessage) {
+    let target = this.getTarget(aMessage);
+
+    // Paranoia: check disableSetDesktopBackground again, in case the
+    // image changed since the context menu was initiated.
+    let disable = this._disableSetDesktopBackground(target);
+
+    if (!disable) {
+      try {
+        BrowserUtils.urlSecurityCheck(target.currentURI.spec,
+                                      target.ownerDocument.nodePrincipal);
+        let canvas = this.content.document.createElement("canvas");
+        canvas.width = target.naturalWidth;
+        canvas.height = target.naturalHeight;
+        let ctx = canvas.getContext("2d");
+        ctx.drawImage(target, 0, 0);
+        let dataUrl = canvas.toDataURL();
+        let url = (new URL(target.ownerDocument.location.href)).pathname;
+        let imageName = url.substr(url.lastIndexOf("/") + 1);
+        this.global.sendAsyncMessage("ContextMenu:SetAsDesktopBackground:Result",
+                                     { dataUrl, imageName });
+      } catch (e) {
+        Cu.reportError(e);
+        disable = true;
+      }
+    }
+
+    if (disable) {
+      this.global.sendAsyncMessage("ContextMenu:SetAsDesktopBackground:Result",
+                                   { disable });
+    }
+  },
+};
+
+class ContextMenu {
+  // PUBLIC
+  constructor(global) {
+    this.target = null;
+    this.context = null;
+    this.global = global;
+    this.content = global.content;
+
+    Cc["@mozilla.org/eventlistenerservice;1"]
+      .getService(Ci.nsIEventListenerService)
+      .addSystemEventListener(global, "contextmenu",
+                              this._handleContentContextMenu.bind(this), false);
+
+    Object.keys(messageListeners).forEach(key =>
+      global.addMessageListener(key, messageListeners[key].bind(this))
+    );
+  }
+
+  /**
+   * Returns the event target of the context menu, using a locally stored
+   * reference if possible. If not, and aMessage.objects is defined,
+   * aMessage.objects[aKey] is returned. Otherwise null.
+   * @param  {Object} aMessage Message with a objects property
+   * @param  {String} aKey     Key for the target on aMessage.objects
+   * @return {Object}          Context menu target
+   */
+  getTarget(aMessage, aKey = "target") {
+    return this.target || (aMessage.objects && aMessage.objects[aKey]);
+  }
+
+  // PRIVATE
+  _isXULTextLinkLabel(aNode) {
+    const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+    return aNode.namespaceURI == XUL_NS &&
+           aNode.tagName == "label" &&
+           aNode.classList.contains("text-link") &&
+           aNode.href;
+  }
+
+  // Generate fully qualified URL for clicked-on link.
+  _getLinkURL() {
+    let href = this.context.link.href;
+
+    if (href) {
+      // Handle SVG links:
+      if (typeof href == "object" && href.animVal) {
+        return href.animVal;
+      }
+
+      return href;
+    }
+
+    href = this.context.link.getAttribute("href") ||
+           this.context.link.getAttributeNS("http://www.w3.org/1999/xlink", "href");
+
+    if (!href || !href.match(/\S/)) {
+      // Without this we try to save as the current doc,
+      // for example, HTML case also throws if empty
+      throw "Empty href";
+    }
+
+    return this._makeURLAbsolute(this.context.link.baseURI, href);
+  }
+
+  _getLinkURI() {
+    try {
+      return Services.io.newURI(this.context.linkURL);
+    } catch (ex) {
+     // e.g. empty URL string
+    }
+
+    return null;
+  }
+
+  // Get text of link.
+  _getLinkText() {
+    let text = this._gatherTextUnder(this.context.link);
+
+    if (!text || !text.match(/\S/)) {
+      text = this.context.link.getAttribute("title");
+      if (!text || !text.match(/\S/)) {
+        text = this.context.link.getAttribute("alt");
+        if (!text || !text.match(/\S/)) {
+          text = this.context.linkURL;
+        }
+      }
+    }
+
+    return text;
+  }
+
+  _getLinkProtocol() {
+    if (this.context.linkURI) {
+      return this.context.linkURI.scheme; // can be |undefined|
+    }
+
+    return null;
+  }
+
+  // Returns true if clicked-on link targets a resource that can be saved.
+  _isLinkSaveable(aLink) {
+    // We don't do the Right Thing for news/snews yet, so turn them off
+    // until we do.
+    return this.context.linkProtocol && !(
+           this.context.linkProtocol == "mailto" ||
+           this.context.linkProtocol == "javascript" ||
+           this.context.linkProtocol == "news" ||
+           this.context.linkProtocol == "snews");
+  }
+
+  // Gather all descendent text under given document node.
+  _gatherTextUnder(root) {
+    let text = "";
+    let node = root.firstChild;
+    let depth = 1;
+    while (node && depth > 0) {
+      // See if this node is text.
+      if (node.nodeType == Ci.nsIDOMNode.TEXT_NODE) {
+        // Add this text to our collection.
+        text += " " + node.data;
+      } else if (node instanceof this.content.HTMLImageElement) {
+        // If it has an "alt" attribute, add that.
+        let altText = node.getAttribute( "alt" );
+        if ( altText && altText != "" ) {
+          text += " " + altText;
+        }
+      }
+      // Find next node to test.
+      // First, see if this node has children.
+      if (node.hasChildNodes()) {
+        // Go to first child.
+        node = node.firstChild;
+        depth++;
+      } else {
+        // No children, try next sibling (or parent next sibling).
+        while (depth > 0 && !node.nextSibling) {
+          node = node.parentNode;
+          depth--;
+        }
+        if (node.nextSibling) {
+          node = node.nextSibling;
+        }
+      }
+    }
+
+    // Strip leading and tailing whitespace.
+    text = text.trim();
+    // Compress remaining whitespace.
+    text = text.replace(/\s+/g, " ");
+    return text;
+  }
+
+  // Returns a "url"-type computed style attribute value, with the url() stripped.
+  _getComputedURL(aElem, aProp) {
+    let url = aElem.ownerGlobal.getComputedStyle(aElem).getPropertyCSSValue(aProp);
+
+    if (url instanceof this.content.CSSValueList) {
+      if (url.length != 1) {
+        throw "found multiple URLs";
+      }
+
+      url = url[0];
+    }
+
+    return url.primitiveType == this.content.CSSPrimitiveValue.CSS_URI ?
+           url.getStringValue() : null;
+  }
+
+  _makeURLAbsolute(aBase, aUrl) {
+    return Services.io.newURI(aUrl, null, Services.io.newURI(aBase)).spec;
+  }
+
+  _isProprietaryDRM() {
+    return this.context.target.isEncrypted && this.context.target.mediaKeys &&
+           this.context.target.mediaKeys.keySystem != "org.w3.clearkey";
+  }
+
+  _isMediaURLReusable(aURL) {
+    if (aURL.startsWith("blob:")) {
+      return URL.isValidURL(aURL);
+    }
+
+    return true;
+  }
+
+  _isTargetATextBox(node) {
+    if (node instanceof this.content.HTMLInputElement) {
+      return node.mozIsTextField(false);
+    }
+
+    return (node instanceof this.content.HTMLTextAreaElement);
+  }
+
+  _isSpellCheckEnabled(aNode) {
+    // We can always force-enable spellchecking on textboxes
+    if (this._isTargetATextBox(aNode)) {
+      return true;
+    }
+
+    // We can never spell check something which is not content editable
+    let editable = aNode.isContentEditable;
+
+    if (!editable && aNode.ownerDocument) {
+      editable = aNode.ownerDocument.designMode == "on";
+    }
+
+    if (!editable) {
+      return false;
+    }
+
+    // Otherwise make sure that nothing in the parent chain disables spellchecking
+    return aNode.spellcheck;
+  }
+
+  _disableSetDesktopBackground(aTarget) {
+    // Disable the Set as Desktop Background menu item if we're still trying
+    // to load the image or the load failed.
+    if (!(aTarget instanceof Ci.nsIImageLoadingContent)) {
+      return true;
+    }
+
+    if (("complete" in aTarget) && !aTarget.complete) {
+      return true;
+    }
+
+    if (aTarget.currentURI.schemeIs("javascript")) {
+      return true;
+    }
+
+    let request = aTarget.QueryInterface(Ci.nsIImageLoadingContent)
+                         .getRequest(Ci.nsIImageLoadingContent.CURRENT_REQUEST);
+
+    if (!request) {
+      return true;
+    }
+
+    return false;
+  }
+
+  /**
+   * Retrieve the array of CSS selectors corresponding to the provided node. The first item
+   * of the array is the selector of the node in its owner document. Additional items are
+   * used if the node is inside a frame, each representing the CSS selector for finding the
+   * frame element in its parent document.
+   *
+   * This format is expected by DevTools in order to handle the Inspect Node context menu
+   * item.
+   *
+   * @param  {aNode}
+   *         The node for which the CSS selectors should be computed
+   * @return {Array} array of css selectors (strings).
+   */
+  _getNodeSelectors(aNode) {
+    let selectors = [];
+    while (aNode) {
+      selectors.push(findCssSelector(aNode));
+      aNode = aNode.ownerGlobal.frameElement;
+    }
+
+    return selectors;
+  }
+
+  _handleContentContextMenu(aEvent) {
+    let defaultPrevented = aEvent.defaultPrevented;
+
+    if (!Services.prefs.getBoolPref("dom.event.contextmenu.enabled")) {
+      let plugin = null;
+
+      try {
+        plugin = aEvent.target.QueryInterface(Ci.nsIObjectLoadingContent);
+      } catch (e) {}
+
+      if (plugin && plugin.displayedType == Ci.nsIObjectLoadingContent.TYPE_PLUGIN) {
+        // Don't open a context menu for plugins.
+        return;
+      }
+
+      defaultPrevented = false;
+    }
+
+    if (defaultPrevented) {
+      return;
+    }
+
+    let addonInfo = Object.create(null);
+    let subject = {
+      aEvent,
+      addonInfo,
+    };
+
+    subject.wrappedJSObject = subject;
+    Services.obs.notifyObservers(subject, "content-contextmenu");
+
+    let doc = aEvent.target.ownerDocument;
+    let {
+      mozDocumentURIIfNotForErrorPages: docLocation,
+      characterSet: charSet,
+      baseURI,
+      referrer,
+      referrerPolicy
+    } = doc;
+    docLocation = docLocation && docLocation.spec;
+    let frameOuterWindowID = WebNavigationFrames.getFrameId(doc.defaultView);
+    let loginFillInfo = LoginManagerContent.getFieldContext(aEvent.target);
+
+    // The same-origin check will be done in nsContextMenu.openLinkInTab.
+    let parentAllowsMixedContent = !!this.global.docShell.mixedContentChannel;
+
+    // Get referrer attribute from clicked link and parse it
+    let referrerAttrValue = Services.netUtils.parseAttributePolicyString(aEvent.target.
+                            getAttribute("referrerpolicy"));
+
+    if (referrerAttrValue !== Ci.nsIHttpChannel.REFERRER_POLICY_UNSET) {
+      referrerPolicy = referrerAttrValue;
+    }
+
+    let disableSetDesktopBg = null;
+
+    // Media related cache info parent needs for saving
+    let contentType = null;
+    let contentDisposition = null
+    if (aEvent.target.nodeType == Ci.nsIDOMNode.ELEMENT_NODE &&
+        aEvent.target instanceof Ci.nsIImageLoadingContent &&
+        aEvent.target.currentURI) {
+      disableSetDesktopBg = this._disableSetDesktopBackground(aEvent.target);
+
+      try {
+        let imageCache = Cc["@mozilla.org/image/tools;1"].getService(Ci.imgITools)
+                                                         .getImgCacheForDocument(doc);
+        let props = imageCache.findEntryProperties(aEvent.target.currentURI, doc);
+
+        try {
+          contentType = props.get("type", Ci.nsISupportsCString).data;
+        } catch (e) {}
+
+        try {
+          contentDisposition = props.get("content-disposition", Ci.nsISupportsCString).data;
+        } catch (e) {}
+      } catch (e) {}
+    }
+
+    let selectionInfo = BrowserUtils.getSelectionDetails(this.content);
+    let loadContext = this.global.docShell.QueryInterface(Ci.nsILoadContext);
+    let userContextId = loadContext.originAttributes.userContextId;
+    let popupNodeSelectors = this._getNodeSelectors(aEvent.target);
+
+    this._setContext(aEvent);
+    let context = this.context;
+    this.target = context.target;
+
+    let spellInfo = null;
+    let editFlags = null;
+    let principal = null;
+    let customMenuItems = null;
+
+    if (context.target) {
+      this._cleanContext();
+    }
+
+    let isRemote = Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT;
+
+    if (isRemote) {
+      editFlags = SpellCheckHelper.isEditable(aEvent.target, this.content);
+
+      if (editFlags &
+          (SpellCheckHelper.EDITABLE | SpellCheckHelper.CONTENTEDITABLE)) {
+        spellInfo = InlineSpellCheckerContent.initContextMenu(aEvent, editFlags, this.global);
+      }
+
+      // Set the event target first as the copy image command needs it to
+      // determine what was context-clicked on. Then, update the state of the
+      // commands on the context menu.
+      this.global.docShell.contentViewer.QueryInterface(Ci.nsIContentViewerEdit)
+                          .setCommandNode(aEvent.target);
+      aEvent.target.ownerGlobal.updateCommands("contentcontextmenu");
+
+      customMenuItems = PageMenuChild.build(aEvent.target);
+      principal = doc.nodePrincipal;
+    }
+
+    let data = {
+      context,
+      charSet,
+      baseURI,
+      isRemote,
+      referrer,
+      addonInfo,
+      editFlags,
+      principal,
+      spellInfo,
+      contentType,
+      docLocation,
+      loginFillInfo,
+      selectionInfo,
+      userContextId,
+      referrerPolicy,
+      customMenuItems,
+      contentDisposition,
+      frameOuterWindowID,
+      popupNodeSelectors,
+      disableSetDesktopBg,
+      parentAllowsMixedContent,
+    };
+
+    if (isRemote) {
+      this.global.sendAsyncMessage("contextmenu", data);
+    } else {
+      let browser = this.global.docShell.chromeEventHandler;
+      let mainWin = browser.ownerGlobal;
+
+      data.documentURIObject = doc.documentURIObject;
+      data.disableSetDesktopBackground = data.disableSetDesktopBg;
+      delete data.disableSetDesktopBg;
+
+      mainWin.setContextMenuContentData(data);
+    }
+  }
+
+  /**
+   * Some things are not serializable, so we either have to only send
+   * their needed data or regenerate them in nsContextMenu.js
+   * - target and target.ownerDocument
+   * - link
+   * - linkURI
+   */
+  _cleanContext(aEvent) {
+    const context = this.context;
+    const cleanTarget = Object.create(null);
+
+    cleanTarget.ownerDocument = {
+      // used for nsContextMenu.initLeaveDOMFullScreenItems and
+      // nsContextMenu.initMediaPlayerItems
+      fullscreenElement: context.target.ownerDocument.fullscreenElement,
+
+      // used for nsContextMenu.initMiscItems
+      contentType: context.target.ownerDocument.contentType,
+
+      // used for nsContextMenu.saveLink
+      isPrivate: context.target.ownerDocument.isPrivate,
+    };
+
+    // used for nsContextMenu.initMediaPlayerItems
+    Object.assign(cleanTarget, {
+      ended: context.target.ended,
+      muted: context.target.muted,
+      paused: context.target.paused,
+      controls: context.target.controls,
+      duration: context.target.duration,
+    });
+
+    const onMedia = context.onVideo || context.onAudio;
+
+    if (onMedia) {
+      Object.assign(cleanTarget, {
+        loop: context.target.loop,
+        error: context.target.error,
+        networkState: context.target.networkState,
+        playbackRate: context.target.playbackRate,
+        NETWORK_NO_SOURCE: context.target.NETWORK_NO_SOURCE,
+      });
+
+      if (context.onVideo) {
+        Object.assign(cleanTarget, {
+          readyState: context.target.readyState,
+          HAVE_CURRENT_DATA: context.target.HAVE_CURRENT_DATA,
+        });
+      }
+    }
+
+    context.target = cleanTarget;
+
+    if (context.link) {
+      context.link = { href: context.link.href };
+    }
+
+    delete context.linkURI;
+  }
+
+  _setContext(aEvent) {
+    this.context = Object.create(null);
+    const context = this.context;
+
+    context.screenX = aEvent.screenX;
+    context.screenY = aEvent.screenY;
+    context.mozInputSource = aEvent.mozInputSource;
+
+    const node = aEvent.target;
+
+    const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+    context.shouldDisplay = true;
+
+    if (node.nodeType == Ci.nsIDOMNode.DOCUMENT_NODE ||
+        // Don't display for XUL element unless <label class="text-link">
+        (node.namespaceURI == XUL_NS && !this._isXULTextLinkLabel(node))) {
+      context.shouldDisplay = false;
+      return;
+    }
+
+    // Initialize context to be sent to nsContextMenu
+    // Keep this consistent with the similar code in nsContextMenu's setContext
+    context.bgImageURL          = "";
+    context.imageDescURL        = "";
+    context.imageInfo           = null;
+    context.mediaURL            = "";
+    context.webExtBrowserType   = "";
+
+    context.canSpellCheck       = false;
+    context.hasBGImage          = false;
+    context.hasMultipleBGImages = false;
+    context.isDesignMode        = false;
+    context.inFrame             = false;
+    context.inSrcdocFrame       = false;
+    context.inSyntheticDoc      = false;
+    context.inTabBrowser        = true;
+    context.inWebExtBrowser     = false;
+
+    context.link                = null;
+    context.linkDownload        = "";
+    context.linkHasNoReferrer   = false;
+    context.linkProtocol        = "";
+    context.linkTextStr         = "";
+    context.linkURL             = "";
+    context.linkURI             = null;
+
+    context.onAudio             = false;
+    context.onCanvas            = false;
+    context.onCompletedImage    = false;
+    context.onCTPPlugin         = false;
+    context.onDRMMedia          = false;
+    context.onEditableArea      = false;
+    context.onImage             = false;
+    context.onKeywordField      = false;
+    context.onLink              = false;
+    context.onLoadedImage       = false;
+    context.onMailtoLink        = false;
+    context.onMathML            = false;
+    context.onMozExtLink        = false;
+    context.onNumeric           = false;
+    context.onPassword          = false;
+    context.onSaveableLink      = false;
+    context.onTextInput         = false;
+    context.onVideo             = false;
+
+    // Remember the node and its owner document that was clicked
+    // This may be modifed before sending to nsContextMenu
+    context.target = node;
+
+    context.principal = context.target.ownerDocument.nodePrincipal;
+    context.frameOuterWindowID = WebNavigationFrames.getFrameId(context.target.ownerGlobal);
+
+    // Check if we are in a synthetic document (stand alone image, video, etc.).
+    context.inSyntheticDoc = context.target.ownerDocument.mozSyntheticDocument;
+
+    context.shouldInitInlineSpellCheckerUINoChildren = false;
+    context.shouldInitInlineSpellCheckerUIWithChildren = false;
+
+    let editFlags = SpellCheckHelper.isEditable(context.target, this.content);
+    this._setContextForNodesNoChildren(editFlags);
+    this._setContextForNodesWithChildren(editFlags);
+  }
+
+  /**
+   * Sets up the parts of the context menu for when when nodes have no children.
+   *
+   * @param {Integer} editFlags The edit flags for the node. See SpellCheckHelper
+   *                            for the details.
+   */
+  _setContextForNodesNoChildren(editFlags) {
+    const context = this.context;
+
+    if (context.target.nodeType == Ci.nsIDOMNode.TEXT_NODE) {
+      // For text nodes, look at the parent node to determine the spellcheck attribute.
+      context.canSpellCheck = context.target.parentNode &&
+                              this._isSpellCheckEnabled(context.target);
+      return;
+    }
+
+    // We only deal with TEXT_NODE and ELEMENT_NODE in this function, so return
+    // early if we don't have one.
+    if (context.target.nodeType != Ci.nsIDOMNode.ELEMENT_NODE) {
+      return;
+    }
+
+    // See if the user clicked on an image. This check mirrors
+    // nsDocumentViewer::GetInImage. Make sure to update both if this is
+    // changed.
+    if (context.target instanceof Ci.nsIImageLoadingContent &&
+        context.target.currentURI) {
+      context.onImage = true;
+
+      context.imageInfo = {
+        currentSrc: context.target.currentSrc,
+        width: context.target.width,
+        height: context.target.height,
+        imageText: context.target.title || context.target.alt
+      };
+
+      const request = context.target.getRequest(Ci.nsIImageLoadingContent.CURRENT_REQUEST);
+
+      if (request && (request.imageStatus & request.STATUS_SIZE_AVAILABLE)) {
+        context.onLoadedImage = true;
+      }
+
+      if (request &&
+          (request.imageStatus & request.STATUS_LOAD_COMPLETE) &&
+          !(request.imageStatus & request.STATUS_ERROR)) {
+        context.onCompletedImage = true;
+      }
+
+      context.mediaURL = context.target.currentURI.spec;
+
+      const descURL = context.target.getAttribute("longdesc");
+
+      if (descURL) {
+        context.imageDescURL = this._makeURLAbsolute(context.target.ownerDocument.body.baseURI,
+                                                    descURL);
+      }
+    } else if (context.target instanceof this.content.HTMLCanvasElement) {
+      context.onCanvas = true;
+    } else if (context.target instanceof this.content.HTMLVideoElement) {
+      const mediaURL = context.target.currentSrc || context.target.src;
+
+      if (this._isMediaURLReusable(mediaURL)) {
+        context.mediaURL = mediaURL;
+      }
+
+      if (this._isProprietaryDRM()) {
+        context.onDRMMedia = true;
+      }
+
+      // Firefox always creates a HTMLVideoElement when loading an ogg file
+      // directly. If the media is actually audio, be smarter and provide a
+      // context menu with audio operations.
+      if (context.target.readyState >= context.target.HAVE_METADATA &&
+          (context.target.videoWidth == 0 || context.target.videoHeight == 0)) {
+        context.onAudio = true;
+      } else {
+        context.onVideo = true;
+      }
+    } else if (context.target instanceof this.content.HTMLAudioElement) {
+      context.onAudio = true;
+      const mediaURL = context.target.currentSrc || context.target.src;
+
+      if (this._isMediaURLReusable(mediaURL)) {
+        context.mediaURL = mediaURL;
+      }
+
+      if (this._isProprietaryDRM()) {
+        context.onDRMMedia = true;
+      }
+    } else if (editFlags & (SpellCheckHelper.INPUT | SpellCheckHelper.TEXTAREA)) {
+      context.onTextInput = (editFlags & SpellCheckHelper.TEXTINPUT) !== 0;
+      context.onNumeric = (editFlags & SpellCheckHelper.NUMERIC) !== 0;
+      context.onEditableArea = (editFlags & SpellCheckHelper.EDITABLE) !== 0;
+      context.onPassword = (editFlags & SpellCheckHelper.PASSWORD) !== 0;
+
+      if (context.onEditableArea) {
+        context.shouldInitInlineSpellCheckerUINoChildren = true;
+      }
+
+      context.onKeywordField = (editFlags & SpellCheckHelper.KEYWORD);
+    } else if (context.target instanceof this.content.HTMLHtmlElement) {
+      const bodyElt = context.target.ownerDocument.body;
+
+      if (bodyElt) {
+        let computedURL;
+
+        try {
+          computedURL = this._getComputedURL(bodyElt, "background-image");
+          context.hasMultipleBGImages = false;
+        } catch (e) {
+          context.hasMultipleBGImages = true;
+        }
+
+        if (computedURL) {
+          context.hasBGImage = true;
+          context.bgImageURL = this._makeURLAbsolute(bodyElt.baseURI,
+                                                    computedURL);
+        }
+      }
+    } else if ((context.target instanceof this.content.HTMLEmbedElement ||
+               context.target instanceof this.content.HTMLObjectElement) &&
+               context.target.displayedType == this.content.HTMLObjectElement.TYPE_NULL &&
+               context.target.pluginFallbackType == this.content.HTMLObjectElement.PLUGIN_CLICK_TO_PLAY) {
+      context.onCTPPlugin = true;
+    }
+
+    context.canSpellCheck = this._isSpellCheckEnabled(context.target);
+  }
+
+  /**
+   * Sets up the parts of the context menu for when when nodes have children.
+   *
+   * @param {Integer} editFlags The edit flags for the node. See SpellCheckHelper
+   *                            for the details.
+   */
+  _setContextForNodesWithChildren(editFlags) {
+    const context = this.context;
+
+    // Second, bubble out, looking for items of interest that can have childen.
+    // Always pick the innermost link, background image, etc.
+    let elem = context.target;
+
+    while (elem) {
+      if (elem.nodeType == Ci.nsIDOMNode.ELEMENT_NODE) {
+        // Link?
+        const XLINK_NS = "http://www.w3.org/1999/xlink";
+
+        if (!context.onLink &&
+            // Be consistent with what hrefAndLinkNodeForClickEvent
+            // does in browser.js
+            (this._isXULTextLinkLabel(elem) ||
+            (elem instanceof this.content.HTMLAnchorElement && elem.href) ||
+            (elem instanceof this.content.SVGAElement &&
+            (elem.href || elem.hasAttributeNS(XLINK_NS, "href"))) ||
+            (elem instanceof this.content.HTMLAreaElement && elem.href) ||
+            elem instanceof this.content.HTMLLinkElement ||
+            elem.getAttributeNS(XLINK_NS, "type") == "simple")) {
+
+          // Target is a link or a descendant of a link.
+          context.onLink = true;
+
+          // Remember corresponding element.
+          context.link = elem;
+          context.linkURL = this._getLinkURL();
+          context.linkURI = this._getLinkURI();
+          context.linkTextStr = this._getLinkText();
+          context.linkProtocol = this._getLinkProtocol();
+          context.onMailtoLink = (context.linkProtocol == "mailto");
+          context.onMozExtLink = (context.linkProtocol == "moz-extension");
+          context.onSaveableLink = this._isLinkSaveable(context.link);
+          context.linkHasNoReferrer = BrowserUtils.linkHasNoReferrer(elem);
+
+          try {
+            if (elem.download) {
+              // Ignore download attribute on cross-origin links
+              context.principal.checkMayLoad(context.linkURI, false, true);
+              context.linkDownload = elem.download;
+            }
+          } catch (ex) {}
+        }
+
+        // Background image?  Don't bother if we've already found a
+        // background image further down the hierarchy.  Otherwise,
+        // we look for the computed background-image style.
+        if (!context.hasBGImage &&
+            !context.hasMultipleBGImages) {
+          let bgImgUrl = null;
+
+          try {
+            bgImgUrl = this._getComputedURL(elem, "background-image");
+            context.hasMultipleBGImages = false;
+          } catch (e) {
+            context.hasMultipleBGImages = true;
+          }
+
+          if (bgImgUrl) {
+            context.hasBGImage = true;
+            context.bgImageURL = this._makeURLAbsolute(elem.baseURI,
+                                                      bgImgUrl);
+          }
+        }
+      }
+
+      elem = elem.parentNode;
+    }
+
+    // See if the user clicked on MathML
+    const MathML_NS = "http://www.w3.org/1998/Math/MathML";
+
+    if ((context.target.nodeType == Ci.nsIDOMNode.TEXT_NODE &&
+         context.target.parentNode.namespaceURI == MathML_NS) ||
+         (context.target.namespaceURI == MathML_NS)) {
+      context.onMathML = true;
+    }
+
+    // See if the user clicked in a frame.
+    const docDefaultView = context.target.ownerGlobal;
+
+    if (docDefaultView != docDefaultView.top) {
+      context.inFrame = true;
+
+      if (context.target.ownerDocument.isSrcdocDocument) {
+          context.inSrcdocFrame = true;
+      }
+    }
+
+    // if the document is editable, show context menu like in text inputs
+    if (!context.onEditableArea) {
+      if (editFlags & SpellCheckHelper.CONTENTEDITABLE) {
+        // If this._onEditableArea is false but editFlags is CONTENTEDITABLE, then
+        // the document itself must be editable.
+        context.onTextInput       = true;
+        context.onKeywordField    = false;
+        context.onImage           = false;
+        context.onLoadedImage     = false;
+        context.onCompletedImage  = false;
+        context.onMathML          = false;
+        context.inFrame           = false;
+        context.inSrcdocFrame     = false;
+        context.hasBGImage        = false;
+        context.isDesignMode      = true;
+        context.onEditableArea    = true;
+        context.shouldInitInlineSpellCheckerUIWithChildren = true;
+      }
+    }
+  }
+}
--- a/browser/modules/ExtensionsUI.jsm
+++ b/browser/modules/ExtensionsUI.jsm
@@ -11,18 +11,18 @@ Cu.import("resource://gre/modules/XPCOMU
 Cu.import("resource://gre/modules/EventEmitter.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
                                   "resource://gre/modules/AddonManager.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "AddonManagerPrivate",
                                   "resource://gre/modules/AddonManager.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "AppMenuNotifications",
                                   "resource://gre/modules/AppMenuNotifications.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
-                                  "resource://gre/modules/PluralForm.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionData",
+                                  "resource://gre/modules/Extension.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow",
                                   "resource:///modules/RecentWindow.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Services",
                                   "resource://gre/modules/Services.jsm");
 
 XPCOMUtils.defineLazyPreferenceGetter(this, "WEBEXT_PERMISSION_PROMPTS",
                                       "extensions.webextPermissionPrompts", false);
 
@@ -247,137 +247,25 @@ this.ExtensionsUI = {
   _sanitizeName(name) {
     return name.replace(/&/g, "&amp;")
                .replace(/</g, "&lt;")
                .replace(/>/g, "&gt;");
   },
 
   // Create a set of formatted strings for a permission prompt
   _buildStrings(info) {
-    let result = {};
-
     let bundle = Services.strings.createBundle(BROWSER_PROPERTIES);
 
-    let perms = info.permissions || {origins: [], permissions: []};
-
-    // First classify our host permissions
-    let allUrls = false, wildcards = [], sites = [];
-    for (let permission of perms.origins) {
-      if (permission == "<all_urls>") {
-        allUrls = true;
-        break;
-      }
-      let match = /^[htps*]+:\/\/([^/]+)\//.exec(permission);
-      if (!match) {
-        Cu.reportError(`Unparseable host permission ${permission}`);
-        continue;
-      }
-      if (match[1] == "*") {
-        allUrls = true;
-      } else if (match[1].startsWith("*.")) {
-        wildcards.push(match[1].slice(2));
-      } else {
-        sites.push(match[1]);
-      }
-    }
-
-    // Format the host permissions.  If we have a wildcard for all urls,
-    // a single string will suffice.  Otherwise, show domain wildcards
-    // first, then individual host permissions.
-    result.msgs = [];
-    if (allUrls) {
-      result.msgs.push(bundle.GetStringFromName("webextPerms.hostDescription.allUrls"));
-    } else {
-      // Formats a list of host permissions.  If we have 4 or fewer, display
-      // them all, otherwise display the first 3 followed by an item that
-      // says "...plus N others"
-      function format(list, itemKey, moreKey) {
-        function formatItems(items) {
-          result.msgs.push(...items.map(item => bundle.formatStringFromName(itemKey, [item], 1)));
-        }
-        if (list.length < 5) {
-          formatItems(list);
-        } else {
-          formatItems(list.slice(0, 3));
-
-          let remaining = list.length - 3;
-          result.msgs.push(PluralForm.get(remaining, bundle.GetStringFromName(moreKey))
-                                     .replace("#1", remaining));
-        }
-      }
-
-      format(wildcards, "webextPerms.hostDescription.wildcard",
-             "webextPerms.hostDescription.tooManyWildcards");
-      format(sites, "webextPerms.hostDescription.oneSite",
-             "webextPerms.hostDescription.tooManySites");
-    }
-
-    let permissionKey = perm => `webextPerms.description.${perm}`;
+    let brandBundle = Services.strings.createBundle(BRAND_PROPERTIES);
+    let appName = brandBundle.GetStringFromName("brandShortName");
+    let addonName = `<span class="addon-webext-name">${this._sanitizeName(info.addon.name)}</span>`;
 
-    // Next, show the native messaging permission if it is present.
-    const NATIVE_MSG_PERM = "nativeMessaging";
-    if (perms.permissions.includes(NATIVE_MSG_PERM)) {
-      let brandBundle = Services.strings.createBundle(BRAND_PROPERTIES);
-      let appName = brandBundle.GetStringFromName("brandShortName");
-      result.msgs.push(bundle.formatStringFromName(permissionKey(NATIVE_MSG_PERM), [appName], 1));
-    }
-
-    // Finally, show remaining permissions, in any order.
-    for (let permission of perms.permissions) {
-      // Handled above
-      if (permission == "nativeMessaging") {
-        continue;
-      }
-      try {
-        result.msgs.push(bundle.GetStringFromName(permissionKey(permission)));
-      } catch (err) {
-        // We deliberately do not include all permissions in the prompt.
-        // So if we don't find one then just skip it.
-      }
-    }
-
-    // Now figure out all the rest of the notification text.
-    let name = this._sanitizeName(info.addon.name);
-    let addonName = `<span class="addon-webext-name">${name}</span>`;
-
-    result.header = bundle.formatStringFromName("webextPerms.header", [addonName], 1);
-    result.text = info.unsigned ?
-                  bundle.GetStringFromName("webextPerms.unsignedWarning") : "";
-    result.listIntro = bundle.GetStringFromName("webextPerms.listIntro");
+    let info2 = Object.assign({appName, addonName}, info);
 
-    result.acceptText = bundle.GetStringFromName("webextPerms.add.label");
-    result.acceptKey = bundle.GetStringFromName("webextPerms.add.accessKey");
-    result.cancelText = bundle.GetStringFromName("webextPerms.cancel.label");
-    result.cancelKey = bundle.GetStringFromName("webextPerms.cancel.accessKey");
-
-    if (info.type == "sideload") {
-      result.header = bundle.formatStringFromName("webextPerms.sideloadHeader", [addonName], 1);
-      let key = result.msgs.length == 0 ?
-                "webextPerms.sideloadTextNoPerms" : "webextPerms.sideloadText2";
-      result.text = bundle.GetStringFromName(key);
-      result.acceptText = bundle.GetStringFromName("webextPerms.sideloadEnable.label");
-      result.acceptKey = bundle.GetStringFromName("webextPerms.sideloadEnable.accessKey");
-      result.cancelText = bundle.GetStringFromName("webextPerms.sideloadCancel.label");
-      result.cancelKey = bundle.GetStringFromName("webextPerms.sideloadCancel.accessKey");
-    } else if (info.type == "update") {
-      result.header = "";
-      result.text = bundle.formatStringFromName("webextPerms.updateText", [addonName], 1);
-      result.acceptText = bundle.GetStringFromName("webextPerms.updateAccept.label");
-      result.acceptKey = bundle.GetStringFromName("webextPerms.updateAccept.accessKey");
-    } else if (info.type == "optional") {
-      result.header = bundle.formatStringFromName("webextPerms.optionalPermsHeader", [addonName], 1);
-      result.text = "";
-      result.listIntro = bundle.GetStringFromName("webextPerms.optionalPermsListIntro");
-      result.acceptText = bundle.GetStringFromName("webextPerms.optionalPermsAllow.label");
-      result.acceptKey = bundle.GetStringFromName("webextPerms.optionalPermsAllow.accessKey");
-      result.cancelText = bundle.GetStringFromName("webextPerms.optionalPermsDeny.label");
-      result.cancelKey = bundle.GetStringFromName("webextPerms.optionalPermsDeny.accessKey");
-    }
-
-    return result;
+    return ExtensionData.formatPermissionStrings(info2, bundle);
   },
 
   showPermissionsPrompt(browser, strings, icon, histkey) {
     function eventCallback(topic) {
       let doc = this.browser.ownerDocument;
       if (topic == "showing") {
         // eslint-disable-next-line no-unsanitized/property
         doc.getElementById("addon-webext-perm-header").innerHTML = strings.header;
--- a/browser/modules/PluginContent.jsm
+++ b/browser/modules/PluginContent.jsm
@@ -20,17 +20,17 @@ XPCOMUtils.defineLazyGetter(this, "gNavi
   return Services.strings.createBundle(url);
 });
 
 XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
   "resource://gre/modules/AppConstants.jsm");
 
 this.PluginContent = function(global) {
   this.init(global);
-}
+};
 
 const FLASH_MIME_TYPE = "application/x-shockwave-flash";
 const REPLACEMENT_STYLE_SHEET = Services.io.newURI("chrome://pluginproblem/content/pluginReplaceBinding.css");
 
 PluginContent.prototype = {
   init(global) {
     this.global = global;
     // Need to hold onto the content window or else it'll get destroyed
@@ -93,22 +93,24 @@ PluginContent.prototype = {
     switch (msg.name) {
       case "BrowserPlugins:ActivatePlugins":
         this.activatePlugins(msg.data.pluginInfo, msg.data.newState);
         break;
       case "BrowserPlugins:NotificationShown":
         setTimeout(() => this.updateNotificationUI(), 0);
         break;
       case "BrowserPlugins:ContextMenuCommand":
+        let contextMenu = this.global.contextMenu;
+
         switch (msg.data.command) {
           case "play":
-            this._showClickToPlayNotification(msg.objects.plugin, true);
+            this._showClickToPlayNotification(contextMenu.getTarget(msg, "plugin"), true);
             break;
           case "hide":
-            this.hideClickToPlayOverlay(msg.objects.plugin);
+            this.hideClickToPlayOverlay(contextMenu.getTarget(msg, "plugin"));
             break;
         }
         break;
       case "BrowserPlugins:NPAPIPluginProcessCrashed":
         this.NPAPIPluginProcessCrashed({
           pluginName: msg.data.pluginName,
           runID: msg.data.runID,
           state: msg.data.state,
--- a/browser/modules/moz.build
+++ b/browser/modules/moz.build
@@ -123,24 +123,24 @@ BROWSER_CHROME_MANIFESTS += ['test/brows
 XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
 
 EXTRA_JS_MODULES += [
     'AboutHome.jsm',
     'AboutNewTab.jsm',
     'AttributionCode.jsm',
     'BrowserUITelemetry.jsm',
     'BrowserUsageTelemetry.jsm',
-    'CastingApps.jsm',
     'ContentClick.jsm',
     'ContentCrashHandlers.jsm',
     'ContentLinkHandler.jsm',
     'ContentMetaHandler.jsm',
     'ContentObservers.js',
     'ContentSearch.jsm',
     'ContentWebRTC.jsm',
+    'ContextMenu.jsm',
     'DirectoryLinksProvider.jsm',
     'E10SUtils.jsm',
     'ExtensionsUI.jsm',
     'Feeds.jsm',
     'FormSubmitObserver.jsm',
     'FormValidationHandler.jsm',
     'LaterRun.jsm',
     'offlineAppCache.jsm',
--- a/devtools/client/debugger/new/test/mochitest/browser.ini
+++ b/devtools/client/debugger/new/test/mochitest/browser.ini
@@ -74,16 +74,16 @@ skip-if = true # Bug 1393121
 [browser_dbg-scopes-mutations.js]
 [browser_dbg-search-file.js]
 skip-if = os == "win" # Bug 1393121
 [browser_dbg-search-sources.js]
 skip-if = os == "win" # Bug 1393121
 [browser_dbg-search-symbols.js]
 skip-if = os == "win" # Bug 1393121
 [browser_dbg-search-project.js]
-skip-if = os == "win" # Bug 1393121
+skip-if = true # Bug 1393121, 1393299
 [browser_dbg-sourcemaps.js]
 [browser_dbg-sourcemaps-reloading.js]
 [browser_dbg-sourcemaps2.js]
 [browser_dbg-sourcemaps-bogus.js]
 [browser_dbg-sources.js]
 [browser_dbg-tabs.js]
 [browser_dbg-toggling-tools.js]
--- a/devtools/client/inspector/markup/test/browser.ini
+++ b/devtools/client/inspector/markup/test/browser.ini
@@ -33,16 +33,18 @@ support-files =
   doc_markup_search.html
   doc_markup_svg_attributes.html
   doc_markup_toggle.html
   doc_markup_tooltip.png
   doc_markup_void_elements.html
   doc_markup_void_elements.xhtml
   doc_markup_whitespace.html
   doc_markup_xul.xul
+  doc_markup_update-on-navigtion_1.html
+  doc_markup_update-on-navigtion_2.html
   events_bundle.js
   events_bundle.js.map
   events_original.js
   head.js
   helper_attributes_test_runner.js
   helper_diff.js
   helper_events_test_runner.js
   helper_markup_accessibility_navigation.js
--- a/devtools/client/inspector/markup/test/browser_markup_update-on-navigtion.js
+++ b/devtools/client/inspector/markup/test/browser_markup_update-on-navigtion.js
@@ -1,18 +1,17 @@
 /* vim: set ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 "use strict";
 
 // Test that markup view handles page navigation correctly.
 
-const SCHEMA = "data:text/html;charset=UTF-8,";
-const URL_1 = SCHEMA + "<div id='one' style='color:red;'>ONE</div>";
-const URL_2 = SCHEMA + "<div id='two' style='color:green;'>TWO</div>";
+const URL_1 = URL_ROOT + "doc_markup_update-on-navigtion_1.html";
+const URL_2 = URL_ROOT + "doc_markup_update-on-navigtion_2.html";
 
 add_task(function* () {
   let {inspector, testActor} = yield openInspectorForURL(URL_1);
 
   assertMarkupViewIsLoaded();
   yield selectNode("#one", inspector);
 
   let willNavigate = inspector.target.once("will-navigate");
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/markup/test/doc_markup_update-on-navigtion_1.html
@@ -0,0 +1,1 @@
+<div id='one' style='color:red;'>ONE</div>
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/markup/test/doc_markup_update-on-navigtion_2.html
@@ -0,0 +1,1 @@
+<div id='two' style='color:green;'>TWO</div>
new file mode 100644
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/mochitest/file_saveHeapSnapshot_e10s_01.html
@@ -0,0 +1,1 @@
+<!DOCTYPE HTML><html><body></body></html>
--- a/devtools/shared/heapsnapshot/tests/mochitest/mochitest.ini
+++ b/devtools/shared/heapsnapshot/tests/mochitest/mochitest.ini
@@ -1,6 +1,7 @@
 [DEFAULT]
 tags = devtools devtools-memory
 support-files =
+  file_saveHeapSnapshot_e10s_01.html
 
 [test_saveHeapSnapshot_e10s_01.html]
 
--- a/devtools/shared/heapsnapshot/tests/mochitest/test_saveHeapSnapshot_e10s_01.html
+++ b/devtools/shared/heapsnapshot/tests/mochitest/test_saveHeapSnapshot_e10s_01.html
@@ -14,17 +14,17 @@ Bug 1201597 - Sanity test that we can ta
     <script type="application/javascript">
     "use strict";
     window.onerror = function (msg, url, line, col, err) {
       ok(false, "@" + url + ":" + line + ":" + col + ": " + msg + "\n" + err.stack);
     };
 
     SimpleTest.waitForExplicitFinish();
 
-    var childFrameURL = "data:text/html,<!DOCTYPE HTML><html><body></body></html>";
+    var childFrameURL = "file_saveHeapSnapshot_e10s_01.html";
 
     // This function is stringified and loaded in the child process as a frame
     // script.
     function childFrameScript() {
       try {
         ChromeUtils.saveHeapSnapshot({ runtime: true });
       } catch (err) {
         sendAsyncMessage("testSaveHeapSnapshot:error",
--- a/dom/base/nsJSEnvironment.cpp
+++ b/dom/base/nsJSEnvironment.cpp
@@ -1783,17 +1783,17 @@ bool
 InterSliceGCRunnerFired(TimeStamp aDeadline, void* aData)
 {
   nsJSContext::KillInterSliceGCRunner();
   MOZ_ASSERT(sActiveIntersliceGCBudget > 0);
   // We use longer budgets when the CC has been locked out but the CC has tried
   // to run since that means we may have significant amount garbage to collect
   // and better to GC in several longer slices than in a very long one.
   int64_t budget = aDeadline.IsNull() ?
-    int64_t(sActiveIntersliceGCBudget) :
+    int64_t(sActiveIntersliceGCBudget * 2) :
     int64_t((aDeadline - TimeStamp::Now()).ToMilliseconds());
   if (sCCLockedOut && sCCLockedOutTime) {
     int64_t lockedTime = PR_Now() - sCCLockedOutTime;
     int32_t maxSliceGCBudget = sActiveIntersliceGCBudget * 10;
     double percentOfLockedTime =
       std::min((double)lockedTime / NS_MAX_CC_LOCKEDOUT_TIME, 1.0);
     budget =
       static_cast<int64_t>(
--- a/dom/fetch/BodyExtractor.cpp
+++ b/dom/fetch/BodyExtractor.cpp
@@ -130,17 +130,19 @@ BodyExtractor<nsIDocument>::GetAsStream(
 
 template<> nsresult
 BodyExtractor<const nsAString>::GetAsStream(nsIInputStream** aResult,
                                             uint64_t* aContentLength,
                                             nsACString& aContentTypeWithCharset,
                                             nsACString& aCharset) const
 {
   nsCString encoded;
-  CopyUTF16toUTF8(*mBody, encoded);
+  if (!CopyUTF16toUTF8(*mBody, encoded, fallible)) {
+    return NS_ERROR_OUT_OF_MEMORY;
+  }
 
   nsresult rv = NS_NewCStringInputStream(aResult, encoded);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
   *aContentLength = encoded.Length();
   aContentTypeWithCharset.AssignLiteral("text/plain;charset=UTF-8");
--- a/dom/ipc/ContentParent.cpp
+++ b/dom/ipc/ContentParent.cpp
@@ -53,16 +53,17 @@
 #include "mozilla/dom/PresentationParent.h"
 #include "mozilla/dom/PPresentationParent.h"
 #include "mozilla/dom/PushNotifier.h"
 #include "mozilla/dom/FlyWebPublishedServerIPC.h"
 #include "mozilla/dom/quota/QuotaManagerService.h"
 #include "mozilla/dom/time/DateCacheCleaner.h"
 #include "mozilla/dom/URLClassifierParent.h"
 #include "mozilla/embedding/printingui/PrintingParent.h"
+#include "mozilla/extensions/StreamFilterParent.h"
 #include "mozilla/gfx/gfxVars.h"
 #include "mozilla/gfx/GPUProcessManager.h"
 #include "mozilla/hal_sandbox/PHalParent.h"
 #include "mozilla/ipc/BackgroundChild.h"
 #include "mozilla/ipc/BackgroundParent.h"
 #include "mozilla/ipc/FileDescriptorUtils.h"
 #include "mozilla/ipc/PChildToParentStreamParent.h"
 #include "mozilla/ipc/TestShellParent.h"
@@ -3309,16 +3310,29 @@ ContentParent::GetPrintingParent()
 {
   MOZ_ASSERT(mPrintingParent);
 
   RefPtr<embedding::PrintingParent> printingParent = mPrintingParent;
   return printingParent.forget();
 }
 #endif
 
+mozilla::ipc::IPCResult
+ContentParent::RecvInitStreamFilter(const uint64_t& aChannelId,
+                                    const nsString& aAddonId,
+                                    InitStreamFilterResolver&& aResolver)
+{
+  Endpoint<PStreamFilterChild> endpoint;
+  Unused << extensions::StreamFilterParent::Create(this, aChannelId, aAddonId, &endpoint);
+
+  aResolver(Move(endpoint));
+
+  return IPC_OK();
+}
+
 PChildToParentStreamParent*
 ContentParent::AllocPChildToParentStreamParent()
 {
   return nsIContentParent::AllocPChildToParentStreamParent();
 }
 
 bool
 ContentParent::DeallocPChildToParentStreamParent(PChildToParentStreamParent* aActor)
--- a/dom/ipc/ContentParent.h
+++ b/dom/ipc/ContentParent.h
@@ -427,16 +427,21 @@ public:
 
 #if defined(NS_PRINTING)
   /**
    * @return the PrintingParent for this ContentParent.
    */
   already_AddRefed<embedding::PrintingParent> GetPrintingParent();
 #endif
 
+  virtual mozilla::ipc::IPCResult
+  RecvInitStreamFilter(const uint64_t& aChannelId,
+                       const nsString& aAddonId,
+                       InitStreamFilterResolver&& aResolver) override;
+
   virtual PChildToParentStreamParent* AllocPChildToParentStreamParent() override;
   virtual bool
   DeallocPChildToParentStreamParent(PChildToParentStreamParent* aActor) override;
 
   virtual PParentToChildStreamParent*
   SendPParentToChildStreamConstructor(PParentToChildStreamParent*) override;
 
   virtual PFileDescriptorSetParent*
--- a/dom/ipc/PContent.ipdl
+++ b/dom/ipc/PContent.ipdl
@@ -16,16 +16,17 @@ include protocol PHandlerService;
 include protocol PFileDescriptorSet;
 include protocol PHal;
 include protocol PHeapSnapshotTempFileHelper;
 include protocol PProcessHangMonitor;
 include protocol PImageBridge;
 include protocol PIPCBlobInputStream;
 include protocol PMedia;
 include protocol PNecko;
+include protocol PStreamFilter;
 include protocol PGMPContent;
 include protocol PGMPService;
 include protocol PPluginModule;
 include protocol PGMP;
 include protocol PPrinting;
 include protocol PChildToParentStream;
 include protocol PParentToChildStream;
 include protocol POfflineCacheUpdate;
@@ -670,16 +671,19 @@ parent:
                             TabId openerTabId,
                             TabId tabId)
         returns (ContentParentId cpId, bool isForBrowser);
     sync BridgeToChildProcess(ContentParentId cpId)
         returns (Endpoint<PContentBridgeParent> endpoint);
 
     async CreateGMPService();
 
+    async InitStreamFilter(uint64_t channelId, nsString addonId)
+        returns (Endpoint<PStreamFilterChild> aEndpoint);
+
     /**
      * This call connects the content process to a plugin process. This call
      * returns an endpoint for a new PluginModuleParent. The corresponding
      * PluginModuleChild will be started up in the plugin process.
      */
     sync LoadPlugin(uint32_t aPluginId)
         returns (nsresult aResult, uint32_t aRunID, Endpoint<PPluginModuleParent> aEndpoint);
 
--- a/dom/ipc/TabChild.h
+++ b/dom/ipc/TabChild.h
@@ -752,20 +752,21 @@ public:
 
   void MaybeDispatchCoalescedMouseMoveEvents();
 
   static bool HasActiveTabs()
   {
     return sActiveTabs && !sActiveTabs->IsEmpty();
   }
 
-  // Returns whichever TabChild is currently in the foreground. If there are
-  // multiple TabChilds in the foreground (due to multiple windows being open),
-  // this returns null. This should only be called if HasActiveTabs() returns
-  // true.
+  // Returns the set of TabChilds that are currently in the foreground. There
+  // can be multiple foreground TabChilds if Firefox has multiple windows
+  // open. There can also be zero foreground TabChilds if the foreground tab is
+  // in a different content process. Note that this function should only be
+  // called if HasActiveTabs() returns true.
   static const nsTArray<TabChild*>& GetActiveTabs()
   {
     MOZ_ASSERT(HasActiveTabs());
     return *sActiveTabs;
   }
 
 protected:
   virtual ~TabChild();
--- a/editor/libeditor/tests/chrome.ini
+++ b/editor/libeditor/tests/chrome.ini
@@ -3,13 +3,14 @@ skip-if = os == 'android'
 support-files = green.png
 
 [test_bug489202.xul]
 [test_bug599983.xul]
 [test_bug607584.xul]
 [test_bug616590.xul]
 [test_bug780908.xul]
 [test_bug1386222.xul]
+[test_bug1397412.xul]
 [test_contenteditable_text_input_handling.html]
 [test_htmleditor_keyevent_handling.html]
 [test_texteditor_keyevent_handling.html]
 skip-if = (debug && os=='win') || (os == 'linux') # Bug 1116205, leaks on windows debug, fails delete key on linux
 [test_pasteImgTextarea.xul]
new file mode 100644
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1397412.xul
@@ -0,0 +1,72 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin"
+                 type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+                 type="text/css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1397412
+-->
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+        title="Mozilla Bug 1397412" onload="runTest();">
+  <script type="application/javascript"
+          src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+  <script type="application/javascript"
+          src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+
+  <body xmlns="http://www.w3.org/1999/xhtml">
+  <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1397412"
+     target="_blank">Mozilla Bug 1397412</a>
+  <p/>
+  <editor xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+          id="editor"
+          type="content"
+          editortype="textmail"
+          style="width: 400px; height: 200px;"/>
+  <p/>
+  <pre id="test">
+  </pre>
+  </body>
+  <script class="testbody" type="application/javascript">
+  <![CDATA[
+  var Cc = Components.classes;
+  var Ci = Components.interfaces;
+
+function runTest() {
+  var initialHTML1 = "xx<br><br>";
+  var expectedHTML1 = "xx<br>t<br>";
+  var initialHTML2 = "xx<br><br>yy<br>";
+  var expectedHTML2 = "xx<br>t<br>yy<br>";
+  window.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+     .getInterface(Components.interfaces.nsIWebNavigation)
+     .QueryInterface(Components.interfaces.nsIDocShellTreeItem)
+     .rootTreeItem
+     .QueryInterface(Components.interfaces.nsIDocShell)
+     .appType = Components.interfaces.nsIDocShell.APP_TYPE_EDITOR;
+  var e = document.getElementById("editor");
+  var doc = e.contentDocument;
+  doc.designMode = "on";
+  doc.defaultView.focus();
+  var selection = doc.defaultView.getSelection();
+  var body = doc.body;
+
+  // Test 1.
+  body.innerHTML = initialHTML1;
+  selection.collapse(body, 2);
+  synthesizeKey("t", { code: "KeyT" });
+  var actualHTML = body.innerHTML;
+  is(actualHTML, expectedHTML1, "'t' should be inserted between <br>s");
+
+  // Test 2.
+  body.innerHTML = initialHTML2;
+  selection.collapse(body, 2);
+  synthesizeKey("t", { code: "KeyT" });
+  actualHTML = body.innerHTML;
+  is(actualHTML, expectedHTML2, "'t' should be inserted between <br>s");
+
+  SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+]]>
+</script>
+</window>
--- a/gfx/doc/README.webrender
+++ b/gfx/doc/README.webrender
@@ -74,9 +74,9 @@ there is another crate in m-c called moz
 the same folder to store its rust dependencies. If one of the libraries that is
 required by both mozjs_sys and webrender is updated without updating the other
 project's Cargo.lock file, that results in build bustage.
 This means that any time you do this sort of manual update of packages, you need
 to make sure that mozjs_sys also has its Cargo.lock file updated if needed, hence
 the need to run the cargo update command in js/src as well. Hopefully this will
 be resolved soon.
 
-Latest Commit: fe83b424e7b8bce3d3661c3561cfd58a6cd186fe
+Latest Commit: dbffdc219fa394ab1225cebc71fe5998b9337cb2
--- a/gfx/webrender/src/prim_store.rs
+++ b/gfx/webrender/src/prim_store.rs
@@ -157,17 +157,17 @@ pub struct PrimitiveMetadata {
 }
 
 impl PrimitiveMetadata {
     pub fn needs_clipping(&self) -> bool {
         self.clip_task_id.is_some()
     }
 }
 
-#[derive(Debug, Clone)]
+#[derive(Debug)]
 #[repr(C)]
 pub struct RectanglePrimitive {
     pub color: ColorF,
 }
 
 impl ToGpuBlocks for RectanglePrimitive {
     fn write_gpu_blocks(&self, mut request: GpuDataRequest) {
         request.push(self.color);
@@ -221,17 +221,17 @@ pub struct YuvImagePrimitiveCpu {
 }
 
 impl ToGpuBlocks for YuvImagePrimitiveCpu {
     fn write_gpu_blocks(&self, mut request: GpuDataRequest) {
         request.push(self.gpu_block);
     }
 }
 
-#[derive(Debug, Clone)]
+#[derive(Debug)]
 pub struct BorderPrimitiveCpu {
     pub corner_instances: [BorderCornerInstance; 4],
     pub gpu_blocks: [GpuBlockData; 8],
 }
 
 impl ToGpuBlocks for BorderPrimitiveCpu {
     fn write_gpu_blocks(&self, mut request: GpuDataRequest) {
         request.extend_from_slice(&self.gpu_blocks);
@@ -241,17 +241,17 @@ impl ToGpuBlocks for BorderPrimitiveCpu 
 #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
 pub struct BoxShadowPrimitiveCacheKey {
     pub shadow_rect_size: Size2D<Au>,
     pub border_radius: Au,
     pub blur_radius: Au,
     pub inverted: bool,
 }
 
-#[derive(Debug, Clone)]
+#[derive(Debug)]
 pub struct BoxShadowPrimitiveCpu {
     // todo(gw): generate on demand
     // gpu data
     pub src_rect: LayerRect,
     pub bs_rect: LayerRect,
     pub color: ColorF,
     pub border_radius: f32,
     pub edge_size: f32,
@@ -322,17 +322,17 @@ pub const GRADIENT_DATA_TABLE_BEGIN: usi
 // The exclusive bound of the gradient data table
 pub const GRADIENT_DATA_TABLE_END: usize = GRADIENT_DATA_LAST_STOP;
 // The number of entries in the gradient data table.
 pub const GRADIENT_DATA_TABLE_SIZE: usize = 128;
 
 // The number of entries in a gradient data: GRADIENT_DATA_TABLE_SIZE + first stop entry + last stop entry
 pub const GRADIENT_DATA_SIZE: usize = GRADIENT_DATA_TABLE_SIZE + 2;
 
-#[derive(Debug, Clone, Copy)]
+#[derive(Debug)]
 #[repr(C)]
 // An entry in a gradient data table representing a segment of the gradient color space.
 pub struct GradientDataEntry {
     pub start_color: ColorF,
     pub end_color: ColorF,
 }
 
 struct GradientGpuBlockBuilder<'a> {
@@ -484,23 +484,17 @@ impl RadialGradientPrimitiveCpu {
         request.extend_from_slice(&self.gpu_blocks);
 
         let gradient_builder = GradientGpuBlockBuilder::new(self.stops_range,
                                                             display_list);
         gradient_builder.build(false, &mut request);
     }
 }
 
-#[derive(Debug, Clone)]
-pub struct TextDecoration {
-    pub local_rect: LayerRect,
-    pub prim: RectanglePrimitive,
-}
-
-#[derive(Debug, Clone)]
+#[derive(Debug)]
 pub struct TextShadowPrimitiveCpu {
     pub shadow: TextShadow,
     pub primitives: Vec<PrimitiveIndex>,
 }
 
 #[derive(Debug, Clone)]
 pub struct TextRunPrimitiveCpu {
     pub font: FontInstance,
@@ -588,31 +582,24 @@ impl TextRunPrimitiveCpu {
                       self.font.subpx_dir as u32 as f32,
                       0.0]);
         request.extend_from_slice(&self.glyph_gpu_blocks);
 
         assert!(request.current_used_block_num() <= MAX_VERTEX_TEXTURE_WIDTH);
     }
 }
 
-#[derive(Debug, Clone)]
-#[repr(C)]
-struct GlyphPrimitive {
-    offset: LayerPoint,
-    padding: LayerPoint,
-}
-
-#[derive(Debug, Clone)]
+#[derive(Debug)]
 #[repr(C)]
 struct ClipRect {
     rect: LayerRect,
     mode: f32,
 }
 
-#[derive(Debug, Clone)]
+#[derive(Debug)]
 #[repr(C)]
 struct ClipCorner {
     rect: LayerRect,
     outer_radius_x: f32,
     outer_radius_y: f32,
     inner_radius_x: f32,
     inner_radius_y: f32,
 }
@@ -637,29 +624,29 @@ impl ClipCorner {
             outer_radius_x: outer_radius,
             outer_radius_y: outer_radius,
             inner_radius_x: inner_radius,
             inner_radius_y: inner_radius,
         }
     }
 }
 
-#[derive(Debug, Clone)]
+#[derive(Debug)]
 #[repr(C)]
 pub struct ImageMaskData {
     pub local_rect: LayerRect,
 }
 
 impl ToGpuBlocks for ImageMaskData {
     fn write_gpu_blocks(&self, mut request: GpuDataRequest) {
         request.push(self.local_rect);
     }
 }
 
-#[derive(Debug, Clone)]
+#[derive(Debug)]
 pub struct ClipData {
     rect: ClipRect,
     top_left: ClipCorner,
     top_right: ClipCorner,
     bottom_left: ClipCorner,
     bottom_right: ClipCorner,
 }
 
--- a/gfx/webrender/src/render_task.rs
+++ b/gfx/webrender/src/render_task.rs
@@ -112,32 +112,32 @@ impl RenderTaskTree {
 #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
 pub enum RenderTaskKey {
     /// Draw this box shadow to a cache target.
     BoxShadow(BoxShadowPrimitiveCacheKey),
     /// Draw the alpha mask for a shared clip.
     CacheMask(ClipId),
 }
 
-#[derive(Debug, Clone)]
+#[derive(Debug)]
 pub enum RenderTaskLocation {
     Fixed,
     Dynamic(Option<(DeviceIntPoint, RenderTargetIndex)>, DeviceIntSize),
 }
 
-#[derive(Debug, Clone)]
+#[derive(Debug)]
 pub enum AlphaRenderItem {
     Primitive(Option<ClipScrollGroupIndex>, PrimitiveIndex, i32),
     Blend(StackingContextIndex, RenderTaskId, FilterOp, i32),
     Composite(StackingContextIndex, RenderTaskId, RenderTaskId, MixBlendMode, i32),
     SplitComposite(StackingContextIndex, RenderTaskId, GpuCacheHandle, i32),
     HardwareComposite(StackingContextIndex, RenderTaskId, HardwareCompositeOp, i32),
 }
 
-#[derive(Debug, Clone)]
+#[derive(Debug)]
 pub struct AlphaRenderTask {
     pub screen_origin: DeviceIntPoint,
     pub items: Vec<AlphaRenderItem>,
 }
 
 #[derive(Debug, Copy, Clone)]
 #[repr(C)]
 pub enum MaskSegment {
@@ -154,42 +154,42 @@ pub enum MaskSegment {
 pub enum MaskGeometryKind {
     Default,        // Draw the entire rect
     CornersOnly,    // Draw the corners (simple axis aligned mask)
     // TODO(gw): Add more types here (e.g. 4 rectangles outside the inner rect)
 }
 
 pub type ClipWorkItem = (PackedLayerIndex, MaskCacheInfo);
 
-#[derive(Debug, Clone)]
+#[derive(Debug)]
 pub struct CacheMaskTask {
     actual_rect: DeviceIntRect,
     inner_rect: DeviceIntRect,
     pub clips: Vec<ClipWorkItem>,
     pub geometry_kind: MaskGeometryKind,
 }
 
-#[derive(Debug, Clone)]
+#[derive(Debug)]
 pub struct RenderTaskData {
     pub data: [f32; FLOATS_PER_RENDER_TASK_INFO],
 }
 
-#[derive(Debug, Clone)]
+#[derive(Debug)]
 pub enum RenderTaskKind {
     Alpha(AlphaRenderTask),
     CachePrimitive(PrimitiveIndex),
     BoxShadow(PrimitiveIndex),
     CacheMask(CacheMaskTask),
     VerticalBlur(DeviceIntLength),
     HorizontalBlur(DeviceIntLength),
     Readback(DeviceIntRect),
     Alias(RenderTaskId),
 }
 
-#[derive(Debug, Clone)]
+#[derive(Debug)]
 pub struct RenderTask {
     pub cache_key: Option<RenderTaskKey>,
     pub location: RenderTaskLocation,
     pub children: Vec<RenderTaskId>,
     pub kind: RenderTaskKind,
 }
 
 impl RenderTask {
--- a/gfx/webrender/src/renderer.rs
+++ b/gfx/webrender/src/renderer.rs
@@ -221,30 +221,25 @@ const DESC_CACHE_BOX_SHADOW: VertexDescr
         VertexAttribute { name: "aPosition", count: 2, kind: VertexAttributeKind::F32 },
     ],
     instance_attributes: &[
         VertexAttribute { name: "aPrimAddress", count: 2, kind: VertexAttributeKind::U16 },
         VertexAttribute { name: "aTaskIndex", count: 1, kind: VertexAttributeKind::I32 },
     ]
 };
 
+#[derive(Debug, Copy, Clone)]
 enum VertexArrayKind {
     Primitive,
     Blur,
     Clip,
 
     CacheBoxShadow,
 }
 
-#[derive(Copy, Clone, Debug, PartialEq, Eq)]
-pub enum VertexFormat {
-    PrimitiveInstances,
-    Blur,
-}
-
 #[derive(Clone, Debug, PartialEq)]
 pub enum GraphicsApi {
     OpenGL,
 }
 
 #[derive(Clone, Debug)]
 pub struct GraphicsApiInfo {
     pub kind: GraphicsApi,
@@ -702,17 +697,17 @@ impl VertexDataTexture {
 }
 
 const TRANSFORM_FEATURE: &str = "TRANSFORM";
 const SUBPIXEL_AA_FEATURE: &str = "SUBPIXEL_AA";
 const CLIP_FEATURE: &str = "CLIP";
 
 enum ShaderKind {
     Primitive,
-    Cache(VertexFormat),
+    Cache(VertexArrayKind),
     ClipCache,
 }
 
 struct LazilyCompiledShader {
     program: Option<Program>,
     name: &'static str,
     kind: ShaderKind,
     features: Vec<&'static str>,
@@ -753,17 +748,17 @@ impl LazilyCompiledShader {
     fn get(&mut self, device: &mut Device) -> Result<&Program, ShaderError> {
         if self.program.is_none() {
             let program = try!{
                 match self.kind {
                     ShaderKind::Primitive => {
                         create_prim_shader(self.name,
                                            device,
                                            &self.features,
-                                           VertexFormat::PrimitiveInstances)
+                                           VertexArrayKind::Primitive)
                     }
                     ShaderKind::Cache(format) => {
                         create_prim_shader(self.name,
                                            device,
                                            &self.features,
                                            format)
                     }
                     ShaderKind::ClipCache => {
@@ -847,29 +842,31 @@ impl PrimitiveShader {
         self.simple.deinit(device);
         self.transform.deinit(device);
     }
 }
 
 fn create_prim_shader(name: &'static str,
                       device: &mut Device,
                       features: &[&'static str],
-                      vertex_format: VertexFormat) -> Result<Program, ShaderError> {
+                      vertex_format: VertexArrayKind) -> Result<Program, ShaderError> {
     let mut prefix = format!("#define WR_MAX_VERTEX_TEXTURE_WIDTH {}\n",
                               MAX_VERTEX_TEXTURE_WIDTH);
 
     for feature in features {
         prefix.push_str(&format!("#define WR_FEATURE_{}\n", feature));
     }
 
     debug!("PrimShader {}", name);
 
     let vertex_descriptor = match vertex_format {
-        VertexFormat::PrimitiveInstances => DESC_PRIM_INSTANCES,
-        VertexFormat::Blur => DESC_BLUR,
+        VertexArrayKind::Primitive => DESC_PRIM_INSTANCES,
+        VertexArrayKind::Blur => DESC_BLUR,
+        VertexArrayKind::Clip => DESC_CLIP,
+        VertexArrayKind::CacheBoxShadow => DESC_CACHE_BOX_SHADOW,
     };
 
     let program = device.create_program(name,
                                         &prefix,
                                         &vertex_descriptor);
 
     if let Ok(ref program) = program {
         device.bind_shader_samplers(program, &[
@@ -1090,41 +1087,41 @@ impl Renderer {
         );
 
         register_thread_with_profiler("Compositor".to_owned());
 
         // device-pixel ratio doesn't matter here - we are just creating resources.
         device.begin_frame(1.0);
 
         let cs_box_shadow = try!{
-            LazilyCompiledShader::new(ShaderKind::Cache(VertexFormat::PrimitiveInstances),
+            LazilyCompiledShader::new(ShaderKind::Cache(VertexArrayKind::CacheBoxShadow),
                                       "cs_box_shadow",
                                       &[],
                                       &mut device,
                                       options.precache_shaders)
         };
 
         let cs_text_run = try!{
-            LazilyCompiledShader::new(ShaderKind::Cache(VertexFormat::PrimitiveInstances),
+            LazilyCompiledShader::new(ShaderKind::Cache(VertexArrayKind::Primitive),
                                       "cs_text_run",
                                       &[],
                                       &mut device,
                                       options.precache_shaders)
         };
 
         let cs_line = try!{
-            LazilyCompiledShader::new(ShaderKind::Cache(VertexFormat::PrimitiveInstances),
+            LazilyCompiledShader::new(ShaderKind::Cache(VertexArrayKind::Primitive),
                                       "ps_line",
                                       &["CACHE"],
                                       &mut device,
                                       options.precache_shaders)
         };
 
         let cs_blur = try!{
-            LazilyCompiledShader::new(ShaderKind::Cache(VertexFormat::Blur),
+            LazilyCompiledShader::new(ShaderKind::Cache(VertexArrayKind::Blur),
                                      "cs_blur",
                                       &[],
                                       &mut device,
                                       options.precache_shaders)
         };
 
         let cs_clip_rectangle = try!{
             LazilyCompiledShader::new(ShaderKind::ClipCache,
--- a/gfx/webrender/src/tiling.rs
+++ b/gfx/webrender/src/tiling.rs
@@ -1011,16 +1011,17 @@ impl RenderTarget for ColorRenderTarget 
                                 PrimitiveKind::TextRun => {
                                     // Add instances that reference the text run GPU location. Also supply
                                     // the parent text-shadow prim address as a user data field, allowing
                                     // the shader to fetch the text-shadow parameters.
                                     let text = &ctx.prim_store.cpu_text_runs[sub_metadata.cpu_prim_index.0];
 
                                     let mut font = text.font.clone();
                                     font.size = font.size.scale_by(ctx.device_pixel_ratio);
+                                    font.render_mode = text.shadow_render_mode;
 
                                     let texture_id = ctx.resource_cache.get_glyphs(font,
                                                                                    &text.glyph_keys,
                                                                                    |index, handle| {
                                         let uv_address = handle.as_int(gpu_cache);
                                         instances.push(instance.build(index as i32,
                                                                       uv_address,
                                                                       prim_address));
--- a/ipc/glue/BackgroundChildImpl.cpp
+++ b/ipc/glue/BackgroundChildImpl.cpp
@@ -27,17 +27,16 @@
 #include "mozilla/dom/quota/PQuotaChild.h"
 #include "mozilla/dom/StorageIPC.h"
 #include "mozilla/dom/GamepadEventChannelChild.h"
 #include "mozilla/dom/GamepadTestChannelChild.h"
 #include "mozilla/dom/LocalStorage.h"
 #include "mozilla/dom/MessagePortChild.h"
 #include "mozilla/dom/TabChild.h"
 #include "mozilla/dom/TabGroup.h"
-#include "mozilla/extensions/StreamFilterChild.h"
 #include "mozilla/ipc/IPCStreamAlloc.h"
 #include "mozilla/ipc/PBackgroundTestChild.h"
 #include "mozilla/ipc/PChildToParentStreamChild.h"
 #include "mozilla/ipc/PParentToChildStreamChild.h"
 #include "mozilla/layout/VsyncChild.h"
 #include "mozilla/net/HttpBackgroundChannelChild.h"
 #include "mozilla/net/PUDPSocketChild.h"
 #include "mozilla/dom/network/UDPSocketChild.h"
@@ -423,36 +422,16 @@ BackgroundChildImpl::AllocPCacheStreamCo
 bool
 BackgroundChildImpl::DeallocPCacheStreamControlChild(PCacheStreamControlChild* aActor)
 {
   dom::cache::DeallocPCacheStreamControlChild(aActor);
   return true;
 }
 
 // -----------------------------------------------------------------------------
-// StreamFilter API
-// -----------------------------------------------------------------------------
-
-extensions::PStreamFilterChild*
-BackgroundChildImpl::AllocPStreamFilterChild(const uint64_t& aChannelId, const nsString& aAddonId)
-{
-  RefPtr<extensions::StreamFilterChild> agent = new extensions::StreamFilterChild();
-  return agent.forget().take();
-}
-
-bool
-BackgroundChildImpl::DeallocPStreamFilterChild(PStreamFilterChild* aActor)
-{
-  RefPtr<extensions::StreamFilterChild> child =
-    dont_AddRef(static_cast<extensions::StreamFilterChild*>(aActor));
-  MOZ_ASSERT(child);
-  return true;
-}
-
-// -----------------------------------------------------------------------------
 // MessageChannel/MessagePort API
 // -----------------------------------------------------------------------------
 
 dom::PMessagePortChild*
 BackgroundChildImpl::AllocPMessagePortChild(const nsID& aUUID,
                                             const nsID& aDestinationUUID,
                                             const uint32_t& aSequenceID)
 {
--- a/ipc/glue/BackgroundChildImpl.h
+++ b/ipc/glue/BackgroundChildImpl.h
@@ -148,23 +148,16 @@ protected:
 
   virtual PMessagePortChild*
   AllocPMessagePortChild(const nsID& aUUID, const nsID& aDestinationUUID,
                          const uint32_t& aSequenceID) override;
 
   virtual bool
   DeallocPMessagePortChild(PMessagePortChild* aActor) override;
 
-  virtual PStreamFilterChild*
-  AllocPStreamFilterChild(const uint64_t& aChannelId,
-                          const nsString& aAddonId) override;
-
-  virtual bool
-  DeallocPStreamFilterChild(PStreamFilterChild* aActor) override;
-
   virtual PChildToParentStreamChild*
   AllocPChildToParentStreamChild() override;
 
   virtual bool
   DeallocPChildToParentStreamChild(PChildToParentStreamChild* aActor) override;
 
   virtual PParentToChildStreamChild*
   AllocPParentToChildStreamChild() override;
--- a/ipc/glue/BackgroundParentImpl.cpp
+++ b/ipc/glue/BackgroundParentImpl.cpp
@@ -25,17 +25,16 @@
 #include "mozilla/dom/ServiceWorkerRegistrar.h"
 #include "mozilla/dom/asmjscache/AsmJSCache.h"
 #include "mozilla/dom/cache/ActorUtils.h"
 #include "mozilla/dom/indexedDB/ActorsParent.h"
 #include "mozilla/dom/ipc/IPCBlobInputStreamParent.h"
 #include "mozilla/dom/ipc/PendingIPCBlobParent.h"
 #include "mozilla/dom/quota/ActorsParent.h"
 #include "mozilla/dom/StorageIPC.h"
-#include "mozilla/extensions/StreamFilterParent.h"
 #include "mozilla/ipc/BackgroundParent.h"
 #include "mozilla/ipc/BackgroundUtils.h"
 #include "mozilla/ipc/IPCStreamAlloc.h"
 #include "mozilla/ipc/PBackgroundSharedTypes.h"
 #include "mozilla/ipc/PBackgroundTestParent.h"
 #include "mozilla/ipc/PChildToParentStreamParent.h"
 #include "mozilla/ipc/PParentToChildStreamParent.h"
 #include "mozilla/layout/VsyncParent.h"
@@ -64,17 +63,16 @@ using mozilla::dom::cache::PCacheParent;
 using mozilla::dom::cache::PCacheStorageParent;
 using mozilla::dom::cache::PCacheStreamControlParent;
 using mozilla::dom::FileSystemBase;
 using mozilla::dom::FileSystemRequestParent;
 using mozilla::dom::MessagePortParent;
 using mozilla::dom::PMessagePortParent;
 using mozilla::dom::UDPSocketParent;
 using mozilla::dom::WebAuthnTransactionParent;
-using mozilla::extensions::StreamFilterParent;
 
 namespace {
 
 void
 AssertIsOnMainThread()
 {
   MOZ_ASSERT(NS_IsMainThread());
 }
@@ -712,54 +710,16 @@ BackgroundParentImpl::AllocPCacheStreamC
 
 bool
 BackgroundParentImpl::DeallocPCacheStreamControlParent(PCacheStreamControlParent* aActor)
 {
   dom::cache::DeallocPCacheStreamControlParent(aActor);
   return true;
 }
 
-PStreamFilterParent*
-BackgroundParentImpl::AllocPStreamFilterParent(const uint64_t& aChannelId, const nsString& aAddonId)
-{
-  AssertIsInMainProcess();
-  AssertIsOnBackgroundThread();
-
-  return StreamFilterParent::Create(aChannelId, aAddonId).take();
-}
-
-mozilla::ipc::IPCResult
-BackgroundParentImpl::RecvPStreamFilterConstructor(PStreamFilterParent* aActor,
-                                                   const uint64_t& aChannelId,
-                                                   const nsString& aAddonId)
-{
-  AssertIsInMainProcess();
-  AssertIsOnBackgroundThread();
-
-  StreamFilterParent* filter = static_cast<StreamFilterParent*>(aActor);
-
-  RefPtr<ContentParent> parent = BackgroundParent::GetContentParent(this);
-
-  filter->Init(parent.forget());
-
-  return IPC_OK();
-}
-
-bool
-BackgroundParentImpl::DeallocPStreamFilterParent(PStreamFilterParent* aActor)
-{
-  AssertIsInMainProcess();
-  AssertIsOnBackgroundThread();
-  MOZ_ASSERT(aActor);
-
-  RefPtr<StreamFilterParent> filter = dont_AddRef(
-      static_cast<StreamFilterParent*>(aActor));
-  return true;
-}
-
 PMessagePortParent*
 BackgroundParentImpl::AllocPMessagePortParent(const nsID& aUUID,
                                               const nsID& aDestinationUUID,
                                               const uint32_t& aSequenceID)
 {
   AssertIsInMainProcess();
   AssertIsOnBackgroundThread();
 
--- a/ipc/glue/BackgroundParentImpl.h
+++ b/ipc/glue/BackgroundParentImpl.h
@@ -13,18 +13,16 @@
 namespace mozilla {
 
 namespace layout {
 class VsyncParent;
 } // namespace layout
 
 namespace ipc {
 
-using mozilla::extensions::PStreamFilterParent;
-
 // Instances of this class should never be created directly. This class is meant
 // to be inherited in BackgroundImpl.
 class BackgroundParentImpl : public PBackgroundParent
 {
 protected:
   BackgroundParentImpl();
   virtual ~BackgroundParentImpl();
 
@@ -199,28 +197,16 @@ protected:
   virtual bool
   DeallocPMessagePortParent(PMessagePortParent* aActor) override;
 
   virtual mozilla::ipc::IPCResult
   RecvMessagePortForceClose(const nsID& aUUID,
                             const nsID& aDestinationUUID,
                             const uint32_t& aSequenceID) override;
 
-  virtual PStreamFilterParent*
-  AllocPStreamFilterParent(const uint64_t& aChannelId,
-                           const nsString& aAddonId) override;
-
-  virtual mozilla::ipc::IPCResult
-  RecvPStreamFilterConstructor(PStreamFilterParent* aActor,
-                               const uint64_t& aChannelId,
-                               const nsString& aAddonId) override;
-
-  virtual bool
-  DeallocPStreamFilterParent(PStreamFilterParent* aActor) override;
-
   virtual PAsmJSCacheEntryParent*
   AllocPAsmJSCacheEntryParent(const dom::asmjscache::OpenMode& aOpenMode,
                               const dom::asmjscache::WriteParams& aWriteParams,
                               const PrincipalInfo& aPrincipalInfo) override;
 
   virtual bool
   DeallocPAsmJSCacheEntryParent(PAsmJSCacheEntryParent* aActor) override;
 
--- a/ipc/glue/MessageChannel.cpp
+++ b/ipc/glue/MessageChannel.cpp
@@ -1840,27 +1840,36 @@ MessageChannel::RunMessage(MessageTask& 
     Message& msg = aTask.Msg();
 
     if (!Connected()) {
         ReportConnectionError("RunMessage");
         return;
     }
 
     // Check that we're going to run the first message that's valid to run.
+#if 0
 #ifdef DEBUG
+    nsCOMPtr<nsIEventTarget> messageTarget =
+        mListener->GetMessageEventTarget(msg);
+
     for (MessageTask* task : mPending) {
         if (task == &aTask) {
             break;
         }
 
+        nsCOMPtr<nsIEventTarget> taskTarget =
+            mListener->GetMessageEventTarget(task->Msg());
+
         MOZ_ASSERT(!ShouldRunMessage(task->Msg()) ||
+                   taskTarget != messageTarget ||
                    aTask.Msg().priority() != task->Msg().priority());
 
     }
 #endif
+#endif
 
     if (!mDeferred.empty()) {
         MaybeUndeferIncall();
     }
 
     if (!ShouldRunMessage(msg)) {
         return;
     }
--- a/ipc/glue/PBackground.ipdl
+++ b/ipc/glue/PBackground.ipdl
@@ -14,17 +14,16 @@ include protocol PCacheStreamControl;
 include protocol PFileDescriptorSet;
 include protocol PFileSystemRequest;
 include protocol PGamepadEventChannel;
 include protocol PGamepadTestChannel;
 include protocol PHttpBackgroundChannel;
 include protocol PIPCBlobInputStream;
 include protocol PPendingIPCBlob;
 include protocol PMessagePort;
-include protocol PStreamFilter;
 include protocol PCameras;
 include protocol PQuota;
 include protocol PChildToParentStream;
 include protocol PParentToChildStream;
 include protocol PServiceWorkerManager;
 include protocol PWebAuthnTransaction;
 include protocol PUDPSocket;
 include protocol PVsync;
@@ -64,17 +63,16 @@ sync protocol PBackground
   manages PFileDescriptorSet;
   manages PFileSystemRequest;
   manages PGamepadEventChannel;
   manages PGamepadTestChannel;
   manages PHttpBackgroundChannel;
   manages PIPCBlobInputStream;
   manages PPendingIPCBlob;
   manages PMessagePort;
-  manages PStreamFilter;
   manages PCameras;
   manages PQuota;
   manages PChildToParentStream;
   manages PParentToChildStream;
   manages PServiceWorkerManager;
   manages PWebAuthnTransaction;
   manages PUDPSocket;
   manages PVsync;
@@ -109,18 +107,16 @@ parent:
   async PServiceWorkerManager();
 
   async ShutdownServiceWorkerRegistrar();
 
   async PCacheStorage(Namespace aNamespace, PrincipalInfo aPrincipalInfo);
 
   async PMessagePort(nsID uuid, nsID destinationUuid, uint32_t sequenceId);
 
-  async PStreamFilter(uint64_t channelId, nsString addonId);
-
   async PChildToParentStream();
 
   async MessagePortForceClose(nsID uuid, nsID destinationUuid, uint32_t sequenceId);
 
   async PAsmJSCacheEntry(OpenMode openMode,
                          WriteParams write,
                          PrincipalInfo principalInfo);
 
--- a/ipc/glue/ProtocolUtils.cpp
+++ b/ipc/glue/ProtocolUtils.cpp
@@ -821,23 +821,16 @@ IToplevelProtocol::ShmemDestroyed(const 
     Shmem::Dealloc(Shmem::IHadBetterBeIPDLCodeCallingThis_OtherwiseIAmADoodyhead(), rawmem);
   }
   return true;
 }
 
 already_AddRefed<nsIEventTarget>
 IToplevelProtocol::GetMessageEventTarget(const Message& aMsg)
 {
-  if (IsMainThreadProtocol() && SystemGroup::Initialized()) {
-    if (aMsg.type() == SHMEM_CREATED_MESSAGE_TYPE ||
-        aMsg.type() == SHMEM_DESTROYED_MESSAGE_TYPE) {
-      return do_AddRef(SystemGroup::EventTargetFor(TaskCategory::Other));
-    }
-  }
-
   int32_t route = aMsg.routing_id();
 
   Maybe<MutexAutoLock> lock;
   lock.emplace(mEventTargetMutex);
 
   nsCOMPtr<nsIEventTarget> target = mEventTargetMap.Lookup(route);
 
   if (aMsg.is_constructor()) {
--- a/ipc/ipdl/test/cxx/TestAsyncReturns.cpp
+++ b/ipc/ipdl/test/cxx/TestAsyncReturns.cpp
@@ -52,17 +52,17 @@ TestAsyncReturnsParent::Main()
                      fail("sending Ping");
                    });
 }
 
 
 mozilla::ipc::IPCResult
 TestAsyncReturnsParent::RecvPong(PongResolver&& aResolve)
 {
-  aResolve(MakeTuple(sMagic1, sMagic2));
+  aResolve(Tuple<const uint32_t&, const uint32_t&>(sMagic1, sMagic2));
   return IPC_OK();
 }
 
 
 //-----------------------------------------------------------------------------
 // child
 
 TestAsyncReturnsChild::TestAsyncReturnsChild()
--- a/ipc/mscom/ProxyStream.cpp
+++ b/ipc/mscom/ProxyStream.cpp
@@ -44,34 +44,35 @@ ProxyStream::ProxyStream(REFIID aIID, co
   , mHGlobal(nullptr)
   , mBufSize(aInitBufSize)
   , mPreserveStream(false)
 {
 #if defined(MOZ_CRASHREPORTER)
   NS_NAMED_LITERAL_CSTRING(kCrashReportKey, "ProxyStreamUnmarshalStatus");
 #endif
 
+  if (!aInitBufSize) {
+#if defined(MOZ_CRASHREPORTER)
+    CrashReporter::AnnotateCrashReport(kCrashReportKey,
+                                       NS_LITERAL_CSTRING("!aInitBufSize"));
+#endif // defined(MOZ_CRASHREPORTER)
+    // We marshaled a nullptr. Nothing else to do here.
+    return;
+  }
+
   HRESULT createStreamResult = CreateStream(aInitBuf, aInitBufSize,
                                             getter_AddRefs(mStream));
   if (FAILED(createStreamResult)) {
 #if defined(MOZ_CRASHREPORTER)
     nsPrintfCString hrAsStr("0x%08X", createStreamResult);
     CrashReporter::AnnotateCrashReport(kCrashReportKey, hrAsStr);
 #endif // defined(MOZ_CRASHREPORTER)
     return;
   }
 
-  if (!aInitBufSize) {
-#if defined(MOZ_CRASHREPORTER)
-    CrashReporter::AnnotateCrashReport(kCrashReportKey,
-                                       NS_LITERAL_CSTRING("!aInitBufSize"));
-#endif // defined(MOZ_CRASHREPORTER)
-    // We marshaled a nullptr. Nothing else to do here.
-    return;
-  }
   // NB: We can't check for a null mStream until after we have checked for
   // the zero aInitBufSize above. This is because InitStream will also fail
   // in that case, even though marshaling a nullptr is allowable.
   MOZ_ASSERT(mStream);
   if (!mStream) {
 #if defined(MOZ_CRASHREPORTER)
     CrashReporter::AnnotateCrashReport(kCrashReportKey,
                                        NS_LITERAL_CSTRING("!mStream"));
--- a/layout/svg/SVGTextFrame.cpp
+++ b/layout/svg/SVGTextFrame.cpp
@@ -14,16 +14,17 @@
 #include "gfxSkipChars.h"
 #include "gfxTypes.h"
 #include "gfxUtils.h"
 #include "LookAndFeel.h"
 #include "mozilla/gfx/2D.h"
 #include "mozilla/gfx/PatternHelpers.h"
 #include "mozilla/Likely.h"
 #include "nsAlgorithm.h"
+#include "nsBidiPresUtils.h"
 #include "nsBlockFrame.h"
 #include "nsCaret.h"
 #include "nsContentUtils.h"
 #include "nsGkAtoms.h"
 #include "nsIDOMSVGLength.h"
 #include "nsISelection.h"
 #include "nsQuickSort.h"
 #include "nsSVGEffects.h"
@@ -2138,17 +2139,18 @@ public:
    * @param aSVGTextFrame The SVGTextFrame whose characters to iterate
    *   through.
    * @param aFilter Indicates which characters to iterate over.
    * @param aSubtree A content subtree to track whether the current character
    *   is within.
    */
   CharIterator(SVGTextFrame* aSVGTextFrame,
                CharacterFilter aFilter,
-               nsIContent* aSubtree = nullptr);
+               nsIContent* aSubtree,
+               bool aPostReflow = true);
 
   /**
    * Returns whether the iterator is finished.
    */
   bool AtEnd() const
   {
     return !mFrameIterator.Current();
   }
@@ -2411,29 +2413,37 @@ private:
    */
   uint32_t mGlyphUndisplayedCharacters;
 
   /**
    * The scale factor to apply to glyph advances returned by
    * GetGlyphAdvance etc. to take into account textLength="".
    */
   float mLengthAdjustScaleFactor;
+
+  /**
+   * Whether the instance of this class is being used after reflow has occurred
+   * or not.
+   */
+  bool mPostReflow;
 };
 
 CharIterator::CharIterator(SVGTextFrame* aSVGTextFrame,
                            CharIterator::CharacterFilter aFilter,
-                           nsIContent* aSubtree)
+                           nsIContent* aSubtree,
+                           bool aPostReflow)
   : mFilter(aFilter),
     mFrameIterator(FrameIfAnonymousChildReflowed(aSVGTextFrame), aSubtree),
     mFrameForTrimCheck(nullptr),
     mTrimmedOffset(0),
     mTrimmedLength(0),
     mTextElementCharIndex(0),
     mGlyphStartTextElementCharIndex(0),
     mLengthAdjustScaleFactor(aSVGTextFrame->mLengthAdjustScaleFactor)
+  , mPostReflow(aPostReflow)
 {
   if (!AtEnd()) {
     mSkipCharsIterator = TextFrame()->EnsureTextRun(nsTextFrame::eInflated);
     mTextRun = TextFrame()->GetTextRun(nsTextFrame::eInflated);
     mTextElementCharIndex = mFrameIterator.UndisplayedCharacters();
     UpdateGlyphStartTextElementCharIndex();
     if (!MatchesFilter()) {
       Next();
@@ -2544,17 +2554,19 @@ CharIterator::IsOriginalCharTrimmed() co
   if (mFrameForTrimCheck != TextFrame()) {
     // Since we do a lot of trim checking, we cache the trimmed offsets and
     // lengths while we are in the same frame.
     mFrameForTrimCheck = TextFrame();
     uint32_t offset = mFrameForTrimCheck->GetContentOffset();
     uint32_t length = mFrameForTrimCheck->GetContentLength();
     nsIContent* content = mFrameForTrimCheck->GetContent();
     nsTextFrame::TrimmedOffsets trim =
-      mFrameForTrimCheck->GetTrimmedOffsets(content->GetText(), true);
+      mFrameForTrimCheck->GetTrimmedOffsets(content->GetText(),
+                                            /* aTrimAfter */ true,
+                                            mPostReflow);
     TrimOffsets(offset, length, trim);
     mTrimmedOffset = offset;
     mTrimmedLength = length;
   }
 
   // A character is trimmed if it is outside the mTrimmedOffset/mTrimmedLength
   // range and it is not a significant newline character.
   uint32_t index = mSkipCharsIterator.GetOriginalOffset();
@@ -4521,17 +4533,17 @@ SVGTextFrame::ResolvePositionsForNode(ns
 
 bool
 SVGTextFrame::ResolvePositions(nsTArray<gfxPoint>& aDeltas,
                                bool aRunPerGlyph)
 {
   NS_ASSERTION(mPositions.IsEmpty(), "expected mPositions to be empty");
   RemoveStateBits(NS_STATE_SVG_POSITIONING_MAY_USE_PERCENTAGES);
 
-  CharIterator it(this, CharIterator::eOriginal);
+  CharIterator it(this, CharIterator::eOriginal, /* aSubtree */ nullptr);
   if (it.AtEnd()) {
     return false;
   }
 
   // We assume the first character position is (0,0) unless we later see
   // otherwise, and note it as unaddressable if it is.
   bool firstCharUnaddressable = it.IsOriginalCharUnaddressable();
   mPositions.AppendElement(CharPosition::Unspecified(firstCharUnaddressable));
@@ -4721,32 +4733,33 @@ ShiftAnchoredChunk(nsTArray<mozilla::Cha
 void
 SVGTextFrame::AdjustChunksForLineBreaks()
 {
   nsBlockFrame* block = nsLayoutUtils::GetAsBlock(PrincipalChildList().FirstChild());
   NS_ASSERTION(block, "expected block frame");
 
   nsBlockFrame::LineIterator line = block->LinesBegin();
 
-  CharIterator it(this, CharIterator::eOriginal);
+  CharIterator it(this, CharIterator::eOriginal, /* aSubtree */ nullptr);
   while (!it.AtEnd() && line != block->LinesEnd()) {
     if (it.TextFrame() == line->mFirstChild) {
       mPositions[it.TextElementCharIndex()].mStartOfChunk = true;
       line++;
     }
     it.AdvancePastCurrentFrame();
   }
 }
 
 void
 SVGTextFrame::AdjustPositionsForClusters()
 {
   nsPresContext* presContext = PresContext();
 
-  CharIterator it(this, CharIterator::eClusterOrLigatureGroupMiddle);
+  CharIterator it(this, CharIterator::eClusterOrLigatureGroupMiddle,
+                  /* aSubtree */ nullptr);
   while (!it.AtEnd()) {
     // Find the start of the cluster/ligature group.
     uint32_t charIndex = it.TextElementCharIndex();
     uint32_t startIndex = it.GlyphStartTextElementCharIndex();
 
     mPositions[charIndex].mClusterOrLigatureGroupMiddle = true;
 
     // Don't allow different rotations on ligature parts.
@@ -4887,17 +4900,18 @@ SVGTextFrame::GetStartOffset(nsIFrame* a
   return length->GetAnimValue(tp) * GetOffsetScale(aTextPathFrame);
 }
 
 void
 SVGTextFrame::DoTextPathLayout()
 {
   nsPresContext* context = PresContext();
 
-  CharIterator it(this, CharIterator::eClusterAndLigatureGroupStart);
+  CharIterator it(this, CharIterator::eClusterAndLigatureGroupStart,
+                  /* aSubtree */ nullptr);
   while (!it.AtEnd()) {
     nsIFrame* textPathFrame = it.TextPathFrame();
     if (!textPathFrame) {
       // Skip past this frame if we're not in a text path.
       it.AdvancePastCurrentFrame();
       continue;
     }
 
@@ -4957,17 +4971,17 @@ SVGTextFrame::DoTextPathLayout()
   }
 }
 
 void
 SVGTextFrame::DoAnchoring()
 {
   nsPresContext* presContext = PresContext();
 
-  CharIterator it(this, CharIterator::eOriginal);
+  CharIterator it(this, CharIterator::eOriginal, /* aSubtree */ nullptr);
 
   // Don't need to worry about skipped or trimmed characters.
   while (!it.AtEnd() &&
          (it.IsOriginalCharSkipped() || it.IsOriginalCharTrimmed())) {
     it.Next();
   }
 
   bool vertical = GetWritingMode().IsVertical();
@@ -5229,16 +5243,30 @@ SVGTextFrame::UpdateGlyphPositioning()
   }
 
   if (mState & NS_STATE_SVG_POSITIONING_DIRTY) {
     DoGlyphPositioning();
   }
 }
 
 void
+SVGTextFrame::MaybeResolveBidiForAnonymousBlockChild()
+{
+  nsIFrame* kid = PrincipalChildList().FirstChild();
+
+  if (kid &&
+      kid->GetStateBits() & NS_BLOCK_NEEDS_BIDI_RESOLUTION &&
+      PresContext()->BidiEnabled()) {
+    MOZ_ASSERT(static_cast<nsBlockFrame*>(do_QueryFrame(kid)),
+               "Expect anonymous child to be an nsBlockFrame");
+    nsBidiPresUtils::Resolve(static_cast<nsBlockFrame*>(kid));
+  }
+}
+
+void
 SVGTextFrame::MaybeReflowAnonymousBlockChild()
 {
   nsIFrame* kid = PrincipalChildList().FirstChild();
   if (!kid)
     return;
 
   NS_ASSERTION(!(kid->GetStateBits() & NS_FRAME_IN_REFLOW),
                "should not be in reflow when about to reflow again");
--- a/layout/svg/SVGTextFrame.h
+++ b/layout/svg/SVGTextFrame.h
@@ -385,16 +385,21 @@ private:
     {
       mFrame->GetContent()->RemoveMutationObserver(this);
     }
 
     SVGTextFrame* const mFrame;
   };
 
   /**
+   * Resolves Bidi for the anonymous block child if it needs it.
+   */
+  void MaybeResolveBidiForAnonymousBlockChild();
+
+  /**
    * Reflows the anonymous block child if it is dirty or has dirty
    * children, or if the SVGTextFrame itself is dirty.
    */
   void MaybeReflowAnonymousBlockChild();
 
   /**
    * Performs the actual work of reflowing the anonymous block child.
    */
--- a/mobile/android/app/mobile.js
+++ b/mobile/android/app/mobile.js
@@ -225,16 +225,18 @@ pref("extensions.getAddons.getWithPerfor
 
 /* preference for the locale picker */
 pref("extensions.getLocales.get.url", "");
 pref("extensions.compatability.locales.buildid", "0");
 
 /* Don't let XPIProvider install distribution add-ons; we do our own thing on mobile. */
 pref("extensions.installDistroAddons", false);
 
+pref("extensions.webextPermissionPrompts", true);
+
 // Add-on content security policies.
 pref("extensions.webextensions.base-content-security-policy", "script-src 'self' https://* moz-extension: blob: filesystem: 'unsafe-eval' 'unsafe-inline'; object-src 'self' https://* moz-extension: blob: filesystem:;");
 pref("extensions.webextensions.default-content-security-policy", "script-src 'self'; object-src 'self';");
 
 /* block popups by default, and notify the user about blocked popups */
 pref("dom.disable_open_during_load", true);
 pref("privacy.popups.showBrowserMessage", true);
 
new file mode 100644
--- /dev/null
+++ b/mobile/android/app/src/main/res/drawable/ic_extension.xml
@@ -0,0 +1,4 @@
+<vector android:height="24dp" android:viewportHeight="64.0"
+    android:viewportWidth="64.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="#FF66CC52" android:pathData="M42,62c2.2,0 4,-1.8 4,-4l0,-14.2c0,0 0.4,-3.7 2.8,-3.7c2.4,0 2.2,3.9 6.7,3.9c2.3,0 6.2,-1.2 6.2,-8.2c0,-7 -3.9,-7.9 -6.2,-7.9c-4.5,0 -4.3,3.7 -6.7,3.7c-2.4,0 -2.8,-3.8 -2.8,-3.8V22c0,-2.2 -1.8,-4 -4,-4H31.5c0,0 -3.4,-0.6 -3.4,-3c0,-2.4 3.8,-2.6 3.8,-7.1c0,-2.3 -1.3,-5.9 -8.3,-5.9s-8,3.6 -8,5.9c0,4.5 3.4,4.7 3.4,7.1c0,2.4 -3.4,3 -3.4,3H6c-2.2,0 -4,1.8 -4,4l0,7.8c0,0 -0.4,6 4.4,6c3.1,0 3.2,-4.1 7.3,-4.1c2,0 4,1.9 4,6c0,4.2 -2,6.3 -4,6.3c-4,0 -4.2,-4.1 -7.3,-4.1c-4.8,0 -4.4,5.8 -4.4,5.8L2,58c0,2.2 1.8,4 4,4H19c0,0 6.3,0.4 6.3,-4.4c0,-3.1 -4,-3.6 -4,-7.7c0,-2 2.2,-4.5 6.4,-4.5c4.2,0 6.6,2.5 6.6,4.5c0,4 -3.9,4.6 -3.9,7.7c0,4.9 6.3,4.4 6.3,4.4H42z"/>
+</vector>
new file mode 100644
--- /dev/null
+++ b/mobile/android/app/src/main/res/layout/extension_permissions_dialog.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              android:layout_width="match_parent"
+              android:layout_height="wrap_content"
+              android:orientation="vertical"
+              android:paddingLeft="@dimen/doorhanger_section_padding_small"
+              android:paddingRight="@dimen/doorhanger_section_padding_small"
+              android:paddingTop="@dimen/doorhanger_section_padding_medium"
+              android:paddingBottom="@dimen/doorhanger_section_padding_medium">
+
+  <TextView android:id="@+id/extension_permission_header"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:gravity="center_vertical"
+            android:padding="@dimen/doorhanger_section_padding_small"
+            android:drawablePadding="@dimen/doorhanger_section_padding_small"/>
+
+  <TextView android:id="@+id/extension_permission_body"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"/>
+</LinearLayout>
+
--- a/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
@@ -84,16 +84,17 @@ import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.db.SuggestedSites;
 import org.mozilla.gecko.delegates.BookmarkStateChangeDelegate;
 import org.mozilla.gecko.delegates.BrowserAppDelegate;
 import org.mozilla.gecko.delegates.OfflineTabStatusDelegate;
 import org.mozilla.gecko.delegates.ScreenshotDelegate;
 import org.mozilla.gecko.distribution.Distribution;
 import org.mozilla.gecko.distribution.DistributionStoreCallback;
 import org.mozilla.gecko.dlc.DownloadContentService;
+import org.mozilla.gecko.extensions.ExtensionPermissionsHelper;
 import org.mozilla.gecko.firstrun.FirstrunAnimationContainer;
 import org.mozilla.gecko.gfx.DynamicToolbarAnimator;
 import org.mozilla.gecko.gfx.DynamicToolbarAnimator.PinReason;
 import org.mozilla.gecko.gfx.ImmutableViewportMetrics;
 import org.mozilla.gecko.home.BrowserSearch;
 import org.mozilla.gecko.home.HomeBanner;
 import org.mozilla.gecko.home.HomeConfig;
 import org.mozilla.gecko.home.HomeConfig.PanelType;
@@ -310,16 +311,18 @@ public class BrowserApp extends GeckoApp
     private int mToolbarHeight;
 
     private SharedPreferencesHelper mSharedPreferencesHelper;
 
     private ReadingListHelper mReadingListHelper;
 
     private AccountsHelper mAccountsHelper;
 
+    private ExtensionPermissionsHelper mExtensionPermissionsHelper;
+
     // The tab to be selected on editing mode exit.
     private Integer mTargetTabForEditingMode;
 
     private final TabEditingState mLastTabEditingState = new TabEditingState();
 
     // The animator used to toggle HomePager visibility has a race where if the HomePager is shown
     // (starting the animation), the HomePager is hidden, and the HomePager animation completes,
     // both the web content and the HomePager will be hidden. This flag is used to prevent the
@@ -813,16 +816,17 @@ public class BrowserApp extends GeckoApp
         // Init suggested sites engine in BrowserDB.
         final SuggestedSites suggestedSites = new SuggestedSites(appContext, distribution);
         final BrowserDB db = BrowserDB.from(profile);
         db.setSuggestedSites(suggestedSites);
 
         mSharedPreferencesHelper = new SharedPreferencesHelper(appContext);
         mReadingListHelper = new ReadingListHelper(appContext, profile);
         mAccountsHelper = new AccountsHelper(appContext, profile);
+        mExtensionPermissionsHelper = new ExtensionPermissionsHelper(this);
 
         if (AppConstants.MOZ_ANDROID_BEAM) {
             NfcAdapter nfc = NfcAdapter.getDefaultAdapter(this);
             if (nfc != null) {
                 nfc.setNdefPushMessageCallback(new NfcAdapter.CreateNdefMessageCallback() {
                     @Override
                     public NdefMessage createNdefMessage(NfcEvent event) {
                         Tab tab = Tabs.getInstance().getSelectedTab();
@@ -1531,16 +1535,21 @@ public class BrowserApp extends GeckoApp
             mReadingListHelper = null;
         }
 
         if (mAccountsHelper != null) {
             mAccountsHelper.uninit();
             mAccountsHelper = null;
         }
 
+        if (mExtensionPermissionsHelper != null) {
+            mExtensionPermissionsHelper.uninit();
+            mExtensionPermissionsHelper = null;
+        }
+
         mSearchEngineManager.unregisterListeners();
 
         EventDispatcher.getInstance().unregisterGeckoThreadListener(this,
             "Search:Keyword",
             null);
 
         EventDispatcher.getInstance().unregisterUiThreadListener(this,
             "Accessibility:Enabled",
--- a/mobile/android/base/java/org/mozilla/gecko/customtabs/CustomTabsActivity.java
+++ b/mobile/android/base/java/org/mozilla/gecko/customtabs/CustomTabsActivity.java
@@ -539,25 +539,24 @@ public class CustomTabsActivity extends 
     public void onCanGoForward(GeckoView view, boolean canGoForward) {
         mCanGoForward = canGoForward;
         updateMenuItemForward();
     }
 
     @Override
     public boolean onLoadUri(final GeckoView view, final String uriStr,
                              final TargetWindow where) {
+        if (where != TargetWindow.NEW) {
+            return false;
+        }
+
         final Uri uri = Uri.parse(uriStr);
-        if (!TextUtils.isEmpty(mCurrentUrl) &&
-            Uri.parse(mCurrentUrl).getHost().equals(uri.getHost())) {
-            view.loadUri(uri);
-        } else {
-            final Intent intent = new Intent(Intent.ACTION_VIEW);
-            intent.setData(uri);
-            startActivity(intent);
-        }
+        final Intent intent = new Intent(Intent.ACTION_VIEW);
+        intent.setData(uri);
+        startActivity(intent);
         return true;
     }
 
     /* GeckoView.ProgressListener */
     @Override
     public void onPageStart(GeckoView view, String url) {
         mCurrentUrl = url;
         mCanStop = true;
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/extensions/ExtensionPermissionsHelper.java
@@ -0,0 +1,85 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * 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/. */
+
+package org.mozilla.gecko.extensions;
+
+import org.mozilla.gecko.EventDispatcher;
+import org.mozilla.gecko.util.BundleEventListener;
+import org.mozilla.gecko.util.EventCallback;
+import org.mozilla.gecko.util.GeckoBundle;
+import org.mozilla.gecko.util.ResourceDrawableUtils;
+import org.mozilla.gecko.R;
+
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+public class ExtensionPermissionsHelper implements BundleEventListener {
+    private final Context mContext;
+
+    public ExtensionPermissionsHelper(Context context) {
+        mContext = context;
+
+        EventDispatcher.getInstance().registerUiThreadListener(this,
+            "Extension:PermissionPrompt");
+    }
+
+    public void uninit() {
+        EventDispatcher.getInstance().unregisterUiThreadListener(this,
+            "Extension:PermissionPrompt");
+    }
+
+    @Override // BundleEventListener
+    public void handleMessage(final String event, final GeckoBundle message,
+                              final EventCallback callback) {
+        if ("Extension:PermissionPrompt".equals(event)) {
+            final AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
+
+            final View view = LayoutInflater.from(mContext)
+                .inflate(R.layout.extension_permissions_dialog, null);
+            builder.setView(view);
+
+            final TextView headerText = (TextView) view.findViewById(R.id.extension_permission_header);
+            headerText.setText(message.getString("header"));
+
+            final TextView bodyText = (TextView) view.findViewById(R.id.extension_permission_body);
+            bodyText.setText(message.getString("message"));
+
+            builder.setPositiveButton(message.getString("acceptText"), new DialogInterface.OnClickListener() {
+                @Override
+                public void onClick(DialogInterface dialog, int whichButton) {
+                    callback.sendSuccess(true);
+                }
+            });
+            builder.setNegativeButton(message.getString("cancelText"), new DialogInterface.OnClickListener() {
+                @Override
+                public void onClick(DialogInterface dialog, int whichButton) {
+                    callback.sendSuccess(false);
+                }
+            });
+
+            final String iconUrl = message.getString("icon");
+            if ("DEFAULT".equals(iconUrl)) {
+                headerText.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_extension, 0, 0, 0);
+            } else {
+                ResourceDrawableUtils.getDrawable(mContext, iconUrl, new ResourceDrawableUtils.BitmapLoader() {
+                        @Override
+                        public void onBitmapFound(final Drawable d) {
+                            headerText.setCompoundDrawablesWithIntrinsicBounds(d, null, null, null);
+                        }
+                    });
+            }
+
+            final AlertDialog dialog = builder.create();
+            dialog.show();
+        }
+    }
+}
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -623,16 +623,17 @@ gbjar.sources += ['java/org/mozilla/geck
     'dlc/VerifyAction.java',
     'DoorHangerPopup.java',
     'DownloadsIntegration.java',
     'drawable/DrawableWrapper.java',
     'drawable/ShiftDrawable.java',
     'DynamicToolbar.java',
     'EditBookmarkDialog.java',
     'Experiments.java',
+    'extensions/ExtensionPermissionsHelper.java',
     'FilePicker.java',
     'FilePickerResultHandler.java',
     'FindInPageBar.java',
     'firstrun/DataPanel.java',
     'firstrun/FirstrunAnimationContainer.java',
     'firstrun/FirstrunPager.java',
     'firstrun/FirstrunPagerConfig.java',
     'firstrun/FirstrunPanel.java',
new file mode 100644
--- /dev/null
+++ b/mobile/android/chrome/content/ExtensionPermissions.js
@@ -0,0 +1,68 @@
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionData",
+                                  "resource://gre/modules/Extension.jsm");
+
+var ExtensionPermissions = {
+  // Prepare the strings needed for a permission notification.
+  _prepareStrings(info) {
+    let appName = Strings.brand.GetStringFromName("brandShortName");
+    let info2 = Object.assign({appName, addonName: info.addon.name}, info);
+    let strings = ExtensionData.formatPermissionStrings(info2, Strings.browser);
+
+    // We dump the main body of the dialog into a big android
+    // TextView.  Build a big string with the full contents here.
+    let message = "";
+    if (strings.msgs.length > 0) {
+      message = [strings.listIntro, ...strings.msgs.map(s => `\u2022 ${s}`)].join("\n");
+    }
+
+    return {
+      header: strings.header || strings.text,
+      message,
+      acceptText: strings.acceptText,
+      cancelText: strings.cancelText,
+    };
+  },
+
+  // Prepare an icon for a permission notification
+  _prepareIcon(iconURL) {
+    // We can render pngs with ResourceDrawableUtils
+    if (iconURL.endsWith(".png")) {
+      return iconURL;
+    }
+
+    // If we can't render an icon, show the default
+    return "DEFAULT";
+  },
+
+  async observe(subject, topic, data) {
+    switch (topic) {
+      case "webextension-permission-prompt": {
+        let {target, info} = subject.wrappedJSObject;
+
+        let details = this._prepareStrings(info);
+        details.icon = this._prepareIcon(info.icon);
+        details.type = "Extension:PermissionPrompt";
+        let accepted = await EventDispatcher.instance.sendRequestForResult(details);
+
+        if (accepted) {
+          info.resolve();
+        } else {
+          info.reject();
+        }
+        break;
+      }
+
+      case "webextension-update-permissions":
+        // To be implemented in bug 1391579, just auto-approve until then
+        subject.wrappedJSObject.resolve();
+        break;
+
+      case "webextension-optional-permission-prompt":
+        // To be implemented in bug 1392176, just auto-approve until then
+        subject.wrappedJSObject.resolve(true);
+        break;
+    }
+  },
+};
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -146,16 +146,20 @@ lazilyLoadedBrowserScripts.forEach(funct
     Services.scriptloader.loadSubScript(script, sandbox);
     return sandbox[name];
   });
 });
 
 var lazilyLoadedObserverScripts = [
   ["MemoryObserver", ["memory-pressure", "Memory:Dump"], "chrome://browser/content/MemoryObserver.js"],
   ["ConsoleAPI", ["console-api-log-event"], "chrome://browser/content/ConsoleAPI.js"],
+  ["ExtensionPermissions", ["webextension-permission-prompt",
+                            "webextension-update-permissions",
+                            "webextension-optional-permission-prompt"],
+   "chrome://browser/content/ExtensionPermissions.js"],
 ];
 
 lazilyLoadedObserverScripts.forEach(function (aScript) {
   let [name, notifications, script] = aScript;
   XPCOMUtils.defineLazyGetter(window, name, function() {
     let sandbox = {};
     Services.scriptloader.loadSubScript(script, sandbox);
     return sandbox[name];
--- a/mobile/android/chrome/jar.mn
+++ b/mobile/android/chrome/jar.mn
@@ -55,16 +55,17 @@ chrome.jar:
 #endif
   content/aboutAccounts.xhtml          (content/aboutAccounts.xhtml)
   content/aboutAccounts.js             (content/aboutAccounts.js)
   content/aboutLogins.xhtml            (content/aboutLogins.xhtml)
   content/aboutLogins.js               (content/aboutLogins.js)
 #ifndef RELEASE_OR_BETA
   content/WebcompatReporter.js         (content/WebcompatReporter.js)
 #endif
+  content/ExtensionPermissions.js      (content/ExtensionPermissions.js)
 
 % content branding %content/branding/
 
 % override chrome://global/content/config.xul chrome://browser/content/config.xhtml
 % override chrome://global/content/netError.xhtml chrome://browser/content/netError.xhtml
 % override chrome://mozapps/content/extensions/extensions.xul chrome://browser/content/aboutAddons.xhtml
 
 # L10n resource overrides.
--- a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoAccessibility.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoAccessibility.java
@@ -3,16 +3,17 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko;
 
 import org.json.JSONException;
 import org.json.JSONObject;
 import org.mozilla.gecko.EventDispatcher;
+import org.mozilla.gecko.gfx.LayerView;
 import org.mozilla.gecko.util.GeckoBundle;
 import org.mozilla.gecko.util.ThreadUtils;
 
 import android.content.Context;
 import android.graphics.Rect;
 import android.os.Build;
 import android.os.Bundle;
 import android.util.Log;
@@ -142,17 +143,17 @@ public class GeckoAccessibility {
                     @Override
                     public void run() {
                         sendDirectAccessibilityEvent(eventType, message);
                 }
             });
         } else {
             // In Jelly Bean we populate an AccessibilityNodeInfo with the minimal amount of data to have
             // it work with TalkBack.
-            final View view = GeckoAppShell.getLayerView();
+            final LayerView view = GeckoAppShell.getLayerView();
             if (view == null)
                 return;
 
             if (sVirtualCursorNode == null) {
                 sVirtualCursorNode = AccessibilityNodeInfo.obtain(view, VIRTUAL_CURSOR_POSITION);
             }
             sVirtualCursorNode.setEnabled(message.getBoolean("enabled", true));
             sVirtualCursorNode.setClickable(message.getBoolean("clickable"));
@@ -173,16 +174,17 @@ public class GeckoAccessibility {
 
             final GeckoBundle bounds = message.getBundle("bounds");
             if (bounds != null) {
                 Rect relativeBounds = new Rect(bounds.getInt("left"), bounds.getInt("top"),
                                                bounds.getInt("right"), bounds.getInt("bottom"));
                 sVirtualCursorNode.setBoundsInParent(relativeBounds);
                 int[] locationOnScreen = new int[2];
                 view.getLocationOnScreen(locationOnScreen);
+                locationOnScreen[1] += view.getCurrentToolbarHeight();
                 Rect screenBounds = new Rect(relativeBounds);
                 screenBounds.offset(locationOnScreen[0], locationOnScreen[1]);
                 sVirtualCursorNode.setBoundsInScreen(screenBounds);
             }
 
             final GeckoBundle braille = message.getBundle("brailleOutput");
             if (braille != null) {
                 sendBrailleText(view, braille.getString("text", ""),
--- a/mobile/android/locales/en-US/chrome/browser.properties
+++ b/mobile/android/locales/en-US/chrome/browser.properties
@@ -104,16 +104,79 @@ blockPopups.label2=Popups
 xpinstallPromptWarning2=%S prevented this site (%S) from asking you to install software on your device.
 xpinstallPromptWarningLocal=%S prevented this add-on (%S) from installing on your device.
 xpinstallPromptWarningDirect=%S prevented an add-on from installing on your device.
 xpinstallPromptAllowButton=Allow
 xpinstallDisabledMessageLocked=Software installation has been disabled by your system administrator.
 xpinstallDisabledMessage2=Software installation is currently disabled. Press Enable and try again.
 xpinstallDisabledButton=Enable
 
+# LOCALIZATION NOTE (webextPerms.header)
+# This string is used as a header in the webextension permissions dialog,
+# %S is replaced with the localized name of the extension being installed.
+# See https://bug1308309.bmoattachments.org/attachment.cgi?id=8814612
+# for an example of the full dialog.
+# Note, this string will be used as raw markup. Avoid characters like <, >, &
+webextPerms.header=Add %S?
+
+# LOCALIZATION NOTE (webextPerms.listIntro)
+# This string will be followed by a list of permissions requested
+# by the webextension.
+webextPerms.listIntro=It requires your permission to:
+webextPerms.add.label=Add
+webextPerms.add.accessKey=A
+webextPerms.cancel.label=Cancel
+webextPerms.cancel.accessKey=C
+
+webextPerms.description.bookmarks=Read and modify bookmarks
+webextPerms.description.browserSettings=Read and modify browser settings
+webextPerms.description.clipboardRead=Get data from the clipboard
+webextPerms.description.clipboardWrite=Input data to the clipboard
+webextPerms.description.downloads=Download files and read and modify the browser’s download history
+webextPerms.description.geolocation=Access your location
+webextPerms.description.history=Access browsing history
+webextPerms.description.management=Monitor extension usage and manage themes
+# LOCALIZATION NOTE (webextPerms.description.nativeMessaging)
+# %S will be replaced with the name of the application
+webextPerms.description.nativeMessaging=Exchange messages with programs other than %S
+webextPerms.description.notifications=Display notifications to you
+webextPerms.description.privacy=Read and modify privacy settings
+webextPerms.description.sessions=Access recently closed tabs
+webextPerms.description.tabs=Access browser tabs
+webextPerms.description.topSites=Access browsing history
+webextPerms.description.unlimitedStorage=Store unlimited amount of client-side data
+webextPerms.description.webNavigation=Access browser activity during navigation
+
+webextPerms.hostDescription.allUrls=Access your data for all websites
+
+# LOCALIZATION NOTE (webextPerms.hostDescription.wildcard)
+# %S will be replaced by the DNS domain for which a webextension
+# is requesting access (e.g., mozilla.org)
+webextPerms.hostDescription.wildcard=Access your data for sites in the %S domain
+
+# LOCALIZATION NOTE (webextPerms.hostDescription.tooManyWildcards):
+# Semi-colon list of plural forms.
+# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
+# #1 will be replaced by an integer indicating the number of additional
+# domains for which this webextension is requesting permission.
+webextPerms.hostDescription.tooManyWildcards=Access your data in #1 other domain;Access your data in #1 other domains
+
+# LOCALIZATION NOTE (webextPerms.hostDescription.oneSite)
+# %S will be replaced by the DNS host name for which a webextension
+# is requesting access (e.g., www.mozilla.org)
+webextPerms.hostDescription.oneSite=Access your data for %S
+
+# LOCALIZATION NOTE (webextPerms.hostDescription.tooManySites)
+# Semi-colon list of plural forms.
+# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
+# #1 will be replaced by an integer indicating the number of additional
+# hosts for which this webextension is requesting permission.
+webextPerms.hostDescription.tooManySites=Access your data on #1 other site;Access your data on #1 other sites
+
+
 # Site Identity
 identity.identified.verifier=Verified by: %S
 identity.identified.verified_by_you=You have added a security exception for this site
 identity.identified.state_and_country=%S, %S
 identity.identified.title_with_country=%S (%S)
 
 # Geolocation UI
 geolocation.allow=Share
--- a/netwerk/protocol/http/HttpChannelChild.cpp
+++ b/netwerk/protocol/http/HttpChannelChild.cpp
@@ -10,16 +10,17 @@
 
 #include "nsHttp.h"
 #include "nsICacheEntry.h"
 #include "mozilla/Unused.h"
 #include "mozilla/dom/ContentChild.h"
 #include "mozilla/dom/DocGroup.h"
 #include "mozilla/dom/TabChild.h"
 #include "mozilla/dom/TabGroup.h"
+#include "mozilla/extensions/StreamFilterParent.h"
 #include "mozilla/ipc/FileDescriptorSetChild.h"
 #include "mozilla/ipc/IPCStreamUtils.h"
 #include "mozilla/net/NeckoChild.h"
 #include "mozilla/net/HttpChannelChild.h"
 
 #include "AltDataOutputStreamChild.h"
 #include "CookieServiceChild.h"
 #include "HttpBackgroundChannelChild.h"
@@ -3629,16 +3630,23 @@ HttpChannelChild::ShouldInterceptURI(nsI
 
 mozilla::ipc::IPCResult
 HttpChannelChild::RecvSetPriority(const int16_t& aPriority)
 {
   mPriority = aPriority;
   return IPC_OK();
 }
 
+mozilla::ipc::IPCResult
+HttpChannelChild::RecvAttachStreamFilter(Endpoint<extensions::PStreamFilterParent>&& aEndpoint)
+{
+  extensions::StreamFilterParent::Attach(this, Move(aEndpoint));
+  return IPC_OK();
+}
+
 void
 HttpChannelChild::ActorDestroy(ActorDestroyReason aWhy)
 {
   MOZ_ASSERT(NS_IsMainThread());
 
   // OnStartRequest might be dropped if IPDL is destroyed abnormally
   // and BackgroundChild might have pending IPC messages.
   // Clean up BackgroundChild at this time to prevent memleak.
--- a/netwerk/protocol/http/HttpChannelChild.h
+++ b/netwerk/protocol/http/HttpChannelChild.h
@@ -159,16 +159,18 @@ protected:
   mozilla::ipc::IPCResult RecvReportSecurityMessage(const nsString& messageTag,
                                                     const nsString& messageCategory) override;
 
   mozilla::ipc::IPCResult RecvIssueDeprecationWarning(const uint32_t& warning,
                                                       const bool& asError) override;
 
   mozilla::ipc::IPCResult RecvSetPriority(const int16_t& aPriority) override;
 
+  mozilla::ipc::IPCResult RecvAttachStreamFilter(Endpoint<extensions::PStreamFilterParent>&& aEndpoint) override;
+
   virtual void ActorDestroy(ActorDestroyReason aWhy) override;
 
   MOZ_MUST_USE bool
   GetAssociatedContentSecurity(nsIAssociatedContentSecurity** res = nullptr);
   virtual void DoNotifyListenerCleanup() override;
 
   NS_IMETHOD GetResponseSynthesized(bool* aSynthesized) override;
 
--- a/netwerk/protocol/http/PHttpChannel.ipdl
+++ b/netwerk/protocol/http/PHttpChannel.ipdl
@@ -1,16 +1,17 @@
 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set sw=2 ts=8 et tw=80 ft=cpp : */
 
 /* 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 protocol PNecko;
+include protocol PStreamFilter;
 include InputStreamParams;
 include URIParams;
 include PBackgroundSharedTypes;
 include NeckoChannelParams;
 
 include "mozilla/net/NeckoMessageUtils.h";
 
 using class nsHttpHeaderArray from "nsHttpHeaderArray.h";
@@ -141,16 +142,18 @@ child:
   // Tell the child to issue a deprecation warning.
   async IssueDeprecationWarning(uint32_t warning, bool asError);
 
   // When CORS blocks the request in the parent process, it doesn't have the
   // correct window ID, so send the message to the child for logging to the web
   // console.
   async LogBlockedCORSRequest(nsString message);
 
+  async AttachStreamFilter(Endpoint<PStreamFilterParent> aEndpoint);
+
 both:
   // After receiving this message, the parent also calls
   // SendFinishInterceptedRedirect, and makes sure not to send any more messages
   // after that. When receiving this message, the child will call
   // Send__delete__() and complete the steps required to finish the redirect.
   async FinishInterceptedRedirect();
 
   async SetPriority(int16_t priority);
--- a/netwerk/protocol/http/nsHttpChannel.cpp
+++ b/netwerk/protocol/http/nsHttpChannel.cpp
@@ -94,16 +94,17 @@
 #include "ScopedNSSTypes.h"
 #include "NullPrincipal.h"
 #include "nsIDeprecationWarner.h"
 #include "nsIDocument.h"
 #include "nsIDOMDocument.h"
 #include "nsICompressConvStats.h"
 #include "nsCORSListenerProxy.h"
 #include "nsISocketProvider.h"
+#include "mozilla/extensions/StreamFilterParent.h"
 #include "mozilla/net/Predictor.h"
 #include "mozilla/MathAlgorithms.h"
 #include "CacheControlParser.h"
 #include "nsMixedContentBlocker.h"
 #include "HSTSPrimerListener.h"
 #include "CacheStorageService.h"
 #include "HttpChannelParent.h"
 #include "nsIBufferedStreams.h"
@@ -6709,16 +6710,43 @@ nsHttpChannel::SetChannelIsForDownload(b
     AddClassFlags(nsIClassOfService::Throttleable);
   } else {
     ClearClassFlags(nsIClassOfService::Throttleable);
   }
 
   return HttpBaseChannel::SetChannelIsForDownload(aChannelIsForDownload);
 }
 
+base::ProcessId
+nsHttpChannel::ProcessId()
+{
+  nsCOMPtr<nsIParentChannel> parentChannel;
+  NS_QueryNotificationCallbacks(this, parentChannel);
+  RefPtr<HttpChannelParent> httpParent = do_QueryObject(parentChannel);
+  if (httpParent) {
+    return httpParent->OtherPid();
+  }
+  return base::GetCurrentProcId();
+}
+
+bool
+nsHttpChannel::AttachStreamFilter(ipc::Endpoint<extensions::PStreamFilterParent>&& aEndpoint)
+
+{
+  nsCOMPtr<nsIParentChannel> parentChannel;
+  NS_QueryNotificationCallbacks(this, parentChannel);
+  RefPtr<HttpChannelParent> httpParent = do_QueryObject(parentChannel);
+  if (httpParent) {
+    return httpParent->SendAttachStreamFilter(Move(aEndpoint));
+  }
+
+  extensions::StreamFilterParent::Attach(this, Move(aEndpoint));
+  return true;
+}
+
 //-----------------------------------------------------------------------------
 // nsHttpChannel::nsISupportsPriority
 //-----------------------------------------------------------------------------
 
 NS_IMETHODIMP
 nsHttpChannel::SetPriority(int32_t value)
 {
     int16_t newValue = clamped<int32_t>(value, INT16_MIN, INT16_MAX);
--- a/netwerk/protocol/http/nsHttpChannel.h
+++ b/netwerk/protocol/http/nsHttpChannel.h
@@ -26,16 +26,17 @@
 #include "ADivertableParentChannel.h"
 #include "AutoClose.h"
 #include "nsIStreamListener.h"
 #include "nsISupportsPrimitives.h"
 #include "nsICorsPreflightCallback.h"
 #include "AlternateServices.h"
 #include "nsIHstsPrimingCallback.h"
 #include "nsIRaceCacheWithNetwork.h"
+#include "mozilla/extensions/PStreamFilterParent.h"
 #include "mozilla/Mutex.h"
 
 class nsDNSPrefetch;
 class nsICancelable;
 class nsIHttpChannelAuthProvider;
 class nsInputStreamPump;
 class nsISSLStatus;
 
@@ -288,16 +289,20 @@ public: /* internal necko use only */
     NS_IMETHOD GetResponseSynthesized(bool* aSynthesized) override;
     bool AwaitingCacheCallbacks();
     void SetCouldBeSynthesized();
 
     // Return true if the latest ODA is invoked by mCachePump.
     // Should only be called on the same thread as ODA.
     bool IsReadingFromCache() const { return mIsReadingFromCache; }
 
+    base::ProcessId ProcessId();
+
+    MOZ_MUST_USE bool AttachStreamFilter(ipc::Endpoint<extensions::PStreamFilterParent>&& aEndpoint);
+
 private: // used for alternate service validation
     RefPtr<TransactionObserver> mTransactionObserver;
 public:
     void SetConnectionInfo(nsHttpConnectionInfo *); // clones the argument
     void SetTransactionObserver(TransactionObserver *arg) { mTransactionObserver = arg; }
     TransactionObserver *GetTransactionObserver() { return mTransactionObserver; }
 
 protected:
--- a/testing/mochitest/runrobocop.py
+++ b/testing/mochitest/runrobocop.py
@@ -221,17 +221,16 @@ class RobocopTestRunner(MochitestDesktop
 
            This is similar to buildProfile in runtestsremote.py.
         """
         self.options.extraPrefs.append('browser.search.suggest.enabled=true')
         self.options.extraPrefs.append('browser.search.suggest.prompted=true')
         self.options.extraPrefs.append('layout.css.devPixelsPerPx=1.0')
         self.options.extraPrefs.append('browser.chrome.dynamictoolbar=false')
         self.options.extraPrefs.append('browser.snippets.enabled=false')
-        self.options.extraPrefs.append('browser.casting.enabled=true')
         self.options.extraPrefs.append('extensions.autoupdate.enabled=false')
 
         # Override the telemetry init delay for integration testing.
         self.options.extraPrefs.append('toolkit.telemetry.initDelay=1')
 
         self.options.extensionsToExclude.extend([
             'mochikit@mozilla.org',
             'worker-test@mozilla.org.xpi',
new file mode 100644
--- /dev/null
+++ b/testing/mozharness/configs/openh264/android-aarch64.py
@@ -0,0 +1,46 @@
+import os
+
+import mozharness
+
+external_tools_path = os.path.join(
+    os.path.abspath(os.path.dirname(os.path.dirname(mozharness.__file__))),
+    'external_tools',
+)
+
+config = {
+    'default_actions': [
+        'get-tooltool',
+        'checkout-sources',
+        'build',
+        # 'test',  # can't run android tests on linux hosts
+        'package',
+        'dump-symbols',
+        'upload',
+    ],
+
+    'tooltool_manifest_file': "android.manifest",
+    'tooltool_cache': "/builds/tooltool_cache",
+    'exes': {
+        'gittool.py': [os.path.join(external_tools_path, 'gittool.py')],
+        'tooltool.py': "/builds/tooltool.py",
+        'python2.7': "/tools/python27/bin/python2.7",
+    },
+    'dump_syms_binary': 'dump_syms',
+    'arch': 'aarch64',
+    # https://dxr.mozilla.org/mozilla-central/rev/5322c03f4c8587fe526172d3f87160031faa6d75/mobile/android/config/mozconfigs/android-aarch64/nightly#6
+    'min_sdk': 21,
+    'use_mock': True,
+    'mock_target': 'mozilla-centos6-x86_64',
+    'mock_packages': ['make', 'git', 'nasm', 'glibc-devel.i686',
+                      'libstdc++-devel.i686', 'zip', 'yasm',
+                      'mozilla-python27'],
+    'mock_files': [
+        ('/home/cltbld/.ssh', '/home/mock_mozilla/.ssh'),
+        ('/builds/relengapi.tok', '/builds/relengapi.tok'),
+        ('/tools/tooltool.py', '/builds/tooltool.py'),
+    ],
+    'operating_system': 'android',
+    'partial_env': {
+        'PATH': '%(abs_work_dir)s/android-sdk-linux/tools:%(PATH)s',
+    },
+}
new file mode 100644
--- /dev/null
+++ b/testing/mozharness/configs/openh264/android-arm.py
@@ -0,0 +1,46 @@
+import os
+
+import mozharness
+
+external_tools_path = os.path.join(
+    os.path.abspath(os.path.dirname(os.path.dirname(mozharness.__file__))),
+    'external_tools',
+)
+
+config = {
+    'default_actions': [
+        'get-tooltool',
+        'checkout-sources',
+        'build',
+        # 'test',  # can't run android tests on linux hosts
+        'package',
+        'dump-symbols',
+        'upload',
+    ],
+
+    'tooltool_manifest_file': "android.manifest",
+    'tooltool_cache': "/builds/tooltool_cache",
+    'exes': {
+        'gittool.py': [os.path.join(external_tools_path, 'gittool.py')],
+        'tooltool.py': "/builds/tooltool.py",
+        'python2.7': "/tools/python27/bin/python2.7",
+    },
+    'dump_syms_binary': 'dump_syms',
+    'arch': 'arm',
+    # https://dxr.mozilla.org/mozilla-central/rev/5322c03f4c8587fe526172d3f87160031faa6d75/mobile/android/config/mozconfigs/android-api-15/nightly#6
+    'min_sdk': 16,
+    'use_mock': True,
+    'mock_target': 'mozilla-centos6-x86_64',
+    'mock_packages': ['make', 'git', 'nasm', 'glibc-devel.i686',
+                      'libstdc++-devel.i686', 'zip', 'yasm',
+                      'mozilla-python27'],
+    'mock_files': [
+        ('/home/cltbld/.ssh', '/home/mock_mozilla/.ssh'),
+        ('/builds/relengapi.tok', '/builds/relengapi.tok'),
+        ('/tools/tooltool.py', '/builds/tooltool.py'),
+    ],
+    'operating_system': 'android',
+    'partial_env': {
+        'PATH': '%(abs_work_dir)s/android-sdk-linux/tools:%(PATH)s',
+    },
+}
new file mode 100644
--- /dev/null
+++ b/testing/mozharness/configs/openh264/android-x86.py
@@ -0,0 +1,47 @@
+import os
+
+import mozharness
+
+external_tools_path = os.path.join(
+    os.path.abspath(os.path.dirname(os.path.dirname(mozharness.__file__))),
+    'external_tools',
+)
+
+config = {
+    'default_actions': [
+        'get-tooltool',
+        'checkout-sources',
+        'build',
+        # 'test',  # can't run android tests on linux hosts
+        'package',
+        'dump-symbols',
+        'upload',
+    ],
+
+    'tooltool_manifest_file': "android.manifest",
+    'tooltool_cache': "/builds/tooltool_cache",
+    'exes': {
+        'gittool.py': [os.path.join(external_tools_path, 'gittool.py')],
+        'tooltool.py': "/builds/tooltool.py",
+        'python2.7': "/tools/python27/bin/python2.7",
+    },
+    'avoid_avx2': True,
+    'dump_syms_binary': 'dump_syms',
+    'arch': 'x86',
+    # https://dxr.mozilla.org/mozilla-central/rev/5322c03f4c8587fe526172d3f87160031faa6d75/mobile/android/config/mozconfigs/android-x86/nightly#4
+    'min_sdk': 16,
+    'use_mock': True,
+    'mock_target': 'mozilla-centos6-x86_64',
+    'mock_packages': ['make', 'git', 'nasm', 'glibc-devel.i686',
+                      'libstdc++-devel.i686', 'zip', 'yasm',
+                      'mozilla-python27'],
+    'mock_files': [
+        ('/home/cltbld/.ssh', '/home/mock_mozilla/.ssh'),
+        ('/builds/relengapi.tok', '/builds/relengapi.tok'),
+        ('/tools/tooltool.py', '/builds/tooltool.py'),
+    ],
+    'operating_system': 'android',
+    'partial_env': {
+        'PATH': '%(abs_work_dir)s/android-sdk-linux/tools:%(PATH)s',
+    },
+}
new file mode 100644
--- /dev/null
+++ b/testing/mozharness/configs/openh264/linux32.py
@@ -0,0 +1,32 @@
+import os
+
+import mozharness
+
+external_tools_path = os.path.join(
+    os.path.abspath(os.path.dirname(os.path.dirname(mozharness.__file__))),
+    'external_tools',
+)
+
+config = {
+    'tooltool_manifest_file': "linux.manifest",
+    'tooltool_cache': "/builds/tooltool_cache",
+    'exes': {
+        'gittool.py': [os.path.join(external_tools_path, 'gittool.py')],
+        'tooltool.py': "/builds/tooltool.py",
+        'python2.7': "/tools/python27/bin/python2.7",
+    },
+    'dump_syms_binary': 'dump_syms',
+    'arch': 'x86',
+    'use_mock': True,
+    'avoid_avx2': True,
+    'mock_target': 'mozilla-centos6-x86_64',
+    'mock_packages': ['make', 'git', 'nasm', 'glibc-devel.i686',
+                      'libstdc++-devel.i686', 'zip', 'yasm',
+                      'mozilla-python27'],
+    'mock_files': [
+        ('/home/cltbld/.ssh', '/home/mock_mozilla/.ssh'),
+        ('/builds/relengapi.tok', '/builds/relengapi.tok'),
+        ('/tools/tooltool.py', '/builds/tooltool.py'),
+    ],
+    'operating_system': 'linux',
+}
new file mode 100644
--- /dev/null
+++ b/testing/mozharness/configs/openh264/linux64.py
@@ -0,0 +1,32 @@
+import os
+
+import mozharness
+
+external_tools_path = os.path.join(
+    os.path.abspath(os.path.dirname(os.path.dirname(mozharness.__file__))),
+    'external_tools',
+)
+
+config = {
+    'tooltool_manifest_file': "linux.manifest",
+    'tooltool_cache': "/builds/tooltool_cache",
+    'exes': {
+        'gittool.py': [os.path.join(external_tools_path, 'gittool.py')],
+        'tooltool.py': "/builds/tooltool.py",
+        'python2.7': "/tools/python27/bin/python2.7",
+    },
+    'dump_syms_binary': 'dump_syms',
+    'arch': 'x64',
+    'use_mock': True,
+    'avoid_avx2': True,
+    'mock_target': 'mozilla-centos6-x86_64',
+    'mock_packages': ['make', 'git', 'nasm', 'glibc-devel.i686',
+                      'libstdc++-devel.i686', 'zip', 'yasm',
+                      'mozilla-python27'],
+    'mock_files': [
+        ('/home/cltbld/.ssh', '/home/mock_mozilla/.ssh'),
+        ('/builds/relengapi.tok', '/builds/relengapi.tok'),
+        ('/tools/tooltool.py', '/builds/tooltool.py'),
+    ],
+    'operating_system': 'linux',
+}
new file mode 100644
--- /dev/null
+++ b/testing/mozharness/configs/openh264/macosx64.py
@@ -0,0 +1,21 @@
+import os
+
+import mozharness
+
+external_tools_path = os.path.join(
+    os.path.abspath(os.path.dirname(os.path.dirname(mozharness.__file__))),
+    'external_tools',
+)
+
+config = {
+    'tooltool_manifest_file': "osx.manifest",
+    'tooltool_cache': "/builds/tooltool_cache",
+    'exes': {
+        'gittool.py': [os.path.join(external_tools_path, 'gittool.py')],
+        'tooltool.py': "/builds/tooltool.py",
+        'python2.7': "/tools/python27/bin/python2.7",
+    },
+    'dump_syms_binary': 'dump_syms',
+    'arch': 'x64',
+    'use_yasm': True,
+}
new file mode 100644
--- /dev/null
+++ b/testing/mozharness/configs/openh264/tooltool-manifests/android.manifest
@@ -0,0 +1,26 @@
+[
+  {
+    "version": "Android NDK r11c full",
+    "size": 434664300,
+    "visibility": "internal",
+    "digest": "c59a8fe59f52324ccce5ca6563e8e0189c3fb5538b2a435249581eb994bfb89a70b28e366bab102c36a1e11ee50cd657f1e084e40306f660d3cd2d60d09097bf",
+    "algorithm": "sha512",
+    "filename": "android-ndk-r11c-linux-x86_64.tar.xz",
+    "unpack": true
+  },
+  {
+    "size": 735225120,
+    "visibility": "internal",
+    "digest": "ec936b87d151fcf0a5920e0954fbfff18fc04595d753f6b11a0582faee3ce60e3429c828a94c1533ae31ff27272a6d0c637d901153b946e51241ea89ce8179b3",
+    "algorithm": "sha512",
+    "filename": "android-sdk-linux.tar.xz",
+    "unpack": true
+  },
+  {
+    "size": 9507240,
+    "visibility": "public",
+    "digest": "3cf69a10dca3969bcd2873b96b0207de01ce13f299b2af37f424f21959be849c0e729d5f74cca41f33e0c59bc87b2329a07ac9f901b4a0a9672e6441ceca704d",
+    "algorithm": "sha512",
+    "filename": "dump_syms"
+  }
+]
new file mode 100644
--- /dev/null
+++ b/testing/mozharness/configs/openh264/tooltool-manifests/linux.manifest
@@ -0,0 +1,9 @@
+[
+  {
+    "size": 9507240,
+    "visibility": "public",
+    "digest": "3cf69a10dca3969bcd2873b96b0207de01ce13f299b2af37f424f21959be849c0e729d5f74cca41f33e0c59bc87b2329a07ac9f901b4a0a9672e6441ceca704d",
+    "algorithm": "sha512",
+    "filename": "dump_syms"
+  }
+]
new file mode 100644
--- /dev/null
+++ b/testing/mozharness/configs/openh264/tooltool-manifests/osx.manifest
@@ -0,0 +1,9 @@
+[
+  {
+    "size": 475508,
+    "visibility": "public",
+    "digest": "d705fb149fd7ec6a0833000224f802bd8cbb0f3ecf96efa558010e63db691ff03a95180d9525229e7055e8810c528bf3d7f1a1ab97aecadab3c7c7de8668ab30",
+    "algorithm": "sha512",
+    "filename": "dump_syms"
+  }
+]
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/testing/mozharness/configs/openh264/tooltool-manifests/win.manifest
@@ -0,0 +1,17 @@
+[
+  {
+    "version": "Visual Studio 2015 Update 3 14.0.25425.01 / SDK 10.0.14393.0",
+    "size": 326656969,
+    "digest": "babc414ffc0457d27f5a1ed24a8e4873afbe2f1c1a4075469a27c005e1babc3b2a788f643f825efedff95b79686664c67ec4340ed535487168a3482e68559bc7",
+    "algorithm": "sha512",
+    "filename": "vs2015u3.zip",
+    "unpack": true
+  },
+  {
+    "size": 56832,
+    "visibility": "public",
+    "digest": "01e6bd936ef008061b95a94008682869ebf2a29d1c035e49cd6579b1a020d0d195431221577f127eb52b97137337efe30e6de6c5a2f4b6a87ff2a990e9874e8e",
+    "algorithm": "sha512",
+    "filename": "dump_syms.exe"
+  }
+]
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/testing/mozharness/configs/openh264/win32.py
@@ -0,0 +1,35 @@
+import sys
+import os
+
+import mozharness
+
+external_tools_path = os.path.join(
+    os.path.abspath(os.path.dirname(os.path.dirname(mozharness.__file__))),
+    'external_tools',
+)
+
+VSPATH = '%(abs_work_dir)s/vs2015u3'
+config = {
+   'tooltool_manifest_file': "win.manifest",
+   'exes': {
+       'gittool.py': [sys.executable, os.path.join(external_tools_path, 'gittool.py')],
+       'python2.7': 'c:\\mozilla-build\\python27\\python2.7.exe',
+       'tooltool.py': [sys.executable, "c:\\mozilla-build\\tooltool.py"],
+   },
+   'dump_syms_binary': 'dump_syms.exe',
+   'arch': 'x86',
+   'use_yasm': True,
+   'operating_system': 'msvc',
+   'partial_env': {
+       'PATH': '%s;%s;%s' % (
+           '{_VSPATH}/VC/redist/x86/Microsoft.VC140.CRT;{_VSPATH}/VC/redist/x64/Microsoft.VC140.CRT;{_VSPATH}/SDK/Redist/ucrt/DLLs/x86;{_VSPATH}/SDK/Redist/ucrt/DLLs/x64;{_VSPATH}/VC/bin/amd64_x86;{_VSPATH}/VC/bin/amd64;{_VSPATH}/VC/bin;{_VSPATH}/SDK/bin/x86;{_VSPATH}/SDK/bin/x64;{_VSPATH}/DIA SDK/bin'.format(_VSPATH=VSPATH),
+           os.environ['PATH'],
+           'C:\\mozilla-build\\Git\\bin',
+       ),
+       'WIN32_REDIST_DIR': '{_VSPATH}/VC/redist/x86/Microsoft.VC140.CRT'.format(_VSPATH=VSPATH),
+       'WIN_UCRT_REDIST_DIR': '{_VSPATH}/SDK/Redist/ucrt/DLLs/x86'.format(_VSPATH=VSPATH),
+       'INCLUDE': '{_VSPATH}/VC/include;{_VSPATH}/VC/atlmfc/include;{_VSPATH}/SDK/Include/10.0.14393.0/ucrt;{_VSPATH}/SDK/Include/10.0.14393.0/shared;{_VSPATH}/SDK/Include/10.0.14393.0/um;{_VSPATH}/SDK/Include/10.0.14393.0/winrt;{_VSPATH}/DIA SDK/include'.format(_VSPATH=VSPATH),
+       'LIB': '{_VSPATH}/VC/lib;{_VSPATH}/VC/atlmfc/lib;{_VSPATH}/SDK/lib/10.0.14393.0/ucrt/x86;{_VSPATH}/SDK/lib/10.0.14393.0/um/x86;{_VSPATH}/DIA SDK/lib'.format(_VSPATH=VSPATH),
+       'WINDOWSSDKDIR': '{_VSPATH}/SDK'.format(_VSPATH=VSPATH),
+   },
+}
new file mode 100644
--- /dev/null
+++ b/testing/mozharness/configs/openh264/win64.py
@@ -0,0 +1,35 @@
+import sys
+import os
+
+import mozharness
+
+external_tools_path = os.path.join(
+    os.path.abspath(os.path.dirname(os.path.dirname(mozharness.__file__))),
+    'external_tools',
+)
+
+VSPATH = '%(abs_work_dir)s/vs2015u3'
+config = {
+   'tooltool_manifest_file': "win.manifest",
+   'exes': {
+        'gittool.py': [sys.executable, os.path.join(external_tools_path, 'gittool.py')],
+        'python2.7': 'c:\\mozilla-build\\python27\\python2.7.exe',
+        'tooltool.py': [sys.executable, "c:\\mozilla-build\\tooltool.py"],
+   },
+   'dump_syms_binary': 'dump_syms.exe',
+   'arch': 'x64',
+   'use_yasm': True,
+   'operating_system': 'msvc',
+   'partial_env': {
+       'PATH': '%s;%s;%s' % (
+           '{_VSPATH}/VC/bin/amd64;{_VSPATH}/VC/bin;{_VSPATH}/SDK/bin/x64;{_VSPATH}/VC/redist/x64/Microsoft.VC140.CRT;{_VSPATH}/SDK/Redist/ucrt/DLLs/x64;{_VSPATH}/VC/redist/x86/Microsoft.VC140.CRT;{_VSPATH}/SDK/Redist/ucrt/DLLs/x86;{_VSPATH}/DIA SDK/bin'.format(_VSPATH=VSPATH),
+           os.environ['PATH'],
+           'C:\\mozilla-build\\Git\\bin',
+       ),
+       'WIN32_REDIST_DIR': '{_VSPATH}/VC/redist/x64/Microsoft.VC140.CRT'.format(_VSPATH=VSPATH),
+       'WIN_UCRT_REDIST_DIR': '{_VSPATH}/SDK/Redist/ucrt/DLLs/x64'.format(_VSPATH=VSPATH),
+       'INCLUDE': '{_VSPATH}/VC/include;{_VSPATH}/VC/atlmfc/include;{_VSPATH}/SDK/Include/10.0.14393.0/ucrt;{_VSPATH}/SDK/Include/10.0.14393.0/shared;{_VSPATH}/SDK/Include/10.0.14393.0/um;{_VSPATH}/SDK/Include/10.0.14393.0/winrt;{_VSPATH}/DIA SDK/include'.format(_VSPATH=VSPATH),
+       'LIB': '{_VSPATH}/VC/lib/amd64;{_VSPATH}/VC/atlmfc/lib/amd64;{_VSPATH}/SDK/lib/10.0.14393.0/ucrt/x64;{_VSPATH}/SDK/lib/10.0.14393.0/um/x64;{_VSPATH}/DIA SDK/lib/amd64'.format(_VSPATH=VSPATH),
+       'WINDOWSSDKDIR': '{_VSPATH}/SDK'.format(_VSPATH=VSPATH),
+   },
+}
new file mode 100644
--- /dev/null
+++ b/testing/mozharness/external_tools/packagesymbols.py
@@ -0,0 +1,74 @@
+#!/usr/bin/env python
+
+from __future__ import print_function
+
+import argparse
+import os
+import subprocess
+import sys
+import zipfile
+
+
+class ProcError(Exception):
+    def __init__(self, returncode, stderr):
+        self.returncode = returncode
+        self.stderr = stderr
+
+
+def check_output(command):
+    proc = subprocess.Popen(command,
+                            stdout=subprocess.PIPE,
+                            stderr=subprocess.PIPE)
+    stdout, stderr = proc.communicate()
+    if proc.returncode != 0:
+        raise ProcError(proc.returncode, stderr)
+    return stdout
+
+
+def process_file(dump_syms, path):
+    try:
+        stdout = check_output([dump_syms, path])
+    except ProcError as e:
+        print('Error: running "%s %s": %s' % (dump_syms, path, e.stderr))
+        return None, None, None
+    bits = stdout.splitlines()[0].split(' ', 4)
+    if len(bits) != 5:
+        return None, None, None
+    _, platform, cpu_arch, debug_id, debug_file = bits
+    if debug_file.lower().endswith('.pdb'):
+        sym_file = debug_file[:-4] + '.sym'
+    else:
+        sym_file = debug_file + '.sym'
+    filename = os.path.join(debug_file, debug_id, sym_file)
+    debug_filename = os.path.join(debug_file, debug_id, debug_file)
+    return filename, stdout, debug_filename
+
+
+def main():
+    parser = argparse.ArgumentParser()
+    parser.add_argument('dump_syms', help='Path to dump_syms binary')
+    parser.add_argument('files', nargs='+',
+                        help='Path to files to dump symbols from')
+    parser.add_argument('--symbol-zip', default='symbols.zip',
+                        help='Name of zip file to put dumped symbols in')
+    parser.add_argument('--no-binaries',
+                        action='store_true',
+                        default=False,
+                        help='Don\'t store binaries in zip file')
+    args = parser.parse_args()
+    count = 0
+    with zipfile.ZipFile(args.symbol_zip, 'w', zipfile.ZIP_DEFLATED) as zf:
+        for f in args.files:
+            filename, contents, debug_filename = process_file(args.dump_syms, f)
+            if not (filename and contents):
+                print('Error dumping symbols')
+                sys.exit(1)
+            zf.writestr(filename, contents)
+            count += 1
+            if not args.no_binaries:
+                zf.write(f, debug_filename)
+                count += 1
+    print('Added %d files to %s' % (count, args.symbol_zip))
+
+if __name__ == '__main__':
+    main()
--- a/testing/mozharness/mozharness/base/script.py
+++ b/testing/mozharness/mozharness/base/script.py
@@ -1545,16 +1545,17 @@ class ScriptMixin(PlatformMixin):
             if halt_on_failure:
                 level = FATAL
             self.log("Can't open %s for writing!" % tmp_stderr_filename +
                      self.exception(), level=level)
             return None
         shell = True
         if isinstance(command, list):
             shell = False
+
         p = subprocess.Popen(command, shell=shell, stdout=tmp_stdout,
                              cwd=cwd, stderr=tmp_stderr, env=env)
         # XXX: changed from self.debug to self.log due to this error:
         #      TypeError: debug() takes exactly 1 argument (2 given)
         self.log("Temporary files: %s and %s" % (tmp_stdout_filename, tmp_stderr_filename), level=DEBUG)
         p.wait()
         tmp_stdout.close()
         tmp_stderr.close()
--- a/testing/mozharness/mozharness/base/transfer.py
+++ b/testing/mozharness/mozharness/base/transfer.py
@@ -116,8 +116,57 @@ class TransferMixin(object):
         try:
             r = urllib2.urlopen(url, timeout=timeout)
             j = json.load(r)
             self.log(pprint.pformat(j), level=log_level)
         except:
             self.exception(message="Unable to download %s!" % url)
             raise
         return j
+
+    def scp_upload_directory(self, local_path, ssh_key, ssh_user,
+                             remote_host, remote_path,
+                             scp_options=None,
+                             error_level=ERROR,
+                             create_remote_directory=True,
+                            ):
+        """
+        Create a remote directory and upload the contents of
+        a local directory to it via scp only
+
+        Returns:
+            None: on success
+              -1: if local_path is not a directory
+              -2: if the remote_directory cannot be created
+                  (it only makes sense if create_remote_directory is True)
+              -3: scp fails to copy to the remote directory
+        """
+        dirs = self.query_abs_dirs()
+        self.info("Uploading the contents of %s to %s:%s" % (local_path, remote_host, remote_path))
+        ssh = self.query_exe("ssh")
+        scp = self.query_exe("scp")
+        if scp_options is None:
+            scp_options = '-rp'
+        if not os.path.isdir(local_path):
+            self.log("%s isn't a directory!" % local_path,
+                     level=ERROR)
+            return -1
+        if create_remote_directory:
+            mkdir_error_list = [{
+                'substr': r'''exists but is not a directory''',
+                'level': ERROR
+            }] + SSHErrorList
+            if self.run_command([ssh, '-oIdentityFile=%s' % ssh_key,
+                                 '%s@%s' % (ssh_user, remote_host),
+                                 'mkdir', '-p', remote_path],
+                                cwd=dirs['abs_work_dir'],
+                                return_type='num_errors',
+                                error_list=mkdir_error_list):
+                self.log("Unable to create remote directory %s:%s!" % (remote_host, remote_path), level=error_level)
+                return -2
+        if self.run_command([scp, '-oIdentityFile=%s' % ssh_key,
+                             scp_options, '.',
+                             '%s@%s:%s/' % (ssh_user, remote_host, remote_path)],
+                            cwd=local_path,
+                            return_type='num_errors',
+                            error_list=SSHErrorList):
+            self.log("Unable to scp %s to %s:%s!" % (local_path, remote_host, remote_path), level=error_level)
+            return -3
\ No newline at end of file
--- a/testing/mozharness/mozharness/mozilla/testing/testbase.py
+++ b/testing/mozharness/mozharness/mozilla/testing/testbase.py
@@ -450,49 +450,52 @@ 2. running via buildbot and running the 
         }
         suite_categories = [aliases.get(name, name) for name in suite_categories]
 
         dirs = self.query_abs_dirs()
         test_install_dir = dirs.get('abs_test_install_dir',
                                     os.path.join(dirs['abs_work_dir'], 'tests'))
         self.mkdir_p(test_install_dir)
         package_requirements = self._read_packages_manifest()
+        target_packages = []
         for category in suite_categories:
             if category in package_requirements:
-                target_packages = package_requirements[category]
+                target_packages.extend(package_requirements[category])
             else:
                 # If we don't harness specific requirements, assume the common zip
                 # has everything we need to run tests for this suite.
-                target_packages = package_requirements['common']
+                target_packages.extend(package_requirements['common'])
 
-            self.info("Downloading packages: %s for test suite category: %s" %
-                      (target_packages, category))
-            for file_name in target_packages:
-                target_dir = test_install_dir
-                unpack_dirs = extract_dirs
+        # eliminate duplicates -- no need to download anything twice
+        target_packages = list(set(target_packages))
+        self.info("Downloading packages: %s for test suite categories: %s" %
+                  (target_packages, suite_categories))
+        for file_name in target_packages:
+            target_dir = test_install_dir
+            unpack_dirs = extract_dirs
 
-                if "common.tests" in file_name and isinstance(unpack_dirs, list):
-                    # Ensure that the following files are always getting extracted
-                    required_files = ["mach",
-                                      "mozinfo.json",
-                                      ]
-                    for req_file in required_files:
-                        if req_file not in unpack_dirs:
-                            self.info("Adding '{}' for extraction from common.tests zip file"
-                                      .format(req_file))
-                            unpack_dirs.append(req_file)
+            if "common.tests" in file_name and isinstance(unpack_dirs, list):
+                # Ensure that the following files are always getting extracted
+                required_files = ["mach",
+                                  "mozinfo.json",
+                                  ]
+                for req_file in required_files:
+                    if req_file not in unpack_dirs:
+                        self.info("Adding '{}' for extraction from common.tests zip file"
+                                  .format(req_file))
+                        unpack_dirs.append(req_file)
 
-                if "jsshell-" in file_name or file_name == "target.jsshell.zip":
-                    self.info("Special-casing the jsshell zip file")
-                    unpack_dirs = None
-                    target_dir = dirs['abs_test_bin_dir']
+            if "jsshell-" in file_name or file_name == "target.jsshell.zip":
+                self.info("Special-casing the jsshell zip file")
+                unpack_dirs = None
+                target_dir = dirs['abs_test_bin_dir']
 
-                url = self.query_build_dir_url(file_name)
-                self.download_unpack(url, target_dir,
-                                     extract_dirs=unpack_dirs)
+            url = self.query_build_dir_url(file_name)
+            self.download_unpack(url, target_dir,
+                                 extract_dirs=unpack_dirs)
 
     def _download_test_zip(self, extract_dirs=None):
         dirs = self.query_abs_dirs()
         test_install_dir = dirs.get('abs_test_install_dir',
                                     os.path.join(dirs['abs_work_dir'], 'tests'))
         self.download_unpack(self.test_url, test_install_dir,
                              extract_dirs=extract_dirs)
 
--- a/testing/mozharness/scripts/openh264_build.py
+++ b/testing/mozharness/scripts/openh264_build.py
@@ -2,42 +2,56 @@
 # ***** BEGIN LICENSE BLOCK *****
 # 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/.
 # ***** END LICENSE BLOCK *****
 import sys
 import os
 import glob
+import re
 
 # load modules from parent dir
 sys.path.insert(1, os.path.dirname(sys.path[0]))
 
 # import the guts
+import mozharness
 from mozharness.base.vcs.vcsbase import VCSScript
 from mozharness.base.log import ERROR
 from mozharness.base.transfer import TransferMixin
 from mozharness.mozilla.mock import MockMixin
+from mozharness.mozilla.tooltool import TooltoolMixin
 
 
-class OpenH264Build(MockMixin, TransferMixin, VCSScript):
+external_tools_path = os.path.join(
+    os.path.abspath(os.path.dirname(os.path.dirname(mozharness.__file__))),
+    'external_tools',
+)
+
+
+class OpenH264Build(MockMixin, TransferMixin, VCSScript, TooltoolMixin):
     all_actions = [
         'clobber',
+        'get-tooltool',
         'checkout-sources',
         'build',
         'test',
         'package',
+        'dump-symbols',
         'upload',
     ]
 
     default_actions = [
+        'get-tooltool',
         'checkout-sources',
         'build',
         'test',
         'package',
+        'dump-symbols',
+        'upload',
     ]
 
     config_options = [
         [["--repo"], {
             "dest": "repo",
             "help": "OpenH264 repository to use",
             "default": "https://github.com/cisco/openh264.git"
         }],
@@ -46,26 +60,19 @@ class OpenH264Build(MockMixin, TransferM
             "help": "revision to checkout",
             "default": "master"
         }],
         [["--debug"], {
             "dest": "debug_build",
             "action": "store_true",
             "help": "Do a debug build",
         }],
-        [["--64"], {
-            "dest": "64bit",
-            "action": "store_true",
-            "help": "Do a 64-bit build",
-            "default": True,
-        }],
-        [["--32"], {
-            "dest": "64bit",
-            "action": "store_false",
-            "help": "Do a 32-bit build",
+        [["--arch"], {
+            "dest": "arch",
+            "help": "Arch type to use (x64, x86, arm, or aarch64)",
         }],
         [["--os"], {
             "dest": "operating_system",
             "help": "Specify the operating system to build for",
         }],
         [["--use-mock"], {
             "dest": "use_mock",
             "help": "use mock to set up build environment",
@@ -73,32 +80,35 @@ class OpenH264Build(MockMixin, TransferM
             "default": False,
         }],
         [["--use-yasm"], {
             "dest": "use_yasm",
             "help": "use yasm instead of nasm",
             "action": "store_true",
             "default": False,
         }],
+        [["--avoid-avx2"], {
+            "dest": "avoid_avx2",
+            "help": "Pass HAVE_AVX2='false' through to Make to support older nasm",
+            "action": "store_true",
+            "default": False,
+        }]
     ]
 
     def __init__(self, require_config_file=False, config={},
                  all_actions=all_actions,
                  default_actions=default_actions):
 
         # Default configuration
         default_config = {
             'debug_build': False,
-            'mock_target': 'mozilla-centos6-x86_64',
-            'mock_packages': ['make', 'git', 'nasm', 'glibc-devel.i686', 'libstdc++-devel.i686', 'zip', 'yasm'],
-            'mock_files': [],
-            'upload_ssh_key': os.path.expanduser("~/.ssh/ffxbld_rsa"),
+            'upload_ssh_key': "~/.ssh/ffxbld_rsa",
             'upload_ssh_user': 'ffxbld',
-            'upload_ssh_host': 'stage.mozilla.org',
-            'upload_path_base': '/home/ffxbld/openh264',
+            'upload_ssh_host': 'upload.ffxbld.productdelivery.prod.mozaws.net',
+            'upload_path_base': '/tmp/openh264',
             'use_yasm': False,
         }
         default_config.update(config)
 
         VCSScript.__init__(
             self,
             config_options=self.config_options,
             require_config_file=require_config_file,
@@ -106,47 +116,82 @@ class OpenH264Build(MockMixin, TransferM
             all_actions=all_actions,
             default_actions=default_actions,
         )
 
         if self.config['use_mock']:
             self.setup_mock()
             self.enable_mock()
 
+    def get_tooltool(self):
+        c = self.config
+        if not c.get('tooltool_manifest_file'):
+            self.info("Skipping tooltool fetching since no tooltool manifest")
+            return
+        dirs = self.query_abs_dirs()
+        self.mkdir_p(dirs['abs_work_dir'])
+        manifest = os.path.join(dirs['base_work_dir'],
+                                'scripts', 'configs',
+                                'openh264', 'tooltool-manifests',
+                                c['tooltool_manifest_file'])
+        self.info("Getting tooltool files from manifest (%s)" % manifest)
+        try:
+            self.tooltool_fetch(
+                manifest=manifest,
+                output_dir=dirs['abs_work_dir'],
+                cache=c.get('tooltool_cache')
+            )
+        except KeyError:
+            self.error('missing a required key.')
+
     def query_package_name(self):
-        if self.config['64bit']:
+        if self.config['arch'] == "x64":
             bits = '64'
         else:
             bits = '32'
-
         version = self.config['revision']
 
         if sys.platform == 'linux2':
             if self.config.get('operating_system') == 'android':
-                return 'openh264-android-{version}.zip'.format(version=version, bits=bits)
+                return 'openh264-android-{arch}-{version}.zip'.format(
+                    version=version, arch=self.config['arch'])
             else:
                 return 'openh264-linux{bits}-{version}.zip'.format(version=version, bits=bits)
         elif sys.platform == 'darwin':
             return 'openh264-macosx{bits}-{version}.zip'.format(version=version, bits=bits)
         elif sys.platform == 'win32':
             return 'openh264-win{bits}-{version}.zip'.format(version=version, bits=bits)
         self.fatal("can't determine platform")
 
     def query_make_params(self):
+        dirs = self.query_abs_dirs()
         retval = []
         if self.config['debug_build']:
             retval.append('BUILDTYPE=Debug')
 
-        if self.config['64bit']:
+        if self.config['avoid_avx2']:
+            retval.append('HAVE_AVX2=false')
+
+        if self.config['arch'] in ('x64', 'aarch64'):
             retval.append('ENABLE64BIT=Yes')
         else:
             retval.append('ENABLE64BIT=No')
 
         if "operating_system" in self.config:
             retval.append("OS=%s" % self.config['operating_system'])
+            if self.config["operating_system"] == "android":
+                if self.config['arch'] == 'x86':
+                    retval.append("ARCH=x86")
+                elif self.config['arch'] == 'aarch64':
+                    retval.append("ARCH=arm64")
+                else:
+                    retval.append("ARCH=arm")
+                retval.append('TARGET=invalid')
+                retval.append('NDKLEVEL=%s' % self.config['min_sdk'])
+                retval.append('NDKROOT=%s/android-ndk-r11c' % dirs['abs_work_dir'])
 
         if self.config['use_yasm']:
             retval.append('ASM=yasm')
 
         return retval
 
     def query_upload_ssh_key(self):
         return self.config['upload_ssh_key']
@@ -155,21 +200,28 @@ class OpenH264Build(MockMixin, TransferM
         return self.config['upload_ssh_host']
 
     def query_upload_ssh_user(self):
         return self.config['upload_ssh_user']
 
     def query_upload_ssh_path(self):
         return "%s/%s" % (self.config['upload_path_base'], self.config['revision'])
 
-    def run_make(self, target):
+    def run_make(self, target, capture_output=False):
         cmd = ['make', target] + self.query_make_params()
         dirs = self.query_abs_dirs()
         repo_dir = os.path.join(dirs['abs_work_dir'], 'src')
-        return self.run_command(cmd, cwd=repo_dir)
+        env = None
+        if self.config.get('partial_env'):
+            env = self.query_env(self.config['partial_env'])
+        kwargs = dict(cwd=repo_dir, env=env)
+        if capture_output:
+            return self.get_output_from_command(cmd, **kwargs)
+        else:
+            return self.run_command(cmd, **kwargs)
 
     def checkout_sources(self):
         repo = self.config['repo']
         rev = self.config['revision']
 
         dirs = self.query_abs_dirs()
         repo_dir = os.path.join(dirs['abs_work_dir'], 'src')
 
@@ -212,28 +264,67 @@ class OpenH264Build(MockMixin, TransferM
 
     def package(self):
         dirs = self.query_abs_dirs()
         srcdir = os.path.join(dirs['abs_work_dir'], 'src')
         package_name = self.query_package_name()
         package_file = os.path.join(dirs['abs_work_dir'], package_name)
         if os.path.exists(package_file):
             os.unlink(package_file)
-        to_package = [os.path.basename(f) for f in glob.glob(os.path.join(srcdir, "*gmpopenh264*"))]
+        to_package = []
+        for f in glob.glob(os.path.join(srcdir, "*gmpopenh264*")):
+            if not re.search(
+                    "(?:lib)?gmpopenh264(?!\.\d)\.(?:dylib|so|dll|info)(?!\.\d)",
+                    f):
+                # Don't package unnecessary zip bloat
+                # Blocks things like libgmpopenh264.2.dylib and libgmpopenh264.so.1
+                self.log("Skipping packaging of {package}".format(package=f))
+                continue
+            to_package.append(os.path.basename(f))
+        self.log("Packaging files %s" % to_package)
         cmd = ['zip', package_file] + to_package
         retval = self.run_command(cmd, cwd=srcdir)
         if retval != 0:
             self.fatal("couldn't make package")
         self.copy_to_upload_dir(package_file)
 
+    def dump_symbols(self):
+        dirs = self.query_abs_dirs()
+        c = self.config
+        srcdir = os.path.join(dirs['abs_work_dir'], 'src')
+        package_name = self.run_make('echo-plugin-name', capture_output=True)
+        if not package_name:
+            self.fatal("failure running make")
+        zip_package_name = self.query_package_name()
+        if not zip_package_name[-4:] == ".zip":
+            self.fatal("Unexpected zip_package_name")
+        symbol_package_name = "{base}.symbols.zip".format(base=zip_package_name[:-4])
+        symbol_zip_path = os.path.join(dirs['abs_upload_dir'], symbol_package_name)
+        repo_dir = os.path.join(dirs['abs_work_dir'], 'src')
+        env = None
+        if self.config.get('partial_env'):
+            env = self.query_env(self.config['partial_env'])
+        kwargs = dict(cwd=repo_dir, env=env)
+        dump_syms = os.path.join(dirs['abs_work_dir'], c['dump_syms_binary'])
+        self.chmod(dump_syms, 0755)
+        python = self.query_exe('python2.7')
+        cmd = [python, os.path.join(external_tools_path, 'packagesymbols.py'),
+               '--symbol-zip', symbol_zip_path,
+               dump_syms, os.path.join(srcdir, package_name)]
+        if self.config['use_mock']:
+            self.disable_mock()
+        self.run_command(cmd, **kwargs)
+        if self.config['use_mock']:
+            self.enable_mock()
+
     def upload(self):
         if self.config['use_mock']:
             self.disable_mock()
         dirs = self.query_abs_dirs()
-        self.rsync_upload_directory(
+        self.scp_upload_directory(
             dirs['abs_upload_dir'],
             self.query_upload_ssh_key(),
             self.query_upload_ssh_user(),
             self.query_upload_ssh_host(),
             self.query_upload_ssh_path(),
         )
         if self.config['use_mock']:
             self.enable_mock()
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -52,16 +52,17 @@ XPCOMUtils.defineLazyModuleGetters(this,
   ExtensionStorage: "resource://gre/modules/ExtensionStorage.jsm",
   ExtensionTestCommon: "resource://testing-common/ExtensionTestCommon.jsm",
   FileSource: "resource://gre/modules/L10nRegistry.jsm",
   L10nRegistry: "resource://gre/modules/L10nRegistry.jsm",
   Log: "resource://gre/modules/Log.jsm",
   MessageChannel: "resource://gre/modules/MessageChannel.jsm",
   NetUtil: "resource://gre/modules/NetUtil.jsm",
   OS: "resource://gre/modules/osfile.jsm",
+  PluralForm: "resource://gre/modules/PluralForm.jsm",
   Schemas: "resource://gre/modules/Schemas.jsm",
   setTimeout: "resource://gre/modules/Timer.jsm",
   TelemetryStopwatch: "resource://gre/modules/TelemetryStopwatch.jsm",
 });
 
 XPCOMUtils.defineLazyGetter(
   this, "processScript",
   () => Cc["@mozilla.org/webextensions/extension-process-script;1"]
@@ -787,16 +788,159 @@ this.ExtensionData = class {
       promises.push(this.readLocaleFile(defaultLocale));
     }
 
     let results = await Promise.all(promises);
 
     this.localeData.selectedLocale = locale;
     return results[0];
   }
+
+  /**
+   * Formats all the strings for a permissions dialog/notification.
+   *
+   * @param {object} info Information about the permissions being requested.
+   *
+   * @param {array<string>} info.permissions.origins
+   *                        Origin permissions requested.
+   * @param {array<string>} info.permissions.permissions
+   *                        Regular (non-origin) permissions requested.
+   * @param {AddonWrapper} info.addonName
+   *                       The name of the addon for which permissions are
+   *                       being requested.
+   * @param {boolean} info.unsigned
+   *                  True if the prompt is for installing an unsigned addon.
+   * @param {string} info.type
+   *                 The type of prompt being shown.  May be one of "update",
+   *                 "sideload", "optional", or omitted for a regular
+   *                 install prompt.
+   * @param {string} info.appName
+   *                 The localized name of the application, to be substituted
+   *                 in computed strings as needed.
+   * @param {nsIStringBundle} bundle
+   *                          The string bundle to use for l10n.
+   *
+   * @returns {object} An object with properties containing localized strings
+   *                   for various elements of a permission dialog.
+   */
+  static formatPermissionStrings(info, bundle) {
+    let result = {};
+
+    let perms = info.permissions || {origins: [], permissions: []};
+
+    // First classify our host permissions
+    let allUrls = false, wildcards = [], sites = [];
+    for (let permission of perms.origins) {
+      if (permission == "<all_urls>") {
+        allUrls = true;
+        break;
+      }
+      let match = /^[htps*]+:\/\/([^/]+)\//.exec(permission);
+      if (!match) {
+        Cu.reportError(`Unparseable host permission ${permission}`);
+        continue;
+      }
+      if (match[1] == "*") {
+        allUrls = true;
+      } else if (match[1].startsWith("*.")) {
+        wildcards.push(match[1].slice(2));
+      } else {
+        sites.push(match[1]);
+      }
+    }
+
+    // Format the host permissions.  If we have a wildcard for all urls,
+    // a single string will suffice.  Otherwise, show domain wildcards
+    // first, then individual host permissions.
+    result.msgs = [];
+    if (allUrls) {
+      result.msgs.push(bundle.GetStringFromName("webextPerms.hostDescription.allUrls"));
+    } else {
+      // Formats a list of host permissions.  If we have 4 or fewer, display
+      // them all, otherwise display the first 3 followed by an item that
+      // says "...plus N others"
+      let format = (list, itemKey, moreKey) => {
+        function formatItems(items) {
+          result.msgs.push(...items.map(item => bundle.formatStringFromName(itemKey, [item], 1)));
+        }
+        if (list.length < 5) {
+          formatItems(list);
+        } else {
+          formatItems(list.slice(0, 3));
+
+          let remaining = list.length - 3;
+          result.msgs.push(PluralForm.get(remaining, bundle.GetStringFromName(moreKey))
+                                     .replace("#1", remaining));
+        }
+      };
+
+      format(wildcards, "webextPerms.hostDescription.wildcard",
+             "webextPerms.hostDescription.tooManyWildcards");
+      format(sites, "webextPerms.hostDescription.oneSite",
+             "webextPerms.hostDescription.tooManySites");
+    }
+
+    let permissionKey = perm => `webextPerms.description.${perm}`;
+
+    // Next, show the native messaging permission if it is present.
+    const NATIVE_MSG_PERM = "nativeMessaging";
+    if (perms.permissions.includes(NATIVE_MSG_PERM)) {
+      result.msgs.push(bundle.formatStringFromName(permissionKey(NATIVE_MSG_PERM), [info.appName], 1));
+    }
+
+    // Finally, show remaining permissions, in any order.
+    for (let permission of perms.permissions) {
+      // Handled above
+      if (permission == "nativeMessaging") {
+        continue;
+      }
+      try {
+        result.msgs.push(bundle.GetStringFromName(permissionKey(permission)));
+      } catch (err) {
+        // We deliberately do not include all permissions in the prompt.
+        // So if we don't find one then just skip it.
+      }
+    }
+
+    result.header = bundle.formatStringFromName("webextPerms.header", [info.addonName], 1);
+    result.text = info.unsigned ?
+                  bundle.GetStringFromName("webextPerms.unsignedWarning") : "";
+    result.listIntro = bundle.GetStringFromName("webextPerms.listIntro");
+
+    result.acceptText = bundle.GetStringFromName("webextPerms.add.label");
+    result.acceptKey = bundle.GetStringFromName("webextPerms.add.accessKey");
+    result.cancelText = bundle.GetStringFromName("webextPerms.cancel.label");
+    result.cancelKey = bundle.GetStringFromName("webextPerms.cancel.accessKey");
+
+    if (info.type == "sideload") {
+      result.header = bundle.formatStringFromName("webextPerms.sideloadHeader", [info.addonName], 1);
+      let key = result.msgs.length == 0 ?
+                "webextPerms.sideloadTextNoPerms" : "webextPerms.sideloadText2";
+      result.text = bundle.GetStringFromName(key);
+      result.acceptText = bundle.GetStringFromName("webextPerms.sideloadEnable.label");
+      result.acceptKey = bundle.GetStringFromName("webextPerms.sideloadEnable.accessKey");
+      result.cancelText = bundle.GetStringFromName("webextPerms.sideloadCancel.label");
+      result.cancelKey = bundle.GetStringFromName("webextPerms.sideloadCancel.accessKey");
+    } else if (info.type == "update") {
+      result.header = "";
+      result.text = bundle.formatStringFromName("webextPerms.updateText", [info.addonName], 1);
+      result.acceptText = bundle.GetStringFromName("webextPerms.updateAccept.label");
+      result.acceptKey = bundle.GetStringFromName("webextPerms.updateAccept.accessKey");
+    } else if (info.type == "optional") {
+      result.header = bundle.formatStringFromName("webextPerms.optionalPermsHeader", [info.addonName], 1);
+      result.text = "";
+      result.listIntro = bundle.GetStringFromName("webextPerms.optionalPermsListIntro");
+      result.acceptText = bundle.GetStringFromName("webextPerms.optionalPermsAllow.label");
+      result.acceptKey = bundle.GetStringFromName("webextPerms.optionalPermsAllow.accessKey");
+      result.cancelText = bundle.GetStringFromName("webextPerms.optionalPermsDeny.label");
+      result.cancelKey = bundle.GetStringFromName("webextPerms.optionalPermsDeny.accessKey");
+    }
+
+    return result;
+  }
 };
 
 const PROXIED_EVENTS = new Set(["test-harness-message", "add-permissions", "remove-permissions"]);
 
 const shutdownPromises = new Map();
 
 class BootstrapScope {
   install(data, reason) {}
--- a/toolkit/components/extensions/ext-c-webRequest.js
+++ b/toolkit/components/extensions/ext-c-webRequest.js
@@ -1,24 +1,17 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
-var {
-  ExtensionError,
-} = ExtensionCommon;
-
 this.webRequest = class extends ExtensionAPI {
   getAPI(context) {
     return {
       webRequest: {
         filterResponseData(requestId) {
-          if (AppConstants.RELEASE_OR_BETA) {
-            throw new ExtensionError("filterResponseData() unsupported in release builds");
-          }
           requestId = parseInt(requestId, 10);
 
           return context.cloneScope.StreamFilter.create(
             requestId, context.extension.id);
         },
       },
     };
   }
--- a/toolkit/components/extensions/test/mochitest/mochitest-common.ini
+++ b/toolkit/components/extensions/test/mochitest/mochitest-common.ini
@@ -127,17 +127,17 @@ skip-if = os == 'android'
 [test_ext_webrequest_auth.html]
 skip-if = os == 'android'
 [test_ext_webrequest_background_events.html]
 [test_ext_webrequest_hsts.html]
 [test_ext_webrequest_basic.html]
 [test_ext_webrequest_filter.html]
 [test_ext_webrequest_frameId.html]
 [test_ext_webrequest_responseBody.html]
-skip-if = release_or_beta || os == 'android'
+skip-if = os == 'android'
 [test_ext_webrequest_suspend.html]
 [test_ext_webrequest_upload.html]
 skip-if = os == 'android' # Currently fails in emulator tests
 [test_ext_webrequest_permission.html]
 [test_ext_webrequest_websocket.html]
 [test_ext_webnavigation.html]
 [test_ext_webnavigation_filters.html]
 [test_ext_window_postMessage.html]
--- a/toolkit/components/extensions/test/mochitest/slow_response.sjs
+++ b/toolkit/components/extensions/test/mochitest/slow_response.sjs
@@ -4,17 +4,17 @@
 
 /* eslint-disable no-unused-vars */
 
 const Cu = Components.utils;
 const Ci = Components.interfaces;
 
 Cu.import("resource://gre/modules/AppConstants.jsm");
 
-const DELAY = AppConstants.DEBUG ? 2000 : 500;
+const DELAY = AppConstants.DEBUG ? 4000 : 800;
 
 let nsTimer = Components.Constructor("@mozilla.org/timer;1", "nsITimer", "initWithCallback");
 
 let timer;
 function delay() {
   return new Promise(resolve => {
     timer = nsTimer(resolve, DELAY, Ci.nsITimer.TYPE_ONE_SHOT);
   });
--- a/toolkit/components/extensions/test/mochitest/test_ext_webrequest_responseBody.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_responseBody.html
@@ -31,17 +31,17 @@ const PARTS = [
   "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. <br>",
   "Excepteur sint occaecat cupidatat non proident, <br>",
   "sunt in culpa qui officia deserunt mollit anim id est laborum.<br>",
   `
     </body>
     </html>`,
 ].map(part => `${part}\n`);
 
-const TIMEOUT = AppConstants.DEBUG ? 2000 : 500;
+const TIMEOUT = AppConstants.DEBUG ? 4000 : 800;
 const TASKS = [
   {
     url: "slow_response.sjs",
     task(filter, resolve, num) {
       let decoder = new TextDecoder("utf-8");
 
       browser.test.assertEq("uninitialized", filter.status,
                             `(${num}): Got expected initial status`);
@@ -275,17 +275,16 @@ const TASKS = [
       filter.onerror = event => {
         browser.test.fail(`(${num}): Got unexpected error event: ${filter.error}`);
       };
     },
     verify(response) {
       is(response, PARTS.slice(0, 3).join(""), "Got expected final HTML");
     },
   },
-/*
   {
     url: "lorem.html.gz",
     task(filter, resolve, num) {
       let response = "";
       let decoder = new TextDecoder("utf-8");
 
       filter.onstart = event => {
         browser.test.log(`(${num}): Request start`);
@@ -317,17 +316,16 @@ const TASKS = [
       filter.onerror = event => {
         browser.test.fail(`(${num}): Got unexpected error event: ${filter.error}`);
       };
     },
     verify(response) {
       is(response, PARTS.join(""), "Got expected final HTML");
     },
   },
-*/
 ];
 
 function serializeTest(test, num) {
   /* globals ExtensionTestCommon */
 
   let url = `${test.url}?test_num=${num}`;
   let task = ExtensionTestCommon.serializeFunction(test.task);
 
--- a/toolkit/components/extensions/webrequest/PStreamFilter.ipdl
+++ b/toolkit/components/extensions/webrequest/PStreamFilter.ipdl
@@ -2,40 +2,35 @@
  * 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 protocol PBackground;
 
 namespace mozilla {
 namespace extensions {
 
-protocol PStreamFilter
+async protocol PStreamFilter
 {
-  manager PBackground;
-
 parent:
   async Write(uint8_t[] data);
 
   async FlushedData();
 
   async Suspend();
   async Resume();
   async Close();
   async Disconnect();
 
 child:
-  async Initialized(bool aSuccess);
   async Resumed();
   async Suspended();
   async Closed();
 
   async FlushData();
 
   async StartRequest();
   async Data(uint8_t[] data);
   async StopRequest(nsresult aStatus);
-
-  async __delete__();
 };
 
 } // namespace extensions
 } // namespace mozilla
 
--- a/toolkit/components/extensions/webrequest/StreamFilter.cpp
+++ b/toolkit/components/extensions/webrequest/StreamFilter.cpp
@@ -4,51 +4,49 @@
  * 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 "StreamFilter.h"
 
 #include "jsapi.h"
 #include "jsfriendapi.h"
 
+#include "mozilla/AbstractThread.h"
 #include "mozilla/HoldDropJSObjects.h"
 #include "mozilla/SystemGroup.h"
 #include "mozilla/extensions/StreamFilterChild.h"
 #include "mozilla/extensions/StreamFilterEvents.h"
-#include "mozilla/ipc/BackgroundChild.h"
-#include "mozilla/ipc/PBackgroundChild.h"
+#include "mozilla/extensions/StreamFilterParent.h"
+#include "mozilla/dom/ContentChild.h"
 #include "nsContentUtils.h"
 #include "nsCycleCollectionParticipant.h"
 #include "nsLiteralString.h"
 #include "nsThreadUtils.h"
 #include "nsTArray.h"
 
 using namespace JS;
 using namespace mozilla::dom;
 
-using mozilla::ipc::BackgroundChild;
-using mozilla::ipc::PBackgroundChild;
-
 namespace mozilla {
 namespace extensions {
 
 /*****************************************************************************
  * Initialization
  *****************************************************************************/
 
 StreamFilter::StreamFilter(nsIGlobalObject* aParent,
                            uint64_t aRequestId,
                            const nsAString& aAddonId)
   : mParent(aParent)
   , mChannelId(aRequestId)
   , mAddonId(NS_Atomize(aAddonId))
 {
   MOZ_ASSERT(aParent);
 
-  ConnectToPBackground();
+  Connect();
 };
 
 StreamFilter::~StreamFilter()
 {
   ForgetActor();
 }
 
 void
@@ -71,47 +69,65 @@ StreamFilter::Create(GlobalObject& aGlob
   return filter.forget();
 }
 
 /*****************************************************************************
  * Actor allocation
  *****************************************************************************/
 
 void
-StreamFilter::ConnectToPBackground()
+StreamFilter::Connect()
 {
-  PBackgroundChild* background = BackgroundChild::GetForCurrentThread();
-  if (background) {
-    ActorCreated(background);
+  MOZ_ASSERT(!mActor);
+
+  mActor = new StreamFilterChild();
+  mActor->SetStreamFilter(this);
+
+  nsAutoString addonId;
+  mAddonId->ToString(addonId);
+
+  ContentChild* cc = ContentChild::GetSingleton();
+  if (cc) {
+    RefPtr<StreamFilter> self(this);
+
+    cc->SendInitStreamFilter(mChannelId, addonId)->Then(
+      GetCurrentThreadSerialEventTarget(),
+      __func__,
+      [=] (mozilla::ipc::Endpoint<PStreamFilterChild>&& aEndpoint) {
+        self->FinishConnect(Move(aEndpoint));
+      },
+      [=] (mozilla::ipc::PromiseRejectReason aReason) {
+        self->mActor->RecvInitialized(false);
+      });
   } else {
-    bool ok = BackgroundChild::GetOrCreateForCurrentThread(this);
-    MOZ_RELEASE_ASSERT(ok);
+    mozilla::ipc::Endpoint<PStreamFilterChild> endpoint;
+    Unused << StreamFilterParent::Create(nullptr, mChannelId, addonId, &endpoint);
+
+    // Always dispatch asynchronously so JS callers have a chance to attach
+    // event listeners before we dispatch events.
+    NS_DispatchToCurrentThread(
+      NewRunnableMethod<mozilla::ipc::Endpoint<PStreamFilterChild>&&>(
+        "StreamFilter::FinishConnect",
+        this, &StreamFilter::FinishConnect,
+        Move(endpoint)));
   }
 }
 
 void
-StreamFilter::ActorFailed()
-{
-  MOZ_CRASH("Failed to create a PBackgroundChild actor");
-}
-
-void
-StreamFilter::ActorCreated(PBackgroundChild* aBackground)
+StreamFilter::FinishConnect(mozilla::ipc::Endpoint<PStreamFilterChild>&& aEndpoint)
 {
-  MOZ_ASSERT(aBackground);
-  MOZ_ASSERT(!mActor);
-
-  nsAutoString addonId;
-  mAddonId->ToString(addonId);
+  if (aEndpoint.IsValid()) {
+    MOZ_RELEASE_ASSERT(aEndpoint.Bind(mActor));
+    mActor->RecvInitialized(true);
 
-  PStreamFilterChild* actor = aBackground->SendPStreamFilterConstructor(mChannelId, addonId);
-  MOZ_ASSERT(actor);
-
-  mActor = static_cast<StreamFilterChild*>(actor);
-  mActor->SetStreamFilter(this);
+    // IPC now owns this reference.
+    Unused << do_AddRef(mActor);
+  } else {
+    mActor->RecvInitialized(false);
+  }
 }
 
 /*****************************************************************************
  * Binding methods
  *****************************************************************************/
 
 template <typename T>
 static inline bool
@@ -268,17 +284,16 @@ JSObject*
 StreamFilter::WrapObject(JSContext* aCx, HandleObject aGivenProto)
 {
   return StreamFilterBinding::Wrap(aCx, this, aGivenProto);
 }
 
 NS_IMPL_CYCLE_COLLECTION_CLASS(StreamFilter)
 
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(StreamFilter)
-  NS_INTERFACE_MAP_ENTRY(nsIIPCBackgroundChildCreateCallback)
 NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
 
 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(StreamFilter, DOMEventTargetHelper)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mParent)
 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
 
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(StreamFilter, DOMEventTargetHelper)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mParent)
--- a/toolkit/components/extensions/webrequest/StreamFilter.h
+++ b/toolkit/components/extensions/webrequest/StreamFilter.h
@@ -9,32 +9,34 @@
 
 #include "mozilla/dom/BindingDeclarations.h"
 #include "mozilla/dom/StreamFilterBinding.h"
 
 #include "mozilla/DOMEventTargetHelper.h"
 #include "nsCOMPtr.h"
 #include "nsCycleCollectionParticipant.h"
 #include "nsIAtom.h"
-#include "nsIIPCBackgroundChildCreateCallback.h"
 
 namespace mozilla {
+namespace ipc {
+  template <class T> class Endpoint;
+}
+
 namespace extensions {
 
+class PStreamFilterChild;
 class StreamFilterChild;
 
 using namespace mozilla::dom;
 
 class StreamFilter : public DOMEventTargetHelper
-                   , public nsIIPCBackgroundChildCreateCallback
 {
   friend class StreamFilterChild;
 
   NS_DECL_ISUPPORTS_INHERITED
-  NS_DECL_NSIIPCBACKGROUNDCHILDCREATECALLBACK
   NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(StreamFilter, DOMEventTargetHelper)
 
   static already_AddRefed<StreamFilter>
   Create(GlobalObject& global,
          uint64_t aRequestId,
          const nsAString& aAddonId);
 
   explicit StreamFilter(nsIGlobalObject* aParent,
@@ -74,17 +76,20 @@ protected:
   void FireEvent(const nsAString& aType);
 
   void FireDataEvent(const nsTArray<uint8_t>& aData);
 
   void FireErrorEvent(const nsAString& aError);
 
 private:
   void
-  ConnectToPBackground();
+  Connect();
+
+  void
+  FinishConnect(mozilla::ipc::Endpoint<PStreamFilterChild>&& aEndpoint);
 
   void ForgetActor();
 
   nsCOMPtr<nsIGlobalObject> mParent;
   RefPtr<StreamFilterChild> mActor;
 
   nsString mError;
 
--- a/toolkit/components/extensions/webrequest/StreamFilterChild.cpp
+++ b/toolkit/components/extensions/webrequest/StreamFilterChild.cpp
@@ -273,31 +273,30 @@ StreamFilterChild::MaybeStopRequest()
     break;
   }
 }
 
 /*****************************************************************************
  * State change acknowledgment callbacks
  *****************************************************************************/
 
-IPCResult
-StreamFilterChild::RecvInitialized(const bool& aSuccess)
+void
+StreamFilterChild::RecvInitialized(bool aSuccess)
 {
   MOZ_ASSERT(mState == State::Uninitialized);
 
   if (aSuccess) {
     mState = State::Initialized;
   } else {
     mState = State::Error;
     if (mStreamFilter) {
       mStreamFilter->FireErrorEvent(NS_LITERAL_STRING("Invalid request ID"));
       mStreamFilter = nullptr;
     }
   }
-  return IPC_OK();
 }
 
 IPCResult
 StreamFilterChild::RecvClosed() {
   MOZ_DIAGNOSTIC_ASSERT(mState == State::Closing);
 
   SetNextState();
   return IPC_OK();
@@ -509,10 +508,16 @@ StreamFilterChild::RecvData(Data&& aData
  *****************************************************************************/
 
 void
 StreamFilterChild::ActorDestroy(ActorDestroyReason aWhy)
 {
   mStreamFilter = nullptr;
 }
 
+void
+StreamFilterChild::DeallocPStreamFilterChild()
+{
+  RefPtr<StreamFilterChild> self = dont_AddRef(this);
+}
+
 } // namespace extensions
 } // namespace mozilla
--- a/toolkit/components/extensions/webrequest/StreamFilterChild.h
+++ b/toolkit/components/extensions/webrequest/StreamFilterChild.h
@@ -87,29 +87,30 @@ public:
 
   State GetState() const
   {
     return mState;
   }
 
   StreamFilterStatus Status() const;
 
+  void  RecvInitialized(bool aSuccess);
+
 protected:
-  virtual IPCResult RecvInitialized(const bool& aSuccess) override;
 
   virtual IPCResult RecvStartRequest() override;
   virtual IPCResult RecvData(Data&& data) override;
   virtual IPCResult RecvStopRequest(const nsresult& aStatus) override;
 
   virtual IPCResult RecvClosed() override;
   virtual IPCResult RecvSuspended() override;
   virtual IPCResult RecvResumed() override;
   virtual IPCResult RecvFlushData() override;
 
-  virtual IPCResult Recv__delete__() override { return IPC_OK(); }
+  virtual void DeallocPStreamFilterChild() override;
 
   void
   SetStreamFilter(StreamFilter* aStreamFilter)
   {
     mStreamFilter = aStreamFilter;
   }
 
 private:
--- a/toolkit/components/extensions/webrequest/StreamFilterParent.cpp
+++ b/toolkit/components/extensions/webrequest/StreamFilterParent.cpp
@@ -3,36 +3,37 @@
 /* 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 "StreamFilterParent.h"
 
 #include "mozilla/ScopeExit.h"
 #include "mozilla/Unused.h"
-#include "mozilla/dom/nsIContentParent.h"
+#include "mozilla/dom/ContentParent.h"
+#include "nsHttpChannel.h"
 #include "nsIChannel.h"
 #include "nsIHttpChannelInternal.h"
 #include "nsIInputStream.h"
 #include "nsITraceableChannel.h"
 #include "nsProxyRelease.h"
+#include "nsQueryObject.h"
+#include "nsSocketTransportService2.h"
 #include "nsStringStream.h"
 
 namespace mozilla {
 namespace extensions {
 
 /*****************************************************************************
  * Initialization
  *****************************************************************************/
 
-StreamFilterParent::StreamFilterParent(uint64_t aChannelId, const nsAString& aAddonId)
-  : mChannelId(aChannelId)
-  , mAddonId(NS_Atomize(aAddonId))
-  , mPBackgroundThread(NS_GetCurrentThread())
-  , mIOThread(do_GetMainThread())
+StreamFilterParent::StreamFilterParent()
+  : mMainThread(GetCurrentThreadEventTarget())
+  , mIOThread(mMainThread)
   , mBufferMutex("StreamFilter buffer mutex")
   , mReceivedStop(false)
   , mSentStop(false)
   , mContext(nullptr)
   , mOffset(0)
   , mState(State::Uninitialized)
 {
 }
@@ -40,59 +41,80 @@ StreamFilterParent::StreamFilterParent(u
 StreamFilterParent::~StreamFilterParent()
 {
   NS_ReleaseOnMainThreadSystemGroup("StreamFilterParent::mOrigListener",
                                     mOrigListener.forget());
   NS_ReleaseOnMainThreadSystemGroup("StreamFilterParent::mContext",
                                     mContext.forget());
 }
 
-void
-StreamFilterParent::Init(already_AddRefed<nsIContentParent> aContentParent)
-{
-  AssertIsPBackgroundThread();
-
-  SystemGroup::Dispatch(
-    TaskCategory::Network,
-    NewRunnableMethod<already_AddRefed<nsIContentParent>&&>(
-        "StreamFilterParent::DoInit",
-        this, &StreamFilterParent::DoInit, Move(aContentParent)));
-}
-
-void
-StreamFilterParent::DoInit(already_AddRefed<nsIContentParent>&& aContentParent)
+bool
+StreamFilterParent::Create(dom::ContentParent* aContentParent, uint64_t aChannelId, const nsAString& aAddonId,
+                           Endpoint<PStreamFilterChild>* aEndpoint)
 {
   AssertIsMainThread();
 
-  nsCOMPtr<nsIContentParent> contentParent = aContentParent;
-
-  bool success = false;
-  auto guard = MakeScopeExit([&] {
-    RefPtr<StreamFilterParent> self(this);
-
-    RunOnPBackgroundThread(FUNC, [=] {
-      if (self->IPCActive()) {
-        self->mState = State::Initialized;
-        self->CheckResult(self->SendInitialized(success));
-      }
-    });
-  });
-
   auto& webreq = WebRequestService::GetSingleton();
 
-  mChannel = webreq.GetTraceableChannel(mChannelId, mAddonId, contentParent);
-  if (NS_WARN_IF(!mChannel)) {
-    return;
+  nsCOMPtr<nsIAtom> addonId = NS_Atomize(aAddonId);
+  nsCOMPtr<nsIChannel> channel = webreq.GetTraceableChannel(aChannelId, addonId, aContentParent);
+
+  RefPtr<nsHttpChannel> chan = do_QueryObject(channel);
+  NS_ENSURE_TRUE(chan, false);
+
+  Endpoint<PStreamFilterParent> parent;
+  Endpoint<PStreamFilterChild> child;
+  nsresult rv = PStreamFilter::CreateEndpoints(chan->ProcessId(),
+                                               aContentParent ? aContentParent->OtherPid()
+                                                              : base::GetCurrentProcId(),
+                                               &parent, &child);
+  NS_ENSURE_SUCCESS(rv, false);
+
+  if (!chan->AttachStreamFilter(Move(parent))) {
+    return false;
   }
 
-  nsCOMPtr<nsITraceableChannel> traceable = do_QueryInterface(mChannel);
+  *aEndpoint = Move(child);
+  return true;
+}
+
+/* static */ void
+StreamFilterParent::Attach(nsIChannel* aChannel, ParentEndpoint&& aEndpoint)
+{
+  auto self = MakeRefPtr<StreamFilterParent>();
+
+  self->ActorThread()->Dispatch(
+    NewRunnableMethod<ParentEndpoint&&>("StreamFilterParent::Bind",
+                                        self,
+                                        &StreamFilterParent::Bind,
+                                        Move(aEndpoint)),
+    NS_DISPATCH_NORMAL);
+
+  self->Init(aChannel);
+
+  // IPC owns this reference now.
+  Unused << self.forget();
+}
+
+void
+StreamFilterParent::Bind(ParentEndpoint&& aEndpoint)
+{
+  aEndpoint.Bind(this);
+}
+
+void
+StreamFilterParent::Init(nsIChannel* aChannel)
+{
+  mChannel = aChannel;
+
+  nsCOMPtr<nsITraceableChannel> traceable = do_QueryInterface(aChannel);
   MOZ_RELEASE_ASSERT(traceable);
 
   nsresult rv = traceable->SetNewListener(this, getter_AddRefs(mOrigListener));
-  success = NS_SUCCEEDED(rv);
+  MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv));
 }
 
 /*****************************************************************************
  * nsIThreadRetargetableStreamListener
  *****************************************************************************/
 
 NS_IMETHODIMP
 StreamFilterParent::CheckListenerChain()
@@ -109,105 +131,117 @@ StreamFilterParent::CheckListenerChain()
 
 /*****************************************************************************
  * Error handling
  *****************************************************************************/
 
 void
 StreamFilterParent::Broken()
 {
-  AssertIsPBackgroundThread();
+  AssertIsActorThread();
 
   mState = State::Disconnecting;
 
   RefPtr<StreamFilterParent> self(this);
   RunOnIOThread(FUNC, [=] {
     self->FlushBufferedData();
 
-    RunOnPBackgroundThread(FUNC, [=] {
+    RunOnActorThread(FUNC, [=] {
       if (self->IPCActive()) {
         self->mState = State::Disconnected;
       }
     });
   });
 }
 
 /*****************************************************************************
  * State change requests
  *****************************************************************************/
 
 IPCResult
 StreamFilterParent::RecvClose()
 {
-  AssertIsPBackgroundThread();
+  AssertIsActorThread();
 
   mState = State::Closed;
 
   if (!mSentStop) {
     RefPtr<StreamFilterParent> self(this);
     RunOnMainThread(FUNC, [=] {
       nsresult rv = self->EmitStopRequest(NS_OK);
       Unused << NS_WARN_IF(NS_FAILED(rv));
     });
   }
 
   Unused << SendClosed();
-  Unused << Send__delete__(this);
+  Destroy();
   return IPC_OK();
 }
 
+void
+StreamFilterParent::Destroy()
+{
+  // Close the channel asynchronously so the actor is never destroyed before
+  // this message is fully processed.
+  ActorThread()->Dispatch(
+    NewRunnableMethod("StreamFilterParent::Close",
+                      this,
+                      &StreamFilterParent::Close),
+    NS_DISPATCH_NORMAL);
+}
+
 IPCResult
 StreamFilterParent::RecvSuspend()
 {
-  AssertIsPBackgroundThread();
+  AssertIsActorThread();
 
   if (mState == State::TransferringData) {
     RefPtr<StreamFilterParent> self(this);
     RunOnMainThread(FUNC, [=] {
       self->mChannel->Suspend();
 
-      RunOnPBackgroundThread(FUNC, [=] {
+      RunOnActorThread(FUNC, [=] {
         if (self->IPCActive()) {
           self->mState = State::Suspended;
           self->CheckResult(self->SendSuspended());
         }
       });
     });
   }
   return IPC_OK();
 }
 
 IPCResult
 StreamFilterParent::RecvResume()
 {
-  AssertIsPBackgroundThread();
+  AssertIsActorThread();
 
   if (mState == State::Suspended) {
     // Change state before resuming so incoming data is handled correctly
     // immediately after resuming.
     mState = State::TransferringData;
 
     RefPtr<StreamFilterParent> self(this);
     RunOnMainThread(FUNC, [=] {
       self->mChannel->Resume();
 
-      RunOnPBackgroundThread(FUNC, [=] {
+      RunOnActorThread(FUNC, [=] {
         if (self->IPCActive()) {
           self->CheckResult(self->SendResumed());
         }
       });
     });
   }
   return IPC_OK();
 }
 
 IPCResult
 StreamFilterParent::RecvDisconnect()
 {
-  AssertIsPBackgroundThread();
+  AssertIsActorThread();
 
   if (mState == State::Suspended) {
   RefPtr<StreamFilterParent> self(this);
     RunOnMainThread(FUNC, [=] {
       self->mChannel->Resume();
     });
   } else if (mState != State::TransferringData) {
     return IPC_OK();
@@ -216,48 +250,52 @@ StreamFilterParent::RecvDisconnect()
   mState = State::Disconnecting;
   CheckResult(SendFlushData());
   return IPC_OK();
 }
 
 IPCResult
 StreamFilterParent::RecvFlushedData()
 {
-  AssertIsPBackgroundThread();
+  AssertIsActorThread();
 
   MOZ_ASSERT(mState == State::Disconnecting);
 
-  Unused << Send__delete__(this);
+  Destroy();
 
   RefPtr<StreamFilterParent> self(this);
   RunOnIOThread(FUNC, [=] {
     self->FlushBufferedData();
 
-    RunOnPBackgroundThread(FUNC, [=] {
+    RunOnActorThread(FUNC, [=] {
       self->mState = State::Disconnected;
     });
   });
   return IPC_OK();
 }
 
 /*****************************************************************************
  * Data output
  *****************************************************************************/
 
 IPCResult
 StreamFilterParent::RecvWrite(Data&& aData)
 {
-  AssertIsPBackgroundThread();
+  AssertIsActorThread();
 
-  mIOThread->Dispatch(
-    NewRunnableMethod<Data&&>("StreamFilterParent::WriteMove",
-                              this,
-                              &StreamFilterParent::WriteMove,
-                              Move(aData)),
-    NS_DISPATCH_NORMAL);
+  if (IsIOThread()) {
+    Write(aData);
+  } else {
+    IOThread()->Dispatch(
+      NewRunnableMethod<Data&&>("StreamFilterParent::WriteMove",
+                                this,
+                                &StreamFilterParent::WriteMove,
+                                Move(aData)),
+      NS_DISPATCH_NORMAL);
+  }
   return IPC_OK();
 }
 
 void
 StreamFilterParent::WriteMove(Data&& aData)
 {
   nsresult rv = Write(aData);
   Unused << NS_WARN_IF(NS_FAILED(rv));
@@ -290,17 +328,17 @@ NS_IMETHODIMP
 StreamFilterParent::OnStartRequest(nsIRequest* aRequest, nsISupports* aContext)
 {
   AssertIsMainThread();
 
   mContext = aContext;
 
   if (mState != State::Disconnected) {
     RefPtr<StreamFilterParent> self(this);
-    RunOnPBackgroundThread(FUNC, [=] {
+    RunOnActorThread(FUNC, [=] {
       if (self->IPCActive()) {
         self->mState = State::TransferringData;
         self->CheckResult(self->SendStartRequest());
       }
     });
   }
 
   return mOrigListener->OnStartRequest(aRequest, aContext);
@@ -314,17 +352,17 @@ StreamFilterParent::OnStopRequest(nsIReq
   AssertIsMainThread();
 
   mReceivedStop = true;
   if (mState == State::Disconnected) {
     return EmitStopRequest(aStatusCode);
   }
 
   RefPtr<StreamFilterParent> self(this);
-  RunOnPBackgroundThread(FUNC, [=] {
+  RunOnActorThread(FUNC, [=] {
     if (self->IPCActive()) {
       self->CheckResult(self->SendStopRequest(aStatusCode));
     }
   });
   return NS_OK;
 }
 
 nsresult
@@ -339,33 +377,37 @@ StreamFilterParent::EmitStopRequest(nsre
 
 /*****************************************************************************
  * Incoming data handling
  *****************************************************************************/
 
 void
 StreamFilterParent::DoSendData(Data&& aData)
 {
-  AssertIsPBackgroundThread();
+  AssertIsActorThread();
 
   if (mState == State::TransferringData) {
     CheckResult(SendData(aData));
   }
 }
 
 NS_IMETHODIMP
 StreamFilterParent::OnDataAvailable(nsIRequest* aRequest,
                                     nsISupports* aContext,
                                     nsIInputStream* aInputStream,
                                     uint64_t aOffset,
                                     uint32_t aCount)
 {
   // Note: No AssertIsIOThread here. Whatever thread we're on now is, by
   // definition, the IO thread.
-  mIOThread = NS_GetCurrentThread();
+  if (OnSocketThread()) {
+    mIOThread = nullptr;
+  } else {
+    mIOThread = NS_GetCurrentThread();
+  }
 
   if (mState == State::Disconnected) {
     // If we're offloading data in a thread pool, it's possible that we'll
     // have buffered some additional data while waiting for the buffer to
     // flush. So, if there's any buffered data left, flush that before we
     // flush this incoming data.
     //
     // Note: When in the eDisconnected state, the buffer list is guaranteed
@@ -389,17 +431,17 @@ StreamFilterParent::OnDataAvailable(nsIR
   NS_ENSURE_TRUE(count == aCount, NS_ERROR_UNEXPECTED);
 
   if (mState == State::Disconnecting) {
     MutexAutoLock al(mBufferMutex);
     BufferData(Move(data));
   } else if (mState == State::Closed) {
     return NS_ERROR_FAILURE;
   } else {
-    mPBackgroundThread->Dispatch(
+    ActorThread()->Dispatch(
       NewRunnableMethod<Data&&>("StreamFilterParent::DoSendData",
                                 this,
                                 &StreamFilterParent::DoSendData,
                                 Move(data)),
       NS_DISPATCH_NORMAL);
   }
   return NS_OK;
 }
@@ -430,26 +472,95 @@ StreamFilterParent::FlushBufferedData()
       }
     });
   }
 
   return NS_OK;
 }
 
 /*****************************************************************************
+ * Thread helpers
+ *****************************************************************************/
+
+void
+StreamFilterParent::AssertIsActorThread()
+{
+  MOZ_ASSERT(OnSocketThread());
+}
+
+nsIEventTarget*
+StreamFilterParent::ActorThread()
+{
+  return gSocketTransportService;
+}
+
+nsIEventTarget*
+StreamFilterParent::IOThread()
+{
+  if (mIOThread) {
+    return mIOThread;
+  }
+  return gSocketTransportService;
+}
+
+bool
+StreamFilterParent::IsIOThread()
+{
+  return (mIOThread ? NS_GetCurrentThread() == mIOThread
+                    : OnSocketThread());
+}
+
+void
+StreamFilterParent::AssertIsIOThread()
+{
+  MOZ_ASSERT(IsIOThread());
+}
+
+template<typename Function>
+void
+StreamFilterParent::RunOnActorThread(const char* aName, Function&& aFunc)
+{
+  if (OnSocketThread()) {
+    aFunc();
+  } else {
+    gSocketTransportService->Dispatch(
+      Move(NS_NewRunnableFunction(aName, aFunc)),
+      NS_DISPATCH_NORMAL);
+  }
+}
+
+template<typename Function>
+void
+StreamFilterParent::RunOnIOThread(const char* aName, Function&& aFunc)
+{
+  if (mIOThread) {
+    mIOThread->Dispatch(Move(NS_NewRunnableFunction(aName, aFunc)),
+                        NS_DISPATCH_NORMAL);
+  } else {
+    RunOnActorThread(aName, Move(aFunc));
+  }
+}
+
+/*****************************************************************************
  * Glue
  *****************************************************************************/
 
 void
 StreamFilterParent::ActorDestroy(ActorDestroyReason aWhy)
 {
-  AssertIsPBackgroundThread();
+  AssertIsActorThread();
 
   if (mState != State::Disconnected && mState != State::Closed) {
     Broken();
   }
 }
 
+void
+StreamFilterParent::DeallocPStreamFilterParent()
+{
+  RefPtr<StreamFilterParent> self = dont_AddRef(this);
+}
+
 NS_IMPL_ISUPPORTS(StreamFilterParent, nsIStreamListener, nsIRequestObserver, nsIThreadRetargetableStreamListener)
 
 } // namespace extensions
 } // namespace mozilla
 
--- a/toolkit/components/extensions/webrequest/StreamFilterParent.h
+++ b/toolkit/components/extensions/webrequest/StreamFilterParent.h
@@ -22,17 +22,20 @@
 #if defined(_MSC_VER)
 #  define FUNC __FUNCSIG__
 #else
 #  define FUNC __PRETTY_FUNCTION__
 #endif
 
 namespace mozilla {
 namespace dom {
-  class nsIContentParent;
+  class ContentParent;
+}
+namespace net {
+  class nsHttpChannel;
 }
 
 namespace extensions {
 
 using namespace mozilla::dom;
 using mozilla::ipc::IPCResult;
 
 class StreamFilterParent final
@@ -42,17 +45,25 @@ class StreamFilterParent final
   , public StreamFilterBase
 {
 public:
   NS_DECL_THREADSAFE_ISUPPORTS
   NS_DECL_NSISTREAMLISTENER
   NS_DECL_NSIREQUESTOBSERVER
   NS_DECL_NSITHREADRETARGETABLESTREAMLISTENER
 
-  explicit StreamFilterParent(uint64_t aChannelId, const nsAString& aAddonId);
+  StreamFilterParent();
+
+  using ParentEndpoint = mozilla::ipc::Endpoint<PStreamFilterParent>;
+
+  static bool Create(ContentParent* aContentParent,
+                     uint64_t aChannelId, const nsAString& aAddonId,
+                     mozilla::ipc::Endpoint<PStreamFilterChild>* aEndpoint);
+
+  static void Attach(nsIChannel* aChannel, ParentEndpoint&& aEndpoint);
 
   enum class State
   {
     // The parent has been created, but not yet constructed by the child.
     Uninitialized,
     // The parent has been successfully constructed.
     Initialized,
     // The OnRequestStarted event has been received, and data is being
@@ -67,44 +78,41 @@ public:
     // currnetly in transit to, or buffered by, the child will be written to the
     // output listener before we enter the Disconnected atate.
     Disconnecting,
     // The channel has been disconnected from the child, and all further data
     // and events will be passed directly to the output listener.
     Disconnected,
   };
 
-  static already_AddRefed<StreamFilterParent>
-  Create(uint64_t aChannelId, const nsAString& aAddonId)
-  {
-    RefPtr<StreamFilterParent> filter = new StreamFilterParent(aChannelId, aAddonId);
-    return filter.forget();
-  }
-
-  void Init(already_AddRefed<nsIContentParent> aContentParent);
-
 protected:
   virtual ~StreamFilterParent();
 
   virtual IPCResult RecvWrite(Data&& aData) override;
   virtual IPCResult RecvFlushedData() override;
   virtual IPCResult RecvSuspend() override;
   virtual IPCResult RecvResume() override;
   virtual IPCResult RecvClose() override;
   virtual IPCResult RecvDisconnect() override;
 
+  virtual void DeallocPStreamFilterParent() override;
+
 private:
   bool IPCActive()
   {
     return (mState != State::Closed &&
             mState != State::Disconnecting &&
             mState != State::Disconnected);
   }
 
-  void DoInit(already_AddRefed<nsIContentParent>&& aContentParent);
+  void Init(nsIChannel* aChannel);
+
+  void Bind(ParentEndpoint&& aEndpoint);
+
+  void Destroy();
 
   nsresult FlushBufferedData();
 
   nsresult Write(Data& aData);
 
   void WriteMove(Data&& aData);
 
   void DoSendData(Data&& aData);
@@ -118,66 +126,51 @@ private:
   void
   CheckResult(bool aResult)
   {
     if (NS_WARN_IF(!aResult)) {
       Broken();
     }
   }
 
-  void
-  AssertIsPBackgroundThread()
-  {
-    MOZ_ASSERT(NS_GetCurrentThread() == mPBackgroundThread);
-  }
+  inline nsIEventTarget* ActorThread();
+
+  inline nsIEventTarget* IOThread();
+
+  inline bool IsIOThread();
 
-  void
-  AssertIsIOThread()
-  {
-    MOZ_ASSERT(NS_GetCurrentThread() == mIOThread);
-  }
+  inline void AssertIsActorThread();
 
-  void
+  inline void AssertIsIOThread();
+
+  static void
   AssertIsMainThread()
   {
     MOZ_ASSERT(NS_IsMainThread());
   }
 
   template<typename Function>
   void
   RunOnMainThread(const char* aName, Function&& aFunc)
   {
-    SystemGroup::Dispatch(TaskCategory::Network,
-                          Move(NS_NewRunnableFunction(aName, aFunc)));
+    mMainThread->Dispatch(Move(NS_NewRunnableFunction(aName, aFunc)),
+                          NS_DISPATCH_NORMAL);
   }
 
   template<typename Function>
-  void
-  RunOnPBackgroundThread(const char* aName, Function&& aFunc)
-  {
-    mPBackgroundThread->Dispatch(Move(NS_NewRunnableFunction(aName, aFunc)),
-                                 NS_DISPATCH_NORMAL);
-  }
+  void RunOnActorThread(const char* aName, Function&& aFunc);
 
   template<typename Function>
-  void
-  RunOnIOThread(const char* aName, Function&& aFunc)
-  {
-    mIOThread->Dispatch(Move(NS_NewRunnableFunction(aName, aFunc)),
-                        NS_DISPATCH_NORMAL);
-  }
-
-  const uint64_t mChannelId;
-  const nsCOMPtr<nsIAtom> mAddonId;
+  void RunOnIOThread(const char* aName, Function&& aFunc);
 
   nsCOMPtr<nsIChannel> mChannel;
   nsCOMPtr<nsIStreamListener> mOrigListener;
 
-  nsCOMPtr<nsIThread> mPBackgroundThread;
-  nsCOMPtr<nsIThread> mIOThread;
+  nsCOMPtr<nsIEventTarget> mMainThread;
+  nsCOMPtr<nsIEventTarget> mIOThread;
 
   Mutex mBufferMutex;
 
   bool mReceivedStop;
   bool mSentStop;
 
   nsCOMPtr<nsISupports> mContext;
   uint64_t mOffset;
--- a/toolkit/components/extensions/webrequest/moz.build
+++ b/toolkit/components/extensions/webrequest/moz.build
@@ -43,12 +43,18 @@ EXPORTS.mozilla.extensions += [
 ]
 
 LOCAL_INCLUDES += [
     '/caps',
 ]
 
 include('/ipc/chromium/chromium-config.mozbuild')
 
+LOCAL_INCLUDES += [
+    # For nsHttpChannel.h
+    '/netwerk/base',
+    '/netwerk/protocol/http',
+]
+
 FINAL_LIBRARY = 'xul'
 
 with Files("**"):
     BUG_COMPONENT = ("Toolkit", "WebExtensions: Request Handling")
--- a/toolkit/components/remotebrowserutils/RemoteWebNavigation.js
+++ b/toolkit/components/remotebrowserutils/RemoteWebNavigation.js
@@ -73,33 +73,33 @@ RemoteWebNavigation.prototype = {
                             aPostData, aHeaders, null);
   },
   loadURIWithOptions(aURI, aLoadFlags, aReferrer, aReferrerPolicy,
                      aPostData, aHeaders, aBaseURI, aTriggeringPrincipal) {
     // We know the url is going to be loaded, let's start requesting network
     // connection before the content process asks.
     // Note that we might have already setup the speculative connection in some
     // cases, especially when the url is from location bar or its popup menu.
-    if (aURI.startsWith("http")) {
-      let uri = makeURI(aURI);
-      let principal = aTriggeringPrincipal;
-      // We usually have a aTriggeringPrincipal assigned, but in case we don't
-      // have one, create it with OA inferred from the current context.
-      if (!principal) {
-        let attrs = {
-          userContextId: this._browser.getAttribute("usercontextid") || 0,
-          privateBrowsingId: PrivateBrowsingUtils.isBrowserPrivate(this._browser) ? 1 : 0
-        };
-        principal = Services.scriptSecurityManager.createCodebasePrincipal(uri, attrs);
-      }
+    if (aURI.startsWith("http:") || aURI.startsWith("https:")) {
       try {
+        let uri = makeURI(aURI);
+        let principal = aTriggeringPrincipal;
+        // We usually have a aTriggeringPrincipal assigned, but in case we don't
+        // have one, create it with OA inferred from the current context.
+        if (!principal) {
+          let attrs = {
+            userContextId: this._browser.getAttribute("usercontextid") || 0,
+            privateBrowsingId: PrivateBrowsingUtils.isBrowserPrivate(this._browser) ? 1 : 0
+          };
+          principal = Services.scriptSecurityManager.createCodebasePrincipal(uri, attrs);
+        }
         Services.io.speculativeConnect2(uri, principal, null);
       } catch (ex) {
         // Can't setup speculative connection for this uri string for some
-        // reason, just ignore it.
+        // reason (such as failing to parse the URI), just ignore it.
       }
     }
     this._sendMessage("WebNavigation:LoadURI", {
       uri: aURI,
       flags: aLoadFlags,
       referrer: aReferrer ? aReferrer.spec : null,
       referrerPolicy: aReferrerPolicy,
       postData: aPostData ? Utils.serializeInputStream(aPostData) : null,
--- a/toolkit/modules/SelectContentHelper.jsm
+++ b/toolkit/modules/SelectContentHelper.jsm
@@ -200,23 +200,23 @@ this.SelectContentHelper.prototype = {
     });
     this._clearPseudoClassStyles();
   },
 
   // Determine user agent background-color and color.
   // This is used to skip applying the custom color if it matches
   // the user agent values.
   _calculateUAColors() {
-    let dummyOption = this.element.ownerDocument.createElement("option");
+    let dummyOption = this.element.ownerDocument.createElementNS("http://www.w3.org/1999/xhtml", "option");
     dummyOption.style.setProperty("color", "-moz-comboboxtext", "important");
     dummyOption.style.setProperty("background-color", "-moz-combobox", "important");
     let optionCS = this.element.ownerGlobal.getComputedStyle(dummyOption);
     this._uaBackgroundColor = optionCS.backgroundColor;
     this._uaColor = optionCS.color;
-    let dummySelect = this.element.ownerDocument.createElement("select");
+    let dummySelect = this.element.ownerDocument.createElementNS("http://www.w3.org/1999/xhtml", "select");
     dummySelect.style.setProperty("color", "-moz-fieldtext", "important");
     dummySelect.style.setProperty("background-color", "-moz-field", "important");
     let selectCS = this.element.ownerGlobal.getComputedStyle(dummySelect);
     this._uaSelectBackgroundColor = selectCS.backgroundColor;
     this._uaSelectColor = selectCS.color;
   },
 
   get uaBackgroundColor() {
--- a/widget/windows/nsNativeThemeWin.cpp
+++ b/widget/windows/nsNativeThemeWin.cpp
@@ -44,17 +44,18 @@ using namespace mozilla::widget;
 extern mozilla::LazyLogModule gWindowsLog;
 
 NS_IMPL_ISUPPORTS_INHERITED(nsNativeThemeWin, nsNativeTheme, nsITheme)
 
 nsNativeThemeWin::nsNativeThemeWin() :
   mProgressDeterminateTimeStamp(TimeStamp::Now()),
   mProgressIndeterminateTimeStamp(TimeStamp::Now()),
   mBorderCacheValid(),
-  mMinimumWidgetSizeCacheValid()
+  mMinimumWidgetSizeCacheValid(),
+  mGutterSizeCacheValid(false)
 {
   // If there is a relevant change in forms.css for windows platform,
   // static widget style variables (e.g. sButtonBorderSize) should be 
   // reinitialized here.
 }
 
 nsNativeThemeWin::~nsNativeThemeWin()
 {
@@ -216,16 +217,29 @@ GetGutterSize(HANDLE theme, HDC hdc)
     int height = std::max(itemSize.cy, std::max(iconSize.cy, checkboxBGSize.cy));
 
     SIZE ret;
     ret.cx = width;
     ret.cy = height;
     return ret;
 }
 
+SIZE
+nsNativeThemeWin::GetCachedGutterSize(HANDLE theme)
+{
+  if (mGutterSizeCacheValid) {
+    return mGutterSizeCache;
+  }
+
+  mGutterSizeCache = GetGutterSize(theme, nullptr);
+  mGutterSizeCacheValid = true;
+
+  return mGutterSizeCache;
+}
+
 /* DrawThemeBGRTLAware - render a theme part based on rtl state.
  * Some widgets are not direction-neutral and need to be drawn reversed for
  * RTL.  Windows provides a way to do this with SetLayout, but this reverses
  * the entire drawing area of a given device context, which means that its
  * use will also affect the positioning of the widget.  There are two ways
  * to work around this:
  *
  * Option 1: Alter the position of the rect that we send so that we cancel
@@ -2268,29 +2282,29 @@ nsNativeThemeWin::GetMinimumWidgetSize(n
       ScaleForFrameDPI(aResult, aFrame);
       return rv;
     }
     case NS_THEME_MENUITEM:
     case NS_THEME_CHECKMENUITEM:
     case NS_THEME_RADIOMENUITEM:
       if(!IsTopLevelMenu(aFrame))
       {
-        SIZE gutterSize(GetGutterSize(theme, nullptr));
+        SIZE gutterSize(GetCachedGutterSize(theme));
         aResult->width = gutterSize.cx;
         aResult->height = gutterSize.cy;
         ScaleForFrameDPI(aResult, aFrame);
         return rv;
       }
       break;
 
     case NS_THEME_MENUIMAGE:
     case NS_THEME_MENUCHECKBOX:
     case NS_THEME_MENURADIO:
       {
-        SIZE boxSize(GetGutterSize(theme, nullptr));
+        SIZE boxSize(GetCachedGutterSize(theme));
         aResult->width = boxSize.cx+2;
         aResult->height = boxSize.cy;
         *aIsOverridable = false;
         ScaleForFrameDPI(aResult, aFrame);
         return rv;
       }
 
     case NS_THEME_MENUITEMTEXT:
@@ -2510,16 +2524,17 @@ nsNativeThemeWin::WidgetStateChanged(nsI
 }
 
 NS_IMETHODIMP
 nsNativeThemeWin::ThemeChanged()
 {
   nsUXThemeData::Invalidate();
   memset(mBorderCacheValid, 0, sizeof(mBorderCacheValid));
   memset(mMinimumWidgetSizeCacheValid, 0, sizeof(mMinimumWidgetSizeCacheValid));
+  mGutterSizeCacheValid = false;
   return NS_OK;
 }
 
 bool 
 nsNativeThemeWin::ThemeSupportsWidget(nsPresContext* aPresContext,
                                       nsIFrame* aFrame,
                                       uint8_t aWidgetType)
 {
--- a/widget/windows/nsNativeThemeWin.h
+++ b/widget/windows/nsNativeThemeWin.h
@@ -127,16 +127,18 @@ protected:
   nsresult GetCachedWidgetBorder(nsIFrame* aFrame, HANDLE aTheme, nsUXThemeClass aThemeClass,
                                  uint8_t aWidgetType, int32_t aPart, int32_t aState,
                                  nsIntMargin* aResult);
 
   nsresult GetCachedMinimumWidgetSize(nsIFrame* aFrame, HANDLE aTheme, nsUXThemeClass aThemeClass,
                                       uint8_t aWidgetType, int32_t aPart, int32_t aState,
                                       THEMESIZE aSizeReq, mozilla::LayoutDeviceIntSize* aResult);
 
+  SIZE GetCachedGutterSize(HANDLE theme);
+
 private:
   TimeStamp mProgressDeterminateTimeStamp;
   TimeStamp mProgressIndeterminateTimeStamp;
 
   // eUXNumClasses * THEME_PART_DISTINCT_VALUE_COUNT is about 800 at the time of writing
   // this, and nsIntMargin is 16 bytes wide, which makes this cache (1/8 + 16) * 800
   // bytes, or about ~12KB. We could probably reduce this cache to 3KB by caching on
   // the aWidgetType value instead, but there would be some uncacheable values, since
@@ -144,11 +146,14 @@ private:
   uint8_t mBorderCacheValid[(eUXNumClasses * THEME_PART_DISTINCT_VALUE_COUNT + 7) / 8];
   nsIntMargin mBorderCache[eUXNumClasses * THEME_PART_DISTINCT_VALUE_COUNT];
 
   // See the above not for mBorderCache and friends. However mozilla::LayoutDeviceIntSize
   // is half the size of nsIntMargin, making the cache roughly half as large. In total
   // the caches should come to about 18KB.
   uint8_t mMinimumWidgetSizeCacheValid[(eUXNumClasses * THEME_PART_DISTINCT_VALUE_COUNT + 7) / 8];
   mozilla::LayoutDeviceIntSize mMinimumWidgetSizeCache[eUXNumClasses * THEME_PART_DISTINCT_VALUE_COUNT];
+
+  bool mGutterSizeCacheValid;
+  SIZE mGutterSizeCache;
 };
 
 #endif
--- a/xpcom/io/nsMultiplexInputStream.cpp
+++ b/xpcom/io/nsMultiplexInputStream.cpp
@@ -55,62 +55,67 @@ public:
 
   void AsyncWaitCompleted();
 
 private:
   ~nsMultiplexInputStream()
   {
   }
 
-  // This method resets mIsSeekable, mIsIPCSerializable, mIsCloneable and
-  // mIsAsyncInputStream values.
-  void UpdateQIMap();
+  // This method updates mSeekableStreams, mIPCSerializableStreams,
+  // mCloneableStreams and mAsyncInputStreams values.
+  void UpdateQIMap(nsIInputStream* aStream, int32_t aCount);
 
   struct MOZ_STACK_CLASS ReadSegmentsState
   {
     nsCOMPtr<nsIInputStream> mThisStream;
     uint32_t mOffset;
     nsWriteSegmentFun mWriter;
     void* mClosure;
     bool mDone;
   };
 
   static nsresult ReadSegCb(nsIInputStream* aIn, void* aClosure,
                             const char* aFromRawSegment, uint32_t aToOffset,
                             uint32_t aCount, uint32_t* aWriteCount);
 
+  bool IsSeekable() const;
+  bool IsIPCSerializable() const;
+  bool IsCloneable() const;
+  bool IsAsyncInputStream() const;
+
   Mutex mLock; // Protects access to all data members.
   nsTArray<nsCOMPtr<nsIInputStream>> mStreams;
   uint32_t mCurrentStream;
   bool mStartedReadingCurrent;
   nsresult mStatus;
   nsCOMPtr<nsIInputStreamCallback> mAsyncWaitCallback;
 
-  bool mIsSeekable;
-  bool mIsIPCSerializable;
-  bool mIsCloneable;
-  bool mIsAsyncInputStream;
+  uint32_t mSeekableStreams;
+  uint32_t mIPCSerializableStreams;
+  uint32_t mCloneableStreams;
+  uint32_t mAsyncInputStreams;
 };
 
 NS_IMPL_ADDREF(nsMultiplexInputStream)
 NS_IMPL_RELEASE(nsMultiplexInputStream)
 
 NS_IMPL_CLASSINFO(nsMultiplexInputStream, nullptr, nsIClassInfo::THREADSAFE,
                   NS_MULTIPLEXINPUTSTREAM_CID)
 
 NS_INTERFACE_MAP_BEGIN(nsMultiplexInputStream)
   NS_INTERFACE_MAP_ENTRY(nsIMultiplexInputStream)
   NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsIInputStream, nsIMultiplexInputStream)
-  NS_INTERFACE_MAP_ENTRY_CONDITIONAL(nsISeekableStream, mIsSeekable)
+  NS_INTERFACE_MAP_ENTRY_CONDITIONAL(nsISeekableStream, IsSeekable())
   NS_INTERFACE_MAP_ENTRY_CONDITIONAL(nsIIPCSerializableInputStream,
-                                     mIsIPCSerializable)
+                                     IsIPCSerializable())
   NS_INTERFACE_MAP_ENTRY_CONDITIONAL(nsICloneableInputStream,
-                                     mIsCloneable)
+                                     IsCloneable())
   NS_INTERFACE_MAP_ENTRY_CONDITIONAL(nsIAsyncInputStream,
-                                     mIsAsyncInputStream)
+                                     IsAsyncInputStream())
   NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIMultiplexInputStream)
   NS_IMPL_QUERY_CLASSINFO(nsMultiplexInputStream)
 NS_INTERFACE_MAP_END
 
 NS_IMPL_CI_INTERFACE_GETTER(nsMultiplexInputStream,
                             nsIMultiplexInputStream,
                             nsIInputStream,
                             nsISeekableStream)
@@ -148,23 +153,25 @@ TellMaybeSeek(nsISeekableStream* aSeekab
     if (NS_SUCCEEDED(rvSeek)) {
       rv = aSeekable->Tell(aResult);
     }
   }
   return rv;
 }
 
 nsMultiplexInputStream::nsMultiplexInputStream()
-  : mLock("nsMultiplexInputStream lock"),
-    mCurrentStream(0),
-    mStartedReadingCurrent(false),
-    mStatus(NS_OK)
-{
-  UpdateQIMap();
-}
+  : mLock("nsMultiplexInputStream lock")
+  , mCurrentStream(0)
+  , mStartedReadingCurrent(false)
+  , mStatus(NS_OK)
+  , mSeekableStreams(0)
+  , mIPCSerializableStreams(0)
+  , mCloneableStreams(0)
+  , mAsyncInputStreams(0)
+{}
 
 NS_IMETHODIMP
 nsMultiplexInputStream::GetCount(uint32_t* aCount)
 {
   MutexAutoLock lock(mLock);
   *aCount = mStreams.Length();
   return NS_OK;
 }
@@ -172,49 +179,54 @@ nsMultiplexInputStream::GetCount(uint32_
 NS_IMETHODIMP
 nsMultiplexInputStream::AppendStream(nsIInputStream* aStream)
 {
   MutexAutoLock lock(mLock);
   if (!mStreams.AppendElement(aStream)) {
     return NS_ERROR_OUT_OF_MEMORY;
   }
 
-  UpdateQIMap();
+  UpdateQIMap(aStream, 1);
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsMultiplexInputStream::InsertStream(nsIInputStream* aStream, uint32_t aIndex)
 {
   MutexAutoLock lock(mLock);
   if (!mStreams.InsertElementAt(aIndex, aStream)) {
     return NS_ERROR_OUT_OF_MEMORY;
   }
 
   if (mCurrentStream > aIndex ||
       (mCurrentStream == aIndex && mStartedReadingCurrent)) {
     ++mCurrentStream;
   }
 
-  UpdateQIMap();
+  UpdateQIMap(aStream, 1);
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsMultiplexInputStream::RemoveStream(uint32_t aIndex)
 {
   MutexAutoLock lock(mLock);
+  if (aIndex >= mStreams.Length()) {
+    return NS_ERROR_FAILURE;
+  }
+
+  UpdateQIMap(mStreams[aIndex], -1);
+
   mStreams.RemoveElementAt(aIndex);
   if (mCurrentStream > aIndex) {
     --mCurrentStream;
   } else if (mCurrentStream == aIndex) {
     mStartedReadingCurrent = false;
   }
 
-  UpdateQIMap();
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsMultiplexInputStream::GetStream(uint32_t aIndex, nsIInputStream** aResult)
 {
   MutexAutoLock lock(mLock);
   *aResult = mStreams.SafeElementAt(aIndex, nullptr);
@@ -1057,48 +1069,61 @@ nsMultiplexInputStream::Clone(nsIInputSt
       return rv;
     }
   }
 
   clone.forget(aClone);
   return NS_OK;
 }
 
-void
-nsMultiplexInputStream::UpdateQIMap()
-{
-  mIsSeekable = true;
-  mIsIPCSerializable = true;
-  mIsCloneable = true;
-  mIsAsyncInputStream = false;
+#define MAYBE_UPDATE_VALUE(x, y)                        \
+  {                                                     \
+    nsCOMPtr<y> substream = do_QueryInterface(aStream); \
+    if (substream) {                                    \
+      if (aCount == 1) {                                \
+        ++x;                                            \
+      } else if (x > 0) {                               \
+        --x;                                            \
+      } else {                                          \
+        MOZ_CRASH("A nsIInputStream changed QI map when stored in a nsMultiplexInputStream!"); \
+      }                                                 \
+    }                                                   \
+  }
 
-  for (uint32_t i = 0, len = mStreams.Length(); i < len; ++i) {
-    if (mIsSeekable) {
-      nsCOMPtr<nsISeekableStream> substream = do_QueryInterface(mStreams[i]);
-      if (!substream) {
-        mIsSeekable = false;
-      }
-    }
+void
+nsMultiplexInputStream::UpdateQIMap(nsIInputStream* aStream, int32_t aCount)
+{
+  MOZ_ASSERT(aStream);
+  MOZ_ASSERT(aCount == -1 || aCount == 1);
+
+  MAYBE_UPDATE_VALUE(mSeekableStreams, nsISeekableStream);
+  MAYBE_UPDATE_VALUE(mIPCSerializableStreams, nsIIPCSerializableInputStream);
+  MAYBE_UPDATE_VALUE(mCloneableStreams, nsICloneableInputStream);
+  MAYBE_UPDATE_VALUE(mAsyncInputStreams, nsIAsyncInputStream);
+}
 
-    if (mIsIPCSerializable) {
-      nsCOMPtr<nsIIPCSerializableInputStream> substream = do_QueryInterface(mStreams[i]);
-      if (!substream) {
-        mIsIPCSerializable = false;
-      }
-    }
+#undef MAYBE_UPDATE_VALUE
+
+bool
+nsMultiplexInputStream::IsSeekable() const
+{
+  return mStreams.Length() == mSeekableStreams;
+}
+
+bool
+nsMultiplexInputStream::IsIPCSerializable() const
+{
+  return mStreams.Length() == mIPCSerializableStreams;
+}
 
-    if (mIsCloneable) {
-      nsCOMPtr<nsICloneableInputStream> substream = do_QueryInterface(mStreams[i]);
-      if (!substream) {
-        mIsCloneable = false;
-      }
-    }
+bool
+nsMultiplexInputStream::IsCloneable() const
+{
+  return mStreams.Length() == mCloneableStreams;
+}
 
-    if (!mIsAsyncInputStream) {
-      // nsMultiplexInputStream is nsIAsyncInputStream if at least 1 of the
-      // substream implements that interface.
-      nsCOMPtr<nsIAsyncInputStream> substream = do_QueryInterface(mStreams[i]);
-      if (substream) {
-        mIsAsyncInputStream = true;
-      }
-    }
-  }
+bool
+nsMultiplexInputStream::IsAsyncInputStream() const
+{
+  // nsMultiplexInputStream is nsIAsyncInputStream if at least 1 of the
+  // substream implements that interface.
+  return !!mAsyncInputStreams;
 }
--- a/xpcom/threads/LabeledEventQueue.cpp
+++ b/xpcom/threads/LabeledEventQueue.cpp
@@ -1,19 +1,23 @@
 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set ts=8 sts=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 "LabeledEventQueue.h"
+#include "mozilla/dom/TabChild.h"
+#include "mozilla/dom/TabGroup.h"
 #include "mozilla/Scheduler.h"
 #include "mozilla/SchedulerGroup.h"
 #include "nsQueryObject.h"
 
+using namespace mozilla::dom;
+
 LabeledEventQueue::LabeledEventQueue()
 {
 }
 
 static SchedulerGroup*
 GetSchedulerGroup(nsIRunnable* aEvent)
 {
   RefPtr<SchedulerGroup::Runnable> groupRunnable = do_QueryObject(aEvent);
@@ -84,16 +88,20 @@ LabeledEventQueue::PutEvent(already_AddR
     }
   }
 
   mNumEvents++;
   epoch->mNumEvents++;
 
   RunnableEpochQueue* queue = isLabeled ? mLabeled.LookupOrAdd(group) : &mUnlabeled;
   queue->Push(QueueEntry(event.forget(), epoch->mEpochNumber));
+
+  if (group && !group->isInList()) {
+    mSchedulerGroups.insertBack(group);
+  }
 }
 
 void
 LabeledEventQueue::PopEpoch()
 {
   Epoch& epoch = mEpochs.FirstElement();
   MOZ_ASSERT(epoch.mNumEvents > 0);
   if (epoch.mNumEvents == 1) {
@@ -120,36 +128,66 @@ LabeledEventQueue::GetEvent(EventPriorit
       PopEpoch();
       mUnlabeled.Pop();
       MOZ_ASSERT(entry.mEpochNumber == epoch.mEpochNumber);
       MOZ_ASSERT(entry.mRunnable.get());
       return entry.mRunnable.forget();
     }
   }
 
-  for (auto iter = mLabeled.Iter(); !iter.Done(); iter.Next()) {
-    SchedulerGroup* key = iter.Key();
-    RunnableEpochQueue* queue = iter.Data();
+  // Move active tabs to the front of the queue. The mAvoidActiveTabCount field
+  // prevents us from preferentially processing events from active tabs twice in
+  // a row. This scheme is designed to prevent starvation.
+  if (TabChild::HasActiveTabs() && mAvoidActiveTabCount <= 0) {
+    for (TabChild* tabChild : TabChild::GetActiveTabs()) {
+      SchedulerGroup* group = tabChild->TabGroup();
+      if (!group->isInList() || group == mSchedulerGroups.getFirst()) {
+        continue;
+      }
+
+      // For each active tab we move to the front of the queue, we have to
+      // process two SchedulerGroups (the active tab and another one, presumably
+      // a background group) before we prioritize active tabs again.
+      mAvoidActiveTabCount += 2;
+
+      group->removeFrom(mSchedulerGroups);
+      mSchedulerGroups.insertFront(group);
+    }
+  }
+
+  // Iterate over SchedulerGroups in order. Each time we pass by a
+  // SchedulerGroup, we move it to the back of the list. This ensures that we
+  // process SchedulerGroups in a round-robin order (ignoring active tab
+  // prioritization).
+  SchedulerGroup* firstGroup = mSchedulerGroups.getFirst();
+  SchedulerGroup* group = firstGroup;
+  do {
+    RunnableEpochQueue* queue = mLabeled.Get(group);
+    MOZ_ASSERT(queue);
     MOZ_ASSERT(!queue->IsEmpty());
 
+    mAvoidActiveTabCount--;
+    SchedulerGroup* next = group->removeAndGetNext();
+    mSchedulerGroups.insertBack(group);
+
     QueueEntry entry = queue->FirstElement();
-    if (entry.mEpochNumber != epoch.mEpochNumber) {
-      continue;
-    }
-
-    if (IsReadyToRun(entry.mRunnable, key)) {
+    if (entry.mEpochNumber == epoch.mEpochNumber &&
+        IsReadyToRun(entry.mRunnable, group)) {
       PopEpoch();
 
       queue->Pop();
       if (queue->IsEmpty()) {
-        iter.Remove();
+        mLabeled.Remove(group);
+        group->removeFrom(mSchedulerGroups);
       }
       return entry.mRunnable.forget();
     }
-  }
+
+    group = next;
+  } while (group != firstGroup);
 
   return nullptr;
 }
 
 bool
 LabeledEventQueue::IsEmpty(const MutexAutoLock& aProofOfLock)
 {
   return mEpochs.IsEmpty();
--- a/xpcom/threads/LabeledEventQueue.h
+++ b/xpcom/threads/LabeledEventQueue.h
@@ -2,16 +2,17 @@
 /* vim: set ts=8 sts=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/. */
 
 #ifndef mozilla_LabeledEventQueue_h
 #define mozilla_LabeledEventQueue_h
 
+#include <stdint.h>
 #include "mozilla/AbstractEventQueue.h"
 #include "mozilla/Queue.h"
 #include "nsClassHashtable.h"
 #include "nsHashKeys.h"
 
 namespace mozilla {
 
 class SchedulerGroup;
@@ -126,17 +127,26 @@ private:
   };
 
   void PopEpoch();
 
   using RunnableEpochQueue = Queue<QueueEntry, 32>;
   using LabeledMap = nsClassHashtable<nsRefPtrHashKey<SchedulerGroup>, RunnableEpochQueue>;
   using EpochQueue = Queue<Epoch, 8>;
 
+  // List of SchedulerGroups that have events in the queue.
+  LinkedList<SchedulerGroup> mSchedulerGroups;
+
   LabeledMap mLabeled;
   RunnableEpochQueue mUnlabeled;
   EpochQueue mEpochs;
   size_t mNumEvents = 0;
+
+  // Number of SchedulerGroups that must be processed before we prioritize an
+  // active tab. This field is designed to guarantee a 1:1 interleaving between
+  // foreground and background SchedulerGroups. For details, see its usage in
+  // LabeledEventQueue.cpp.
+  int64_t mAvoidActiveTabCount = 0;
 };
 
 } // namespace mozilla
 
 #endif // mozilla_LabeledEventQueue_h
new file mode 100644
--- /dev/null
+++ b/xpcom/threads/MainThreadQueue.h
@@ -0,0 +1,53 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=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/. */
+
+#ifndef mozilla_MainThreadQueue_h
+#define mozilla_MainThreadQueue_h
+
+#include "mozilla/Attributes.h"
+#include "mozilla/EventQueue.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/UniquePtr.h"
+#include "nsThread.h"
+#include "PrioritizedEventQueue.h"
+
+namespace mozilla {
+
+template<typename SynchronizedQueueT, typename InnerQueueT>
+inline already_AddRefed<nsThread>
+CreateMainThread(nsIIdlePeriod* aIdlePeriod, SynchronizedQueueT** aSynchronizedQueue = nullptr)
+{
+  using MainThreadQueueT = PrioritizedEventQueue<InnerQueueT>;
+
+  auto queue = MakeUnique<MainThreadQueueT>(
+    MakeUnique<InnerQueueT>(),
+    MakeUnique<InnerQueueT>(),
+    MakeUnique<InnerQueueT>(),
+    MakeUnique<InnerQueueT>(),
+    do_AddRef(aIdlePeriod));
+
+  MainThreadQueueT* prioritized = queue.get();
+
+  RefPtr<SynchronizedQueueT> synchronizedQueue = new SynchronizedQueueT(Move(queue));
+
+  prioritized->SetMutexRef(synchronizedQueue->MutexRef());
+
+  // Setup "main" thread
+  RefPtr<nsThread> mainThread = new nsThread(WrapNotNull(synchronizedQueue), nsThread::MAIN_THREAD, 0);
+
+#ifndef RELEASE_OR_BETA
+  prioritized->SetNextIdleDeadlineRef(mainThread->NextIdleDeadlineRef());
+#endif
+
+  if (aSynchronizedQueue) {
+    synchronizedQueue.forget(aSynchronizedQueue);
+  }
+  return mainThread.forget();
+}
+
+} // namespace mozilla
+
+#endif // mozilla_MainThreadQueue_h
--- a/xpcom/threads/PrioritizedEventQueue.cpp
+++ b/xpcom/threads/PrioritizedEventQueue.cpp
@@ -337,9 +337,10 @@ PrioritizedEventQueue<InnerQueueT>::
 ResumeInputEventPrioritization(const MutexAutoLock& aProofOfLock)
 {
   MOZ_ASSERT(mInputQueueState == STATE_SUSPEND);
   mInputQueueState = STATE_ENABLED;
 }
 
 namespace mozilla {
 template class PrioritizedEventQueue<EventQueue>;
+template class PrioritizedEventQueue<LabeledEventQueue>;
 }
--- a/xpcom/threads/PrioritizedEventQueue.h
+++ b/xpcom/threads/PrioritizedEventQueue.h
@@ -120,12 +120,13 @@ private:
     STATE_SUSPEND,
     STATE_ENABLED
   };
   InputEventQueueState mInputQueueState = STATE_DISABLED;
 };
 
 class EventQueue;
 extern template class PrioritizedEventQueue<EventQueue>;
+extern template class PrioritizedEventQueue<LabeledEventQueue>;
 
 } // namespace mozilla
 
 #endif // mozilla_PrioritizedEventQueue_h
--- a/xpcom/threads/Scheduler.cpp
+++ b/xpcom/threads/Scheduler.cpp
@@ -4,16 +4,17 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "Scheduler.h"
 
 #include "jsfriendapi.h"
 #include "LabeledEventQueue.h"
 #include "LeakRefPtr.h"
+#include "MainThreadQueue.h"
 #include "mozilla/CooperativeThreadPool.h"
 #include "mozilla/dom/ScriptSettings.h"
 #include "mozilla/ipc/BackgroundChild.h"
 #include "mozilla/SchedulerGroup.h"
 #include "nsCycleCollector.h"
 #include "nsIThread.h"
 #include "nsPrintfCString.h"
 #include "nsThread.h"
@@ -86,19 +87,16 @@ private:
 
 using mozilla::detail::SchedulerEventQueue;
 
 class mozilla::SchedulerImpl
 {
 public:
   explicit SchedulerImpl(SchedulerEventQueue* aQueue);
 
-  static already_AddRefed<SchedulerEventQueue>
-  CreateQueue(nsIIdlePeriod* aIdlePeriod, nsThread** aThread);
-
   void Start();
   void Shutdown();
 
   void Dispatch(already_AddRefed<nsIRunnable> aEvent);
 
   void Yield();
 
   static void EnterNestedEventLoop(Scheduler::EventLoopActivation& aOuterActivation);
@@ -437,59 +435,16 @@ SchedulerImpl::Switcher()
 }
 
 /* static */ void
 SchedulerImpl::SwitcherThread(void* aData)
 {
   static_cast<SchedulerImpl*>(aData)->Switcher();
 }
 
-/* static */ already_AddRefed<SchedulerEventQueue>
-SchedulerImpl::CreateQueue(nsIIdlePeriod* aIdlePeriod, nsThread** aThread)
-{
-  UniquePtr<AbstractEventQueue> queue;
-  RefPtr<nsThread> mainThread;
-
-  if (sPrefUseMultipleQueues) {
-    using MainThreadQueueT = PrioritizedEventQueue<LabeledEventQueue>;
-
-    queue = MakeUnique<MainThreadQueueT>(
-      MakeUnique<LabeledEventQueue>(),
-      MakeUnique<LabeledEventQueue>(),
-      MakeUnique<LabeledEventQueue>(),
-      MakeUnique<LabeledEventQueue>(),
-      do_AddRef(aIdlePeriod));
-  } else {
-    using MainThreadQueueT = PrioritizedEventQueue<EventQueue>;
-
-    queue = MakeUnique<MainThreadQueueT>(
-      MakeUnique<EventQueue>(),
-      MakeUnique<EventQueue>(),
-      MakeUnique<EventQueue>(),
-      MakeUnique<EventQueue>(),
-      do_AddRef(aIdlePeriod));
-  }
-
-  auto prioritized = static_cast<PrioritizedEventQueue<AbstractEventQueue>*>(queue.get());
-
-  RefPtr<SchedulerEventQueue> synchronizedQueue = new SchedulerEventQueue(Move(queue));
-
-  prioritized->SetMutexRef(synchronizedQueue->MutexRef());
-
-  // Setup "main" thread
-  mainThread = new nsThread(WrapNotNull(synchronizedQueue), nsThread::MAIN_THREAD, 0);
-
-#ifndef RELEASE_OR_BETA
-  prioritized->SetNextIdleDeadlineRef(mainThread->NextIdleDeadlineRef());
-#endif
-
-  mainThread.forget(aThread);
-  return synchronizedQueue.forget();
-}
-
 void
 SchedulerImpl::Start()
 {
   NS_DispatchToMainThread(NS_NewRunnableFunction("Scheduler::Start", [this]() -> void {
     // Let's pretend the runnable here isn't actually running.
     MOZ_ASSERT(sUnlabeledEventRunning);
     sUnlabeledEventRunning = false;
     MOZ_ASSERT(sNumThreadsRunning == 1);
@@ -733,18 +688,24 @@ SchedulerImpl::Yield()
   CooperativeThreadPool::Yield(nullptr, lock);
 }
 
 /* static */ already_AddRefed<nsThread>
 Scheduler::Init(nsIIdlePeriod* aIdlePeriod)
 {
   MOZ_ASSERT(!sScheduler);
 
+  RefPtr<SchedulerEventQueue> queue;
   RefPtr<nsThread> mainThread;
-  RefPtr<SchedulerEventQueue> queue = SchedulerImpl::CreateQueue(aIdlePeriod, getter_AddRefs(mainThread));
+  if (Scheduler::UseMultipleQueues()) {
+    mainThread = CreateMainThread<SchedulerEventQueue, LabeledEventQueue>(aIdlePeriod, getter_AddRefs(queue));
+  } else {
+    mainThread = CreateMainThread<SchedulerEventQueue, EventQueue>(aIdlePeriod, getter_AddRefs(queue));
+  }
+
   sScheduler = MakeUnique<SchedulerImpl>(queue);
   return mainThread.forget();
 }
 
 /* static */ void
 Scheduler::Start()
 {
   sScheduler->Start();
@@ -802,16 +763,22 @@ Scheduler::SetPrefs(const char* aPrefs)
 
 /* static */ bool
 Scheduler::IsSchedulerEnabled()
 {
   return SchedulerImpl::sPrefScheduler;
 }
 
 /* static */ bool
+Scheduler::UseMultipleQueues()
+{
+  return SchedulerImpl::sPrefUseMultipleQueues;
+}
+
+/* static */ bool
 Scheduler::IsCooperativeThread()
 {
   return CooperativeThreadPool::IsCooperativeThread();
 }
 
 /* static */ void
 Scheduler::Yield()
 {
--- a/xpcom/threads/Scheduler.h
+++ b/xpcom/threads/Scheduler.h
@@ -57,16 +57,17 @@ public:
   static void Shutdown();
 
   // Scheduler prefs need to be handled differently because the scheduler needs
   // to start up in the content process before the normal preferences service.
   static nsCString GetPrefs();
   static void SetPrefs(const char* aPrefs);
 
   static bool IsSchedulerEnabled();
+  static bool UseMultipleQueues();
 
   static bool IsCooperativeThread();
 
   static void Yield();
 
   static bool UnlabeledEventRunning();
   static bool AnyEventRunning();
 
--- a/xpcom/threads/SchedulerGroup.h
+++ b/xpcom/threads/SchedulerGroup.h
@@ -3,16 +3,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef mozilla_SchedulerGroup_h
 #define mozilla_SchedulerGroup_h
 
 #include "mozilla/AlreadyAddRefed.h"
+#include "mozilla/LinkedList.h"
 #include "mozilla/TaskCategory.h"
 #include "mozilla/ThreadLocal.h"
 #include "mozilla/TimeStamp.h"
 #include "nsCOMPtr.h"
 #include "nsILabelableRunnable.h"
 #include "nsISupportsImpl.h"
 #include "nsThreadUtils.h"
 
@@ -35,17 +36,17 @@ class TabGroup;
 // (with roughly one group per tab). Runnables will be annotated with the set of
 // groups that they touch. Two runnables may run concurrently on different
 // fibers as long as they touch different groups.
 //
 // A SchedulerGroup is an abstract class to represent a "group". Essentially the
 // only functionality offered by a SchedulerGroup is the ability to dispatch
 // runnables to the group. TabGroup, DocGroup, and SystemGroup are the concrete
 // implementations of SchedulerGroup.
-class SchedulerGroup
+class SchedulerGroup : public LinkedListElement<SchedulerGroup>
 {
 public:
   SchedulerGroup();
 
   NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING
 
   // This method returns true if all members of the "group" are in a
   // "background" state.
--- a/xpcom/threads/ThreadEventQueue.cpp
+++ b/xpcom/threads/ThreadEventQueue.cpp
@@ -264,9 +264,10 @@ ThreadEventQueue<InnerQueueT>::SetObserv
 {
   MutexAutoLock lock(mLock);
   mObserver = aObserver;
 }
 
 namespace mozilla {
 template class ThreadEventQueue<EventQueue>;
 template class ThreadEventQueue<PrioritizedEventQueue<EventQueue>>;
+template class ThreadEventQueue<PrioritizedEventQueue<LabeledEventQueue>>;
 }
--- a/xpcom/threads/ThreadEventQueue.h
+++ b/xpcom/threads/ThreadEventQueue.h
@@ -19,16 +19,18 @@ class nsIThreadObserver;
 
 namespace mozilla {
 
 class EventQueue;
 
 template<typename InnerQueueT>
 class PrioritizedEventQueue;
 
+class LabeledEventQueue;
+
 class ThreadEventTarget;
 
 // A ThreadEventQueue implements normal monitor-style synchronization over the
 // InnerQueueT AbstractEventQueue. It also implements PushEventQueue and
 // PopEventQueue for workers (see the documentation below for an explanation of
 // those). All threads use a ThreadEventQueue as their event queue. InnerQueueT
 // is a template parameter to avoid virtual dispatch overhead.
 template<class InnerQueueT>
@@ -110,12 +112,13 @@ private:
   CondVar mEventsAvailable;
 
   bool mEventsAreDoomed = false;
   nsCOMPtr<nsIThreadObserver> mObserver;
 };
 
 extern template class ThreadEventQueue<EventQueue>;
 extern template class ThreadEventQueue<PrioritizedEventQueue<EventQueue>>;
+extern template class ThreadEventQueue<PrioritizedEventQueue<LabeledEventQueue>>;
 
 }; // namespace mozilla
 
 #endif // mozilla_ThreadEventQueue_h
--- a/xpcom/threads/nsThreadManager.cpp
+++ b/xpcom/threads/nsThreadManager.cpp
@@ -5,16 +5,18 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "nsThreadManager.h"
 #include "nsThread.h"
 #include "nsThreadUtils.h"
 #include "nsIClassInfoImpl.h"
 #include "nsTArray.h"
 #include "nsAutoPtr.h"
+#include "LabeledEventQueue.h"
+#include "MainThreadQueue.h"
 #include "mozilla/AbstractThread.h"
 #include "mozilla/EventQueue.h"
 #include "mozilla/Preferences.h"
 #include "mozilla/Scheduler.h"
 #include "mozilla/SystemGroup.h"
 #include "mozilla/ThreadEventQueue.h"
 #include "mozilla/ThreadLocal.h"
 #include "PrioritizedEventQueue.h"
@@ -129,37 +131,21 @@ nsThreadManager::Init()
 
   nsCOMPtr<nsIIdlePeriod> idlePeriod = new MainThreadIdlePeriod();
 
   bool startScheduler = false;
   if (XRE_IsContentProcess() && Scheduler::IsSchedulerEnabled()) {
     mMainThread = Scheduler::Init(idlePeriod);
     startScheduler = true;
   } else {
-    using MainThreadQueueT = PrioritizedEventQueue<EventQueue>;
-
-    auto prioritized = MakeUnique<MainThreadQueueT>(MakeUnique<EventQueue>(),
-                                                    MakeUnique<EventQueue>(),
-                                                    MakeUnique<EventQueue>(),
-                                                    MakeUnique<EventQueue>(),
-                                                    idlePeriod.forget());
-
-    // Save a copy temporarily so we can set some state on it.
-    MainThreadQueueT* prioritizedRef = prioritized.get();
-    RefPtr<ThreadEventQueue<MainThreadQueueT>> queue =
-      new ThreadEventQueue<MainThreadQueueT>(Move(prioritized));
-
-    prioritizedRef->SetMutexRef(queue->MutexRef());
-
-    // Setup "main" thread
-    mMainThread = new nsThread(WrapNotNull(queue), nsThread::MAIN_THREAD, 0);
-
-#ifndef RELEASE_OR_BETA
-    prioritizedRef->SetNextIdleDeadlineRef(mMainThread->NextIdleDeadlineRef());
-#endif
+    if (XRE_IsContentProcess() && Scheduler::UseMultipleQueues()) {
+      mMainThread = CreateMainThread<ThreadEventQueue<PrioritizedEventQueue<LabeledEventQueue>>, LabeledEventQueue>(idlePeriod);
+    } else {
+      mMainThread = CreateMainThread<ThreadEventQueue<PrioritizedEventQueue<EventQueue>>, EventQueue>(idlePeriod);
+    }
   }
 
   nsresult rv = mMainThread->InitCurrentThread();
   if (NS_FAILED(rv)) {
     mMainThread = nullptr;
     return rv;
   }