browser/actors/PluginChild.jsm
author Emilio Cobos Álvarez <emilio@crisal.io>
Mon, 18 Nov 2019 19:06:50 +0000
changeset 502471 5bc4fbe48df7276c4b6da448e9b85953620ac913
parent 502470 85408aaba4a32de2ecf495093f24643e93154292
permissions -rw-r--r--
Bug 1596800 - Remove document.getBindingParent usage from PluginChild.jsm. r=mconley It wants to get the containing shadow host of the target to get to the plugin. Do that explicitly. Depends on D53337 Differential Revision: https://phabricator.services.mozilla.com/D53338

/* 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";

var EXPORTED_SYMBOLS = ["PluginChild"];

const { AppConstants } = ChromeUtils.import(
  "resource://gre/modules/AppConstants.jsm"
);
const { BrowserUtils } = ChromeUtils.import(
  "resource://gre/modules/BrowserUtils.jsm"
);
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { XPCOMUtils } = ChromeUtils.import(
  "resource://gre/modules/XPCOMUtils.jsm"
);

XPCOMUtils.defineLazyGetter(this, "gNavigatorBundle", function() {
  const url = "chrome://browser/locale/browser.properties";
  return Services.strings.createBundle(url);
});

XPCOMUtils.defineLazyServiceGetter(
  this,
  "gPluginHost",
  "@mozilla.org/plugin/host;1",
  "nsIPluginHost"
);

const OVERLAY_DISPLAY = {
  HIDDEN: 0, // The overlay will be transparent
  BLANK: 1, // The overlay will be just a grey box
  TINY: 2, // The overlay with a 16x16 plugin icon
  REDUCED: 3, // The overlay with a 32x32 plugin icon
  NOTEXT: 4, // The overlay with a 48x48 plugin icon and the close button
  FULL: 5, // The full overlay: 48x48 plugin icon, close button and label
};

// This gets sent through the content process message manager because the parent
// can't know exactly which child needs to hear about the progress of the
// submission, so we listen "manually" on the CPMM instead of through the actor
// definition.
const kSubmitMsg = "PluginParent:NPAPIPluginCrashReportSubmitted";

class PluginChild extends JSWindowActorChild {
  constructor() {
    super();

    // Cache of plugin crash information sent from the parent
    this.pluginCrashData = new Map();
    Services.cpmm.addMessageListener(kSubmitMsg, this);
  }

  willDestroy() {
    Services.cpmm.removeMessageListener(kSubmitMsg, this);
    if (this._addedListeners) {
      this.contentWindow.removeEventListener("pagehide", this, {
        capture: true,
        mozSystemGroup: true,
      });
      this.contentWindow.removeEventListener("pageshow", this, {
        capture: true,
        mozSystemGroup: true,
      });
    }
  }

  receiveMessage(msg) {
    switch (msg.name) {
      case "PluginParent:ActivatePlugins":
        this.activatePlugins(msg.data.activationInfo, msg.data.newState);
        break;
      case "PluginParent:NPAPIPluginCrashReportSubmitted":
        this.NPAPIPluginCrashReportSubmitted({
          runID: msg.data.runID,
          state: msg.data.state,
        });
        break;
      case "PluginParent:Test:ClearCrashData":
        // This message should ONLY ever be sent by automated tests.
        if (Services.prefs.getBoolPref("plugins.testmode")) {
          this.pluginCrashData.clear();
        }
    }
  }

  observe(aSubject, aTopic, aData) {
    switch (aTopic) {
      case "decoder-doctor-notification":
        let data = JSON.parse(aData);
        let type = data.type.toLowerCase();
        if (
          type == "cannot-play" &&
          this.haveShownNotification &&
          aSubject.top.document == this.document &&
          data.formats.toLowerCase().includes("application/x-mpegurl", 0)
        ) {
          this.contentWindow.pluginRequiresReload = true;
        }
    }
  }

  onPageShow(event) {
    // Ignore events that aren't from the main document.
    if (!this.contentWindow || event.target != this.document) {
      return;
    }

    // The PluginClickToPlay events are not fired when navigating using the
    // BF cache. |persisted| is true when the page is loaded from the
    // BF cache, so this code reshows the notification if necessary.
    if (event.persisted) {
      this.reshowClickToPlayNotification();
    }
  }

  onPageHide(event) {
    // Ignore events that aren't from the main document.
    if (!this.contentWindow || event.target != this.document) {
      return;
    }

    this.clearPluginCaches();
    this.haveShownNotification = false;
  }

  getPluginUI(pluginElement, anonid) {
    if (
      pluginElement.openOrClosedShadowRoot &&
      pluginElement.openOrClosedShadowRoot.isUAWidget()
    ) {
      return pluginElement.openOrClosedShadowRoot.getElementById(anonid);
    }
    return null;
  }

  _getPluginInfo(pluginElement) {
    if (this.isKnownPlugin(pluginElement)) {
      let pluginTag = gPluginHost.getPluginTagForType(pluginElement.actualType);
      let pluginName = BrowserUtils.makeNicePluginName(pluginTag.name);
      let fallbackType = pluginElement.defaultFallbackType;
      let permissionString = gPluginHost.getPermissionStringForType(
        pluginElement.actualType
      );
      return { pluginTag, pluginName, fallbackType, permissionString };
    }
    return {
      fallbackType: null,
      permissionString: null,
      pluginName: gNavigatorBundle.GetStringFromName(
        "pluginInfo.unknownPlugin"
      ),
      pluginTag: null,
    };
  }

  /**
   * _getPluginInfoForTag is called when iterating the plugins for a document,
   * and what we get from nsIDOMWindowUtils is an nsIPluginTag, and not an
   * nsIObjectLoadingContent. This only should happen if the plugin is
   * click-to-play (see bug 1186948).
   */
  _getPluginInfoForTag(pluginTag) {
    // Since we should only have entered _getPluginInfoForTag when
    // examining a click-to-play plugin, we can safely hard-code
    // this fallback type, since we don't actually have an
    // nsIObjectLoadingContent to check.
    let fallbackType = Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY;
    if (pluginTag) {
      let pluginName = BrowserUtils.makeNicePluginName(pluginTag.name);
      let permissionString = gPluginHost.getPermissionStringForTag(pluginTag);
      return { pluginTag, pluginName, permissionString, fallbackType };
    }
    return {
      fallbackType,
      permissionString: null,
      pluginName: gNavigatorBundle.GetStringFromName(
        "pluginInfo.unknownPlugin"
      ),
      pluginTag: null,
    };
  }

  /**
   * Update the visibility of the plugin overlay.
   */
  setVisibility(plugin, overlay, overlayDisplayState) {
    overlay.classList.toggle(
      "visible",
      overlayDisplayState != OVERLAY_DISPLAY.HIDDEN
    );
    if (overlayDisplayState != OVERLAY_DISPLAY.HIDDEN) {
      overlay.removeAttribute("dismissed");
    }
  }

  /**
   * Adjust the style in which the overlay will be displayed. It might be adjusted
   * based on its size, or if there's some other element covering all corners of
   * the overlay.
   *
   * This function will handle adjusting the style of the overlay, but will
   * not handle hiding it. That is done by setVisibility with the return value
   * from this function.
   *
   * @param {Element} plugin  The plug-in element
   * @param {Element} overlay The overlay element inside the UA Shadow DOM of
   *                          the plug-in element
   * @param {boolean} flushLayout Allow flush layout during computation and
   *                              adjustment.
   * @returns A value from OVERLAY_DISPLAY.
   */
  computeAndAdjustOverlayDisplay(plugin, overlay, flushLayout) {
    let fallbackType = plugin.pluginFallbackType;
    if (plugin.pluginFallbackTypeOverride !== undefined) {
      fallbackType = plugin.pluginFallbackTypeOverride;
    }
    if (fallbackType == Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY_QUIET) {
      return OVERLAY_DISPLAY.HIDDEN;
    }

    // If the overlay size is 0, we haven't done layout yet. Presume that
    // plugins are visible until we know otherwise.
    if (flushLayout && overlay.scrollWidth == 0) {
      return OVERLAY_DISPLAY.FULL;
    }

    let overlayDisplay = OVERLAY_DISPLAY.FULL;
    let cwu = plugin.ownerGlobal.windowUtils;

    // Is the <object>'s size too small to hold what we want to show?
    let pluginRect = flushLayout
      ? plugin.getBoundingClientRect()
      : cwu.getBoundsWithoutFlushing(plugin);
    let pluginWidth = Math.ceil(pluginRect.width);
    let pluginHeight = Math.ceil(pluginRect.height);

    let layoutNeedsFlush =
      !flushLayout &&
      cwu.needsFlush(cwu.FLUSH_STYLE) &&
      cwu.needsFlush(cwu.FLUSH_LAYOUT);

    // We must set the attributes while here inside this function in order
    // for a possible re-style to occur, which will make the scrollWidth/Height
    // checks below correct. Otherwise, we would be requesting e.g. a TINY
    // overlay here, but the default styling would be used, and that would make
    // it overflow, causing it to change to BLANK instead of remaining as TINY.

    if (layoutNeedsFlush) {
      // Set the content to be oversized when we the overlay size is 0,
      // so that we could receive an overflow event afterwards when there is
      // a layout.
      overlayDisplay = OVERLAY_DISPLAY.FULL;
      overlay.setAttribute("sizing", "oversized");
      overlay.removeAttribute("notext");
    } else if (pluginWidth <= 32 || pluginHeight <= 32) {
      overlay.setAttribute("sizing", "blank");
      overlayDisplay = OVERLAY_DISPLAY.BLANK;
    } else if (pluginWidth <= 80 || pluginHeight <= 60) {
      overlayDisplay = OVERLAY_DISPLAY.TINY;
      overlay.setAttribute("sizing", "tiny");
      overlay.setAttribute("notext", "notext");
    } else if (pluginWidth <= 120 || pluginHeight <= 80) {
      overlayDisplay = OVERLAY_DISPLAY.REDUCED;
      overlay.setAttribute("sizing", "reduced");
      overlay.setAttribute("notext", "notext");
    } else if (pluginWidth <= 240 || pluginHeight <= 160) {
      overlayDisplay = OVERLAY_DISPLAY.NOTEXT;
      overlay.removeAttribute("sizing");
      overlay.setAttribute("notext", "notext");
    } else {
      overlayDisplay = OVERLAY_DISPLAY.FULL;
      overlay.removeAttribute("sizing");
      overlay.removeAttribute("notext");
    }

    // The hit test below only works with correct layout information,
    // don't do it if layout needs flush.
    // We also don't want to access scrollWidth/scrollHeight if
    // the layout needs flush.
    if (layoutNeedsFlush) {
      return overlayDisplay;
    }

    // XXX bug 446693. The text-shadow on the submitted-report text at
    //     the bottom causes scrollHeight to be larger than it should be.
    let overflows =
      overlay.scrollWidth > pluginWidth ||
      overlay.scrollHeight - 5 > pluginHeight;
    if (overflows) {
      overlay.setAttribute("sizing", "blank");
      return OVERLAY_DISPLAY.BLANK;
    }

    // Is the plugin covered up by other content so that it is not clickable?
    // Floating point can confuse .elementFromPoint, so inset just a bit
    let left = pluginRect.left + 2;
    let right = pluginRect.right - 2;
    let top = pluginRect.top + 2;
    let bottom = pluginRect.bottom - 2;
    let centerX = left + (right - left) / 2;
    let centerY = top + (bottom - top) / 2;
    let points = [
      [left, top],
      [left, bottom],
      [right, top],
      [right, bottom],
      [centerX, centerY],
    ];

    for (let [x, y] of points) {
      if (x < 0 || y < 0) {
        continue;
      }
      let el = cwu.elementFromPoint(x, y, true, true);
      if (el === plugin) {
        return overlayDisplay;
      }
    }

    overlay.setAttribute("sizing", "blank");
    return OVERLAY_DISPLAY.BLANK;
  }

  addLinkClickCallback(linkNode, callbackName /* callbackArgs...*/) {
    // XXX just doing (callback)(arg) was giving a same-origin error. bug?
    let self = this;
    let callbackArgs = Array.prototype.slice.call(arguments).slice(2);
    linkNode.addEventListener(
      "click",
      function(evt) {
        if (!evt.isTrusted) {
          return;
        }
        evt.preventDefault();
        if (!callbackArgs.length) {
          callbackArgs = [evt];
        }
        self[callbackName].apply(self, callbackArgs);
      },
      true
    );

    linkNode.addEventListener(
      "keydown",
      function(evt) {
        if (!evt.isTrusted) {
          return;
        }
        if (evt.keyCode == evt.DOM_VK_RETURN) {
          evt.preventDefault();
          if (!callbackArgs.length) {
            callbackArgs = [evt];
          }
          evt.preventDefault();
          self[callbackName].apply(self, callbackArgs);
        }
      },
      true
    );
  }

  // Helper to get the binding handler type from a plugin object
  _getBindingType(plugin) {
    if (!(plugin instanceof Ci.nsIObjectLoadingContent)) {
      return null;
    }

    switch (plugin.pluginFallbackType) {
      case Ci.nsIObjectLoadingContent.PLUGIN_UNSUPPORTED:
        return "PluginNotFound";
      case Ci.nsIObjectLoadingContent.PLUGIN_DISABLED:
        return "PluginDisabled";
      case Ci.nsIObjectLoadingContent.PLUGIN_BLOCKLISTED:
        return "PluginBlocklisted";
      case Ci.nsIObjectLoadingContent.PLUGIN_OUTDATED:
        return "PluginOutdated";
      case Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY:
      case Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY_QUIET:
        return "PluginClickToPlay";
      case Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_UPDATABLE:
        return "PluginVulnerableUpdatable";
      case Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_NO_UPDATE:
        return "PluginVulnerableNoUpdate";
      default:
        // Not all states map to a handler
        return null;
    }
  }

  handleEvent(event) {
    // Ignore events for other frames.
    let eventDoc = event.target.ownerDocument || event.target.document;
    if (eventDoc && eventDoc != this.document) {
      return;
    }
    if (!this._addedListeners) {
      // Only add pageshow/pagehide listeners here. We don't want this actor
      // to be instantiated for every frame, we only care if a plugin actually
      // gets used in a frame. So we don't add these listeners in the actor
      // specification, but only at runtime once one of our Plugin* events
      // fires.
      this.contentWindow.addEventListener("pagehide", this, {
        capture: true,
        mozSystemGroup: true,
      });
      this.contentWindow.addEventListener("pageshow", this, {
        capture: true,
        mozSystemGroup: true,
      });
      this._addedListeners = true;
    }

    let eventType = event.type;

    if (eventType == "pagehide") {
      this.onPageHide(event);
      return;
    }

    if (eventType == "pageshow") {
      this.onPageShow(event);
      return;
    }

    if (eventType == "click") {
      this.onOverlayClick(event);
      return;
    }

    if (
      eventType == "PluginCrashed" &&
      !(event.target instanceof Ci.nsIObjectLoadingContent)
    ) {
      // If the event target is not a plugin object (i.e., an <object> or
      // <embed> element), this call is for a window-global plugin.
      this.onPluginCrashed(event.target, event);
      return;
    }

    if (eventType == "HiddenPlugin") {
      let pluginTag = event.tag.QueryInterface(Ci.nsIPluginTag);
      this.showClickToPlayNotification(pluginTag, false);
    }

    let pluginElement = event.target;

    if (!(pluginElement instanceof Ci.nsIObjectLoadingContent)) {
      return;
    }

    if (eventType == "PluginBindingAttached") {
      // The plugin binding fires this event when it is created.
      // As an untrusted event, ensure that this object actually has a binding
      // and make sure we don't handle it twice
      let overlay = this.getPluginUI(pluginElement, "main");
      if (!overlay || overlay._bindingHandled) {
        return;
      }
      overlay._bindingHandled = true;

      // Lookup the handler for this binding
      eventType = this._getBindingType(pluginElement);
      if (!eventType) {
        // Not all bindings have handlers
        return;
      }
    }

    let shouldShowNotification = false;
    switch (eventType) {
      case "PluginCrashed":
        this.onPluginCrashed(pluginElement, event);
        break;

      case "PluginNotFound": {
        /* NOP */
        break;
      }

      case "PluginBlocklisted":
      case "PluginOutdated":
        shouldShowNotification = true;
        break;

      case "PluginVulnerableUpdatable":
        let updateLink = this.getPluginUI(pluginElement, "checkForUpdatesLink");
        let { pluginTag } = this._getPluginInfo(pluginElement);
        this.addLinkClickCallback(
          updateLink,
          "forwardCallback",
          "openPluginUpdatePage",
          pluginTag.id
        );

      /* FALLTHRU */
      case "PluginVulnerableNoUpdate":
      case "PluginClickToPlay":
        this._handleClickToPlayEvent(pluginElement);
        let { pluginName } = this._getPluginInfo(pluginElement);
        let messageString = gNavigatorBundle.formatStringFromName(
          "PluginClickToActivate2",
          [pluginName]
        );
        let overlayText = this.getPluginUI(pluginElement, "clickToPlay");
        overlayText.textContent = messageString;
        if (
          eventType == "PluginVulnerableUpdatable" ||
          eventType == "PluginVulnerableNoUpdate"
        ) {
          let vulnerabilityString = gNavigatorBundle.GetStringFromName(
            eventType
          );
          let vulnerabilityText = this.getPluginUI(
            pluginElement,
            "vulnerabilityStatus"
          );
          vulnerabilityText.textContent = vulnerabilityString;
        }
        shouldShowNotification = true;
        break;

      case "PluginDisabled":
        let manageLink = this.getPluginUI(pluginElement, "managePluginsLink");
        this.addLinkClickCallback(
          manageLink,
          "forwardCallback",
          "managePlugins"
        );
        shouldShowNotification = true;
        break;

      case "PluginInstantiated":
        shouldShowNotification = true;
        break;
    }

    // Show the in-content UI if it's not too big. The crashed plugin handler already did this.
    let overlay = this.getPluginUI(pluginElement, "main");
    if (eventType != "PluginCrashed") {
      if (overlay != null) {
        this.setVisibility(
          pluginElement,
          overlay,
          this.computeAndAdjustOverlayDisplay(pluginElement, overlay, false)
        );

        let resizeListener = () => {
          this.setVisibility(
            pluginElement,
            overlay,
            this.computeAndAdjustOverlayDisplay(pluginElement, overlay, true)
          );
        };
        pluginElement.addEventListener("overflow", resizeListener);
        pluginElement.addEventListener("underflow", resizeListener);
      }
    }

    let closeIcon = this.getPluginUI(pluginElement, "closeIcon");
    if (closeIcon) {
      closeIcon.addEventListener(
        "click",
        clickEvent => {
          if (clickEvent.button == 0 && clickEvent.isTrusted) {
            this.hideClickToPlayOverlay(pluginElement);
            overlay.setAttribute("dismissed", "true");
          }
        },
        true
      );
    }

    if (shouldShowNotification) {
      this.showClickToPlayNotification(pluginElement, false);
    }
  }

  isKnownPlugin(objLoadingContent) {
    return (
      objLoadingContent.getContentTypeForMIMEType(
        objLoadingContent.actualType
      ) == Ci.nsIObjectLoadingContent.TYPE_PLUGIN
    );
  }

  canActivatePlugin(objLoadingContent) {
    // if this isn't a known plugin, we can't activate it
    // (this also guards pluginHost.getPermissionStringForType against
    // unexpected input)
    if (!this.isKnownPlugin(objLoadingContent)) {
      return false;
    }

    let permissionString = gPluginHost.getPermissionStringForType(
      objLoadingContent.actualType
    );
    let principal = objLoadingContent.ownerGlobal.top.document.nodePrincipal;
    let pluginPermission = Services.perms.testPermissionFromPrincipal(
      principal,
      permissionString
    );

    let isFallbackTypeValid =
      objLoadingContent.pluginFallbackType >=
        Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY &&
      objLoadingContent.pluginFallbackType <=
        Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY_QUIET;

    return (
      !objLoadingContent.activated &&
      pluginPermission != Ci.nsIPermissionManager.DENY_ACTION &&
      isFallbackTypeValid
    );
  }

  hideClickToPlayOverlay(pluginElement) {
    let overlay = this.getPluginUI(pluginElement, "main");
    if (overlay) {
      overlay.classList.remove("visible");
    }
  }

  // Forward a link click callback to the chrome process.
  forwardCallback(name, pluginId) {
    this.sendAsyncMessage("PluginContent:LinkClickCallback", {
      name,
      pluginId,
    });
  }

  submitReport(plugin) {
    if (!AppConstants.MOZ_CRASHREPORTER) {
      return;
    }
    if (!plugin) {
      Cu.reportError(
        "Attempted to submit crash report without an associated plugin."
      );
      return;
    }
    if (!(plugin instanceof Ci.nsIObjectLoadingContent)) {
      Cu.reportError(
        "Attempted to submit crash report on plugin that does not" +
          "implement nsIObjectLoadingContent."
      );
      return;
    }

    let runID = plugin.runID;
    let submitURLOptIn = this.getPluginUI(plugin, "submitURLOptIn").checked;
    let keyVals = {};
    let userComment = this.getPluginUI(plugin, "submitComment").value.trim();
    if (userComment) {
      keyVals.PluginUserComment = userComment;
    }
    if (submitURLOptIn) {
      keyVals.PluginContentURL = plugin.ownerDocument.URL;
    }

    this.sendAsyncMessage("PluginContent:SubmitReport", {
      runID,
      keyVals,
      submitURLOptIn,
    });
  }

  reloadPage() {
    this.contentWindow.location.reload();
  }

  // Event listener for click-to-play plugins.
  _handleClickToPlayEvent(plugin) {
    let doc = plugin.ownerDocument;
    // guard against giving pluginHost.getPermissionStringForType a type
    // not associated with any known plugin
    if (!this.isKnownPlugin(plugin)) {
      return;
    }
    let permissionString = gPluginHost.getPermissionStringForType(
      plugin.actualType
    );
    let principal = doc.defaultView.top.document.nodePrincipal;
    let pluginPermission = Services.perms.testPermissionFromPrincipal(
      principal,
      permissionString
    );

    let overlay = this.getPluginUI(plugin, "main");

    if (
      pluginPermission == Ci.nsIPermissionManager.DENY_ACTION ||
      pluginPermission ==
        Ci.nsIObjectLoadingContent.PLUGIN_PERMISSION_PROMPT_ACTION_QUIET
    ) {
      if (overlay) {
        overlay.classList.remove("visible");
      }
      return;
    }

    if (overlay) {
      overlay.addEventListener("click", this, true);
    }
  }

  onOverlayClick(event) {
    let plugin = event.target.containingShadowRoot.host;
    let overlay = this.getPluginUI(plugin, "main");
    // Have to check that the target is not the link to update the plugin
    if (
      !(
        ChromeUtils.getClassName(event.originalTarget) === "HTMLAnchorElement"
      ) &&
      event.originalTarget.getAttribute("anonid") != "closeIcon" &&
      event.originalTarget.id != "closeIcon" &&
      !overlay.hasAttribute("dismissed") &&
      event.button == 0 &&
      event.isTrusted
    ) {
      this.showClickToPlayNotification(plugin, true);
      event.stopPropagation();
      event.preventDefault();
    }
  }

  reshowClickToPlayNotification() {
    let { plugins } = this.contentWindow.windowUtils;
    for (let plugin of plugins) {
      let overlay = this.getPluginUI(plugin, "main");
      if (overlay) {
        overlay.removeEventListener("click", this, true);
      }
      if (this.canActivatePlugin(plugin)) {
        this._handleClickToPlayEvent(plugin);
      }
    }
    this.showClickToPlayNotification(null, false);
  }

  /**
   * Activate the plugins that the user has specified.
   */
  activatePlugins(activationInfo, newState) {
    let { plugins } = this.contentWindow.windowUtils;

    let pluginFound = false;
    for (let plugin of plugins) {
      if (!this.isKnownPlugin(plugin)) {
        continue;
      }
      if (
        activationInfo.permissionString ==
        gPluginHost.getPermissionStringForType(plugin.actualType)
      ) {
        let overlay = this.getPluginUI(plugin, "main");
        pluginFound = true;
        if (
          newState == "block" ||
          newState == "blockalways" ||
          newState == "continueblocking"
        ) {
          if (overlay) {
            overlay.addEventListener("click", this, true);
          }
          plugin.pluginFallbackTypeOverride = activationInfo.fallbackType;
          plugin.reload(true);
        } else if (this.canActivatePlugin(plugin)) {
          if (overlay) {
            overlay.removeEventListener("click", this, true);
          }
          plugin.playPlugin();
        }
      }
    }

    // If there are no instances of the plugin on the page any more or if we've
    // noted that the content needs to be reloaded due to replacing HLS, what the
    // user probably needs is for us to allow and then refresh.
    if (
      newState != "block" &&
      newState != "blockalways" &&
      newState != "continueblocking" &&
      (!pluginFound || this.contentWindow.pluginRequiresReload)
    ) {
      this.reloadPage();
    }
  }

  showClickToPlayNotification(pluginElOrTag, showNow) {
    let plugins = [];

    // If pluginElOrTag is null, that means the user has navigated back to a page with
    // plugins, and we need to collect all the plugins.
    if (pluginElOrTag === null) {
      // cwu.plugins may contain non-plugin <object>s, filter them out
      plugins = this.contentWindow.windowUtils.plugins.filter(
        p =>
          p.getContentTypeForMIMEType(p.actualType) ==
          Ci.nsIObjectLoadingContent.TYPE_PLUGIN
      );

      if (!plugins.length) {
        this.removeNotification();
        return;
      }
    } else {
      plugins = [pluginElOrTag];
    }

    // Iterate over the plugins and ensure we have one value for each
    // permission string - though in principle there should only be 1 anyway
    // (for flash), in practice there are still some automated tests where we
    // could encounter other ones.
    let permissionMap = new Map();
    for (let p of plugins) {
      let pluginInfo;
      if (p instanceof Ci.nsIPluginTag) {
        pluginInfo = this._getPluginInfoForTag(p);
      } else {
        pluginInfo = this._getPluginInfo(p);
      }
      if (pluginInfo.permissionString === null) {
        Cu.reportError("No permission string for active plugin.");
        continue;
      }
      if (!permissionMap.has(pluginInfo.permissionString)) {
        permissionMap.set(pluginInfo.permissionString, pluginInfo);
        continue;
      }
    }
    if (permissionMap.size > 1) {
      Cu.reportError(
        "Err, we're not meant to have more than 1 plugin anymore!"
      );
    }
    if (!permissionMap.size) {
      return;
    }

    this.haveShownNotification = true;

    let permissionItem = permissionMap.values().next().value;
    let plugin = {
      id: permissionItem.pluginTag.id,
      fallbackType: permissionItem.fallbackType,
    };

    let msg = "PluginContent:ShowClickToPlayNotification";
    this.sendAsyncMessage(msg, { plugin, showNow });
  }

  removeNotification() {
    this.sendAsyncMessage("PluginContent:RemoveNotification");
  }

  clearPluginCaches() {
    this.pluginCrashData.clear();
  }

  /**
   * Determines whether or not the crashed plugin is contained within current
   * full screen DOM element.
   * @param fullScreenElement (DOM element)
   *   The DOM element that is currently full screen, or null.
   * @param domElement
   *   The DOM element which contains the crashed plugin, or the crashed plugin
   *   itself.
   * @returns bool
   *   True if the plugin is a descendant of the full screen DOM element, false otherwise.
   **/
  isWithinFullScreenElement(fullScreenElement, domElement) {
    /**
     * Traverses down iframes until it find a non-iframe full screen DOM element.
     * @param fullScreenIframe
     *  Target iframe to begin searching from.
     * @returns DOM element
     *  The full screen DOM element contained within the iframe (could be inner iframe), or the original iframe if no inner DOM element is found.
     **/
    let getTrueFullScreenElement = fullScreenIframe => {
      if (
        typeof fullScreenIframe.contentDocument !== "undefined" &&
        fullScreenIframe.contentDocument.mozFullScreenElement
      ) {
        return getTrueFullScreenElement(
          fullScreenIframe.contentDocument.mozFullScreenElement
        );
      }
      return fullScreenIframe;
    };

    if (fullScreenElement.tagName === "IFRAME") {
      fullScreenElement = getTrueFullScreenElement(fullScreenElement);
    }

    if (fullScreenElement.contains(domElement)) {
      return true;
    }
    let parentIframe = domElement.ownerGlobal.frameElement;
    if (parentIframe) {
      return this.isWithinFullScreenElement(fullScreenElement, parentIframe);
    }
    return false;
  }

  /**
   * The PluginCrashed event handler. Note that the PluginCrashed event is
   * fired for both NPAPI and Gecko Media plugins. In the latter case, the
   * target of the event is the document that the GMP is being used in.
   */
  async onPluginCrashed(target, aEvent) {
    if (!(aEvent instanceof this.contentWindow.PluginCrashedEvent)) {
      return;
    }

    let fullScreenElement = this.contentWindow.top.document
      .mozFullScreenElement;
    if (fullScreenElement) {
      if (this.isWithinFullScreenElement(fullScreenElement, target)) {
        this.contentWindow.top.document.mozCancelFullScreen();
      }
    }

    if (aEvent.gmpPlugin) {
      this.GMPCrashed(aEvent);
      return;
    }

    if (!(target instanceof Ci.nsIObjectLoadingContent)) {
      return;
    }

    let crashData = this.pluginCrashData.get(target.runID);
    if (!crashData) {
      // We haven't received information from the parent yet about
      // this crash, so go get it:
      crashData = await this.sendQuery("PluginContent:GetCrashData", {
        runID: target.runID,
      });
      this.pluginCrashData.set(target.runID, crashData);
    }

    this.setCrashedNPAPIPluginState({
      pluginElement: target,
      state: crashData.state,
      pluginName: crashData.pluginName,
    });
  }

  setCrashedNPAPIPluginState({ pluginElement, state, pluginName }) {
    let overlay = this.getPluginUI(pluginElement, "main");
    let statusDiv = this.getPluginUI(pluginElement, "submitStatus");
    let optInCB = this.getPluginUI(pluginElement, "submitURLOptIn");

    this.getPluginUI(pluginElement, "submitButton").addEventListener(
      "click",
      event => {
        if (event.button != 0 || !event.isTrusted) {
          return;
        }
        this.submitReport(pluginElement);
      }
    );

    optInCB.checked = Services.prefs.getBoolPref(
      "dom.ipc.plugins.reportCrashURL",
      true
    );

    statusDiv.setAttribute("status", state);

    let helpIcon = this.getPluginUI(pluginElement, "helpIcon");
    this.addLinkClickCallback(helpIcon, "openHelpPage");

    let crashText = this.getPluginUI(pluginElement, "crashedText");

    let message = gNavigatorBundle.formatStringFromName(
      "crashedpluginsMessage.title",
      [pluginName]
    );
    crashText.textContent = message;

    let link = this.getPluginUI(pluginElement, "reloadLink");
    this.addLinkClickCallback(link, "reloadPage");

    // This might trigger force reflow, but plug-in crashing code path shouldn't be hot.
    let overlayDisplayState = this.computeAndAdjustOverlayDisplay(
      pluginElement,
      overlay,
      true
    );

    // Is the <object>'s size too small to hold what we want to show?
    if (overlayDisplayState != OVERLAY_DISPLAY.FULL) {
      // First try hiding the crash report submission UI.
      statusDiv.removeAttribute("status");

      overlayDisplayState = this.computeAndAdjustOverlayDisplay(
        pluginElement,
        overlay,
        true
      );
    }
    this.setVisibility(pluginElement, overlay, overlayDisplayState);

    let doc = pluginElement.ownerDocument;
    let runID = pluginElement.runID;

    if (overlayDisplayState == OVERLAY_DISPLAY.FULL) {
      doc.mozNoPluginCrashedNotification = true;

      // Notify others that the crash reporter UI is now ready.
      // Currently, this event is only used by tests.
      let winUtils = this.contentWindow.windowUtils;
      let event = new this.contentWindow.CustomEvent(
        "PluginCrashReporterDisplayed",
        {
          bubbles: true,
        }
      );
      winUtils.dispatchEventToChromeOnly(pluginElement, event);
    } else if (!doc.mozNoPluginCrashedNotification) {
      // If another plugin on the page was large enough to show our UI, we don't
      // want to show a notification bar.
      this.sendAsyncMessage("PluginContent:ShowPluginCrashedNotification", {
        pluginCrashID: { runID },
      });
    }
  }

  NPAPIPluginCrashReportSubmitted({ runID, state }) {
    this.pluginCrashData.delete(runID);
    let { plugins } = this.contentWindow.windowUtils;

    for (let pluginElement of plugins) {
      if (
        pluginElement instanceof Ci.nsIObjectLoadingContent &&
        pluginElement.runID == runID
      ) {
        let statusDiv = this.getPluginUI(pluginElement, "submitStatus");
        statusDiv.setAttribute("status", state);
      }
    }
  }

  GMPCrashed(aEvent) {
    let { target, gmpPlugin, pluginID } = aEvent;
    if (!gmpPlugin || !target.document) {
      // TODO: Throw exception? How did we get here?
      return;
    }

    this.sendAsyncMessage("PluginContent:ShowPluginCrashedNotification", {
      pluginCrashID: { pluginID },
    });
  }
}