Bug 1360406 - Remove context menu sync IPC draft
authorPerry Jiang <jiangperry@gmail.com>
Mon, 21 Aug 2017 15:42:37 -0700
changeset 660907 7567fff8f7a8a5c30ccdc42024b805c684543f7b
parent 660845 3c96d611ebd67fc219d22bcb476a72412c76f6c7
child 730431 14ca60c91b240c5b5f3f4379aca2941c668e2626
push id78607
push userbmo:jiangperry@gmail.com
push dateThu, 07 Sep 2017 20:54:31 +0000
bugs1360406
milestone57.0a1
Bug 1360406 - Remove context menu sync IPC MozReview-Commit-ID: 1dah3n5K7cb
browser/base/content/content.js
browser/base/content/nsContextMenu.js
browser/base/content/pageinfo/pageInfo.js
browser/base/content/test/general/browser_addKeywordSearch.js
browser/base/content/test/general/browser_contextmenu.js
browser/base/content/test/pageinfo/browser_pageinfo_image_info.js
browser/modules/ContextMenu.jsm
browser/modules/PluginContent.jsm
browser/modules/moz.build
--- a/browser/base/content/content.js
+++ b/browser/base/content/content.js
@@ -9,57 +9,45 @@
 /* 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",
   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) {
@@ -72,154 +60,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;
@@ -232,39 +82,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 "";
   }
@@ -805,175 +632,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) });
 });
 
@@ -1023,66 +707,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;
@@ -1095,43 +729,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/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,
--- 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");
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/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
@@ -130,16 +130,17 @@ EXTRA_JS_MODULES += [
     'BrowserUsageTelemetry.jsm',
     'CastingApps.jsm',
     'ContentClick.jsm',
     'ContentCrashHandlers.jsm',
     'ContentLinkHandler.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',