diff options
Diffstat (limited to 'application/basilisk/modules/PluginContent.jsm')
-rw-r--r-- | application/basilisk/modules/PluginContent.jsm | 1182 |
1 files changed, 1182 insertions, 0 deletions
diff --git a/application/basilisk/modules/PluginContent.jsm b/application/basilisk/modules/PluginContent.jsm new file mode 100644 index 0000000000..40c4559a15 --- /dev/null +++ b/application/basilisk/modules/PluginContent.jsm @@ -0,0 +1,1182 @@ +/* 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 Cc = Components.classes; +var Ci = Components.interfaces; +var Cu = Components.utils; + +this.EXPORTED_SYMBOLS = [ "PluginContent" ]; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/Timer.jsm"); +Cu.import("resource://gre/modules/BrowserUtils.jsm"); + +XPCOMUtils.defineLazyGetter(this, "gNavigatorBundle", function() { + const url = "chrome://browser/locale/browser.properties"; + return Services.strings.createBundle(url); +}); + +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 + this.content = this.global.content; + // Cache of plugin actions for the current page. + this.pluginData = new Map(); + // Cache of plugin crash information sent from the parent + this.pluginCrashData = new Map(); + + // Note that the XBL binding is untrusted + global.addEventListener("PluginBindingAttached", this, true, true); + global.addEventListener("PluginPlaceholderReplaced", this, true, true); + global.addEventListener("PluginCrashed", this, true); + global.addEventListener("PluginOutdated", this, true); + global.addEventListener("PluginInstantiated", this, true); + global.addEventListener("PluginRemoved", this, true); + global.addEventListener("pagehide", this, true); + global.addEventListener("pageshow", this, true); + global.addEventListener("unload", this); + global.addEventListener("HiddenPlugin", this, true); + + global.addMessageListener("BrowserPlugins:ActivatePlugins", this); + global.addMessageListener("BrowserPlugins:NotificationShown", this); + global.addMessageListener("BrowserPlugins:ContextMenuCommand", this); + global.addMessageListener("BrowserPlugins:NPAPIPluginProcessCrashed", this); + global.addMessageListener("BrowserPlugins:CrashReportSubmitted", this); + global.addMessageListener("BrowserPlugins:Test:ClearCrashData", this); + + Services.obs.addObserver(this, "decoder-doctor-notification", false); + }, + + uninit() { + let global = this.global; + + global.removeEventListener("PluginBindingAttached", this, true); + global.removeEventListener("PluginPlaceholderReplaced", this, true, true); + global.removeEventListener("PluginCrashed", this, true); + global.removeEventListener("PluginOutdated", this, true); + global.removeEventListener("PluginInstantiated", this, true); + global.removeEventListener("PluginRemoved", this, true); + global.removeEventListener("pagehide", this, true); + global.removeEventListener("pageshow", this, true); + global.removeEventListener("unload", this); + global.removeEventListener("HiddenPlugin", this, true); + + global.removeMessageListener("BrowserPlugins:ActivatePlugins", this); + global.removeMessageListener("BrowserPlugins:NotificationShown", this); + global.removeMessageListener("BrowserPlugins:ContextMenuCommand", this); + global.removeMessageListener("BrowserPlugins:NPAPIPluginProcessCrashed", this); + global.removeMessageListener("BrowserPlugins:CrashReportSubmitted", this); + global.removeMessageListener("BrowserPlugins:Test:ClearCrashData", this); + + Services.obs.removeObserver(this, "decoder-doctor-notification"); + + delete this.global; + delete this.content; + }, + + receiveMessage(msg) { + 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": + switch (msg.data.command) { + case "play": + this._showClickToPlayNotification(msg.objects.plugin, true); + break; + case "hide": + this.hideClickToPlayOverlay(msg.objects.plugin); + break; + } + break; + case "BrowserPlugins:NPAPIPluginProcessCrashed": + this.NPAPIPluginProcessCrashed({ + pluginName: msg.data.pluginName, + runID: msg.data.runID, + state: msg.data.state, + }); + break; + case "BrowserPlugins:CrashReportSubmitted": + this.NPAPIPluginCrashReportSubmitted({ + runID: msg.data.runID, + state: msg.data.state, + }) + break; + case "BrowserPlugins:Test:ClearCrashData": + // This message should ONLY ever be sent by automated tests. + if (Services.prefs.getBoolPref("plugins.testmode")) { + this.pluginCrashData.clear(); + } + } + }, + + observe: function 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.content.document && + data.formats.toLowerCase().includes("application/x-mpegurl", 0)) { + this.global.content.pluginRequiresReload = true; + this.updateNotificationUI(this.content.document); + } + } + }, + + onPageShow(event) { + // Ignore events that aren't from the main document. + if (!this.content || event.target != this.content.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.content || event.target != this.content.document) { + return; + } + + this._finishRecordingFlashPluginTelemetry(); + this.clearPluginCaches(); + this.haveShownNotification = false; + }, + + getPluginUI(plugin, anonid) { + return plugin.ownerDocument. + getAnonymousElementByAttribute(plugin, "anonid", anonid); + }, + + _getPluginInfo(pluginElement) { + if (pluginElement instanceof Ci.nsIDOMHTMLAnchorElement) { + // Anchor elements are our place holders, and we only have them for Flash + let pluginHost = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost); + return { + pluginName: "Shockwave Flash", + mimetype: FLASH_MIME_TYPE, + permissionString: pluginHost.getPermissionStringForType(FLASH_MIME_TYPE) + }; + } + let pluginHost = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost); + pluginElement.QueryInterface(Ci.nsIObjectLoadingContent); + + let tagMimetype; + let pluginName = gNavigatorBundle.GetStringFromName("pluginInfo.unknownPlugin"); + let pluginTag = null; + let permissionString = null; + let fallbackType = null; + let blocklistState = null; + + tagMimetype = pluginElement.actualType; + if (tagMimetype == "") { + tagMimetype = pluginElement.type; + } + + if (this.isKnownPlugin(pluginElement)) { + pluginTag = pluginHost.getPluginTagForType(pluginElement.actualType); + pluginName = BrowserUtils.makeNicePluginName(pluginTag.name); + + // Convert this from nsIPluginTag so it can be serialized. + let properties = ["name", "description", "filename", "version", "enabledState", "niceName"]; + let pluginTagCopy = {}; + for (let prop of properties) { + pluginTagCopy[prop] = pluginTag[prop]; + } + pluginTag = pluginTagCopy; + + permissionString = pluginHost.getPermissionStringForType(pluginElement.actualType); + fallbackType = pluginElement.defaultFallbackType; + blocklistState = pluginHost.getBlocklistStateForType(pluginElement.actualType); + // Make state-softblocked == state-notblocked for our purposes, + // they have the same UI. STATE_OUTDATED should not exist for plugin + // items, but let's alias it anyway, just in case. + if (blocklistState == Ci.nsIBlocklistService.STATE_SOFTBLOCKED || + blocklistState == Ci.nsIBlocklistService.STATE_OUTDATED) { + blocklistState = Ci.nsIBlocklistService.STATE_NOT_BLOCKED; + } + } + + return { mimetype: tagMimetype, + pluginName, + pluginTag, + permissionString, + fallbackType, + blocklistState, + }; + }, + + /** + * _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, tagMimetype) { + let pluginHost = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost); + + let pluginName = gNavigatorBundle.GetStringFromName("pluginInfo.unknownPlugin"); + let permissionString = null; + let blocklistState = null; + + if (pluginTag) { + pluginName = BrowserUtils.makeNicePluginName(pluginTag.name); + + permissionString = pluginHost.getPermissionStringForTag(pluginTag); + blocklistState = pluginTag.blocklistState; + + // Convert this from nsIPluginTag so it can be serialized. + let properties = ["name", "description", "filename", "version", "enabledState", "niceName"]; + let pluginTagCopy = {}; + for (let prop of properties) { + pluginTagCopy[prop] = pluginTag[prop]; + } + pluginTag = pluginTagCopy; + + // Make state-softblocked == state-notblocked for our purposes, + // they have the same UI. STATE_OUTDATED should not exist for plugin + // items, but let's alias it anyway, just in case. + if (blocklistState == Ci.nsIBlocklistService.STATE_SOFTBLOCKED || + blocklistState == Ci.nsIBlocklistService.STATE_OUTDATED) { + blocklistState = Ci.nsIBlocklistService.STATE_NOT_BLOCKED; + } + } + + return { mimetype: tagMimetype, + pluginName, + pluginTag, + permissionString, + // 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. + fallbackType: Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY, + blocklistState, + }; + }, + + /** + * Update the visibility of the plugin overlay. + */ + setVisibility(plugin, overlay, shouldShow) { + overlay.classList.toggle("visible", shouldShow); + if (shouldShow) { + overlay.removeAttribute("dismissed"); + } + }, + + /** + * Check whether the plugin should be visible on the page. A plugin should + * not be visible if the overlay is too big, or if any other page content + * overlays it. + * + * This function will handle showing or hiding the overlay. + * @returns true if the plugin is invisible. + */ + shouldShowOverlay(plugin, overlay) { + // If the overlay size is 0, we haven't done layout yet. Presume that + // plugins are visible until we know otherwise. + if (overlay.scrollWidth == 0) { + return true; + } + + // Is the <object>'s size too small to hold what we want to show? + let pluginRect = plugin.getBoundingClientRect(); + // 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 > Math.ceil(pluginRect.width)) || + (overlay.scrollHeight - 5 > Math.ceil(pluginRect.height)); + if (overflows) { + return false; + } + + // 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]]; + + if (right <= 0 || top <= 0) { + return false; + } + + let contentWindow = plugin.ownerGlobal; + let cwu = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + + for (let [x, y] of points) { + let el = cwu.elementFromPoint(x, y, true, true); + if (el !== plugin) { + return false; + } + } + + return true; + }, + + 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 == 0) + 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 == 0) + 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: + 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) { + let eventType = event.type; + + if (eventType == "unload") { + this.uninit(); + return; + } + + if (eventType == "pagehide") { + this.onPageHide(event); + return; + } + + if (eventType == "pageshow") { + this.onPageShow(event); + return; + } + + if (eventType == "PluginRemoved") { + this.updateNotificationUI(event.target); + 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 win = event.target.defaultView; + if (!win.mozHiddenPluginTouched) { + let pluginTag = event.tag.QueryInterface(Ci.nsIPluginTag); + if (win.top.document != this.content.document) { + return; + } + this._showClickToPlayNotification(pluginTag, false); + let winUtils = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); + try { + winUtils.loadSheet(REPLACEMENT_STYLE_SHEET, win.AGENT_SHEET); + win.mozHiddenPluginTouched = true; + } catch (e) { + Cu.reportError("Error adding plugin replacement style sheet: " + e); + } + } + } + + let plugin = event.target; + + if (eventType == "PluginPlaceholderReplaced") { + plugin.removeAttribute("href"); + let overlay = this.getPluginUI(plugin, "main"); + this.setVisibility(plugin, overlay, true); + let inIDOMUtils = Cc["@mozilla.org/inspector/dom-utils;1"] + .getService(Ci.inIDOMUtils); + // Add psuedo class so our styling will take effect + inIDOMUtils.addPseudoClassLock(plugin, "-moz-handler-clicktoplay"); + overlay.addEventListener("click", this, true); + return; + } + + if (!(plugin 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(plugin, "main"); + if (!overlay || overlay._bindingHandled) { + return; + } + overlay._bindingHandled = true; + + // Lookup the handler for this binding + eventType = this._getBindingType(plugin); + if (!eventType) { + // Not all bindings have handlers + return; + } + } + + let shouldShowNotification = false; + switch (eventType) { + case "PluginCrashed": + this.onPluginCrashed(plugin, event); + break; + + case "PluginNotFound": { + /* NOP */ + break; + } + + case "PluginBlocklisted": + case "PluginOutdated": + shouldShowNotification = true; + break; + + case "PluginVulnerableUpdatable": + let updateLink = this.getPluginUI(plugin, "checkForUpdatesLink"); + let { pluginTag } = this._getPluginInfo(plugin); + this.addLinkClickCallback(updateLink, "forwardCallback", + "openPluginUpdatePage", pluginTag); + /* FALLTHRU */ + + case "PluginVulnerableNoUpdate": + case "PluginClickToPlay": + this._handleClickToPlayEvent(plugin); + let pluginName = this._getPluginInfo(plugin).pluginName; + let messageString = gNavigatorBundle.formatStringFromName("PluginClickToActivate", [pluginName], 1); + let overlayText = this.getPluginUI(plugin, "clickToPlay"); + overlayText.textContent = messageString; + if (eventType == "PluginVulnerableUpdatable" || + eventType == "PluginVulnerableNoUpdate") { + let vulnerabilityString = gNavigatorBundle.GetStringFromName(eventType); + let vulnerabilityText = this.getPluginUI(plugin, "vulnerabilityStatus"); + vulnerabilityText.textContent = vulnerabilityString; + } + shouldShowNotification = true; + break; + + case "PluginDisabled": + let manageLink = this.getPluginUI(plugin, "managePluginsLink"); + this.addLinkClickCallback(manageLink, "forwardCallback", "managePlugins"); + shouldShowNotification = true; + break; + + case "PluginInstantiated": + let key = this._getPluginInfo(plugin).pluginTag.niceName; + Services.telemetry.getKeyedHistogramById("PLUGIN_ACTIVATION_COUNT").add(key); + shouldShowNotification = true; + let pluginRect = plugin.getBoundingClientRect(); + if (pluginRect.width <= 5 && pluginRect.height <= 5) { + Services.telemetry.getHistogramById("PLUGIN_TINY_CONTENT").add(1); + } + break; + } + + if (this._getPluginInfo(plugin).mimetype === FLASH_MIME_TYPE) { + this._recordFlashPluginTelemetry(eventType, plugin); + } + + // Show the in-content UI if it's not too big. The crashed plugin handler already did this. + let overlay = this.getPluginUI(plugin, "main"); + if (eventType != "PluginCrashed") { + if (overlay != null) { + this.setVisibility(plugin, overlay, + this.shouldShowOverlay(plugin, overlay)); + let resizeListener = () => { + this.setVisibility(plugin, overlay, + this.shouldShowOverlay(plugin, overlay)); + this.updateNotificationUI(); + }; + plugin.addEventListener("overflow", resizeListener); + plugin.addEventListener("underflow", resizeListener); + } + } + + let closeIcon = this.getPluginUI(plugin, "closeIcon"); + if (closeIcon) { + closeIcon.addEventListener("click", clickEvent => { + if (clickEvent.button == 0 && clickEvent.isTrusted) { + this.hideClickToPlayOverlay(plugin); + overlay.setAttribute("dismissed", "true"); + } + }, true); + } + + if (shouldShowNotification) { + this._showClickToPlayNotification(plugin, false); + } + }, + + _recordFlashPluginTelemetry(eventType, plugin) { + if (!Services.telemetry.canRecordExtended) { + return; + } + + if (!this.flashPluginStats) { + this.flashPluginStats = { + instancesCount: 0, + plugins: new WeakSet() + }; + } + + if (!this.flashPluginStats.plugins.has(plugin)) { + // Reporting plugin instance and its dimensions only once. + this.flashPluginStats.plugins.add(plugin); + + this.flashPluginStats.instancesCount++; + + let pluginRect = plugin.getBoundingClientRect(); + Services.telemetry.getHistogramById("FLASH_PLUGIN_WIDTH") + .add(pluginRect.width); + Services.telemetry.getHistogramById("FLASH_PLUGIN_HEIGHT") + .add(pluginRect.height); + Services.telemetry.getHistogramById("FLASH_PLUGIN_AREA") + .add(pluginRect.width * pluginRect.height); + + let state = this._getPluginInfo(plugin).fallbackType; + if (state === null) { + state = Ci.nsIObjectLoadingContent.PLUGIN_UNSUPPORTED; + } + Services.telemetry.getHistogramById("FLASH_PLUGIN_STATES") + .add(state); + } + }, + + _finishRecordingFlashPluginTelemetry() { + if (this.flashPluginStats) { + Services.telemetry.getHistogramById("FLASH_PLUGIN_INSTANCES_ON_PAGE") + .add(this.flashPluginStats.instancesCount); + delete this.flashPluginStats; + } + }, + + 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 pluginHost = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost); + let permissionString = pluginHost.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_VULNERABLE_NO_UPDATE; + + return !objLoadingContent.activated && + pluginPermission != Ci.nsIPermissionManager.DENY_ACTION && + isFallbackTypeValid; + }, + + hideClickToPlayOverlay(plugin) { + let overlay = this.getPluginUI(plugin, "main"); + if (overlay) { + overlay.classList.remove("visible"); + } + }, + + // Forward a link click callback to the chrome process. + forwardCallback(name, pluginTag) { + this.global.sendAsyncMessage("PluginContent:LinkClickCallback", + { name, pluginTag }); + }, + + submitReport: function submitReport(plugin) { + // **STUB** + return; + }, + + reloadPage() { + this.global.content.location.reload(); + }, + + // Event listener for click-to-play plugins. + _handleClickToPlayEvent(plugin) { + let doc = plugin.ownerDocument; + let pluginHost = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost); + let permissionString; + if (plugin instanceof Ci.nsIDOMHTMLAnchorElement) { + // We only have replacement content for Flash installs + permissionString = pluginHost.getPermissionStringForType(FLASH_MIME_TYPE); + } else { + let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + // guard against giving pluginHost.getPermissionStringForType a type + // not associated with any known plugin + if (!this.isKnownPlugin(objLoadingContent)) + return; + permissionString = pluginHost.getPermissionStringForType(objLoadingContent.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) { + if (overlay) { + overlay.classList.remove("visible"); + } + return; + } + + if (overlay) { + overlay.addEventListener("click", this, true); + } + }, + + onOverlayClick(event) { + let document = event.target.ownerDocument; + let plugin = document.getBindingParent(event.target); + let contentWindow = plugin.ownerGlobal.top; + let overlay = this.getPluginUI(plugin, "main"); + // Have to check that the target is not the link to update the plugin + if (!(event.originalTarget instanceof contentWindow.HTMLAnchorElement) && + (event.originalTarget.getAttribute("anonid") != "closeIcon") && + !overlay.hasAttribute("dismissed") && + event.button == 0 && + event.isTrusted) { + this._showClickToPlayNotification(plugin, true); + event.stopPropagation(); + event.preventDefault(); + } + }, + + reshowClickToPlayNotification() { + let contentWindow = this.global.content; + let cwu = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + let plugins = cwu.plugins; + for (let plugin of plugins) { + let overlay = this.getPluginUI(plugin, "main"); + if (overlay) + overlay.removeEventListener("click", this, true); + let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + if (this.canActivatePlugin(objLoadingContent)) + this._handleClickToPlayEvent(plugin); + } + this._showClickToPlayNotification(null, false); + }, + + /** + * Activate the plugins that the user has specified. + */ + activatePlugins(pluginInfo, newState) { + let contentWindow = this.global.content; + let cwu = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + let plugins = cwu.plugins; + let pluginHost = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost); + + let pluginFound = false; + let placeHolderFound = false; + for (let plugin of plugins) { + plugin.QueryInterface(Ci.nsIObjectLoadingContent); + if (!this.isKnownPlugin(plugin)) { + continue; + } + if (pluginInfo.permissionString == pluginHost.getPermissionStringForType(plugin.actualType)) { + let overlay = this.getPluginUI(plugin, "main"); + if (plugin instanceof Ci.nsIDOMHTMLAnchorElement) { + placeHolderFound = true; + } else { + pluginFound = true; + } + if (newState == "block") { + if (overlay) { + overlay.addEventListener("click", this, true); + } + 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, what the + // user probably needs is for us to allow and then refresh. Additionally, if + // this is content that requires HLS or we replaced the placeholder the page + // needs to be refreshed for it to insert its plugins + if (newState != "block" && + (!pluginFound || placeHolderFound || contentWindow.pluginRequiresReload)) { + this.reloadPage(); + } + this.updateNotificationUI(); + }, + + _showClickToPlayNotification(plugin, showNow) { + let plugins = []; + + // If plugin is null, that means the user has navigated back to a page with + // plugins, and we need to collect all the plugins. + if (plugin === null) { + let contentWindow = this.content; + let cwu = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + // cwu.plugins may contain non-plugin <object>s, filter them out + plugins = cwu.plugins.filter((p) => + p.getContentTypeForMIMEType(p.actualType) == Ci.nsIObjectLoadingContent.TYPE_PLUGIN); + + if (plugins.length == 0) { + this.removeNotification("click-to-play-plugins"); + return; + } + } else { + plugins = [plugin]; + } + + let pluginData = this.pluginData; + + let principal = this.content.document.nodePrincipal; + let location = this.content.document.location.href; + + for (let p of plugins) { + let pluginInfo; + if (p instanceof Ci.nsIPluginTag) { + let mimeType = p.getMimeTypes() > 0 ? p.getMimeTypes()[0] : null; + pluginInfo = this._getPluginInfoForTag(p, mimeType); + } else { + pluginInfo = this._getPluginInfo(p); + } + if (pluginInfo.permissionString === null) { + Cu.reportError("No permission string for active plugin."); + continue; + } + if (pluginData.has(pluginInfo.permissionString)) { + continue; + } + + let permissionObj = Services.perms. + getPermissionObject(principal, pluginInfo.permissionString, false); + if (permissionObj) { + pluginInfo.pluginPermissionPrePath = permissionObj.principal.originNoSuffix; + pluginInfo.pluginPermissionType = permissionObj.expireType; + } else { + pluginInfo.pluginPermissionPrePath = principal.originNoSuffix; + pluginInfo.pluginPermissionType = undefined; + } + + this.pluginData.set(pluginInfo.permissionString, pluginInfo); + } + + this.haveShownNotification = true; + + this.global.sendAsyncMessage("PluginContent:ShowClickToPlayNotification", { + plugins: [...this.pluginData.values()], + showNow, + location, + }, null, principal); + }, + + /** + * Updates the "hidden plugin" notification bar UI. + * + * @param document (optional) + * Specify the document that is causing the update. + * This is useful when the document is possibly no longer + * the current loaded document (for example, if we're + * responding to a PluginRemoved event for an unloading + * document). If this parameter is omitted, it defaults + * to the current top-level document. + */ + updateNotificationUI(document) { + document = document || this.content.document; + + // We're only interested in the top-level document, since that's + // the one that provides the Principal that we send back to the + // parent. + let principal = document.defaultView.top.document.nodePrincipal; + let location = document.location.href; + + // Make a copy of the actions from the last popup notification. + let haveInsecure = false; + let actions = new Map(); + for (let action of this.pluginData.values()) { + switch (action.fallbackType) { + // haveInsecure will trigger the red flashing icon and the infobar + // styling below + case Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_UPDATABLE: + case Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_NO_UPDATE: + haveInsecure = true; + // fall through + + case Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY: + actions.set(action.permissionString, action); + continue; + } + } + + // Remove plugins that are already active, or large enough to show an overlay. + let cwu = this.content.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + for (let plugin of cwu.plugins) { + let info = this._getPluginInfo(plugin); + if (!actions.has(info.permissionString)) { + continue; + } + let fallbackType = info.fallbackType; + if (fallbackType == Ci.nsIObjectLoadingContent.PLUGIN_ACTIVE) { + actions.delete(info.permissionString); + if (actions.size == 0) { + break; + } + continue; + } + if (fallbackType != Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY && + fallbackType != Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_UPDATABLE && + fallbackType != Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_NO_UPDATE) { + continue; + } + let overlay = this.getPluginUI(plugin, "main"); + if (!overlay) { + continue; + } + let shouldShow = this.shouldShowOverlay(plugin, overlay); + this.setVisibility(plugin, overlay, shouldShow); + if (shouldShow) { + actions.delete(info.permissionString); + if (actions.size == 0) { + break; + } + } + } + + // If there are any items remaining in `actions` now, they are hidden + // plugins that need a notification bar. + this.global.sendAsyncMessage("PluginContent:UpdateHiddenPluginUI", { + haveInsecure, + actions: [...actions.values()], + location, + }, null, principal); + }, + + removeNotification(name) { + this.global.sendAsyncMessage("PluginContent:RemoveNotification", { name }); + }, + + clearPluginCaches() { + this.pluginData.clear(); + this.pluginCrashData.clear(); + }, + + hideNotificationBar(name) { + this.global.sendAsyncMessage("PluginContent:HideNotificationBar", { name }); + }, + + /** + * 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 Goanna Media plugins. In the latter case, the + * target of the event is the document that the GMP is being used in. + */ + onPluginCrashed(target, aEvent) { + if (!(aEvent instanceof this.content.PluginCrashedEvent)) + return; + + let fullScreenElement = this.content.document.mozFullScreenElement; + if (fullScreenElement) { + if (this.isWithinFullScreenElement(fullScreenElement, target)) { + this.content.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 we should hold off showing the crash report + // UI. + return; + } + + crashData.instances.delete(target); + if (crashData.instances.length == 0) { + this.pluginCrashData.delete(target.runID); + } + + this.setCrashedNPAPIPluginState({ + plugin: target, + state: crashData.state, + message: crashData.message, + }); + }, + + NPAPIPluginProcessCrashed({pluginName, runID, state}) { + let message = + gNavigatorBundle.formatStringFromName("crashedpluginsMessage.title", + [pluginName], 1); + + let contentWindow = this.global.content; + let cwu = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + let plugins = cwu.plugins; + + for (let plugin of plugins) { + if (plugin instanceof Ci.nsIObjectLoadingContent && + plugin.runID == runID) { + // The parent has told us that the plugin process has died. + // It's possible that this content process hasn't yet noticed, + // in which case we need to stash this data around until the + // PluginCrashed events get sent up. + if (plugin.pluginFallbackType == Ci.nsIObjectLoadingContent.PLUGIN_CRASHED) { + // This plugin has already been put into the crashed state by the + // content process, so we can tweak its crash UI without delay. + this.setCrashedNPAPIPluginState({plugin, state, message}); + } else { + // The content process hasn't yet determined that the plugin has crashed. + // Stash the data in our map, and throw the plugin into a WeakSet. When + // the PluginCrashed event fires on the <object>/<embed>, we'll retrieve + // the information we need from the Map and remove the instance from the + // WeakSet. Once the WeakSet is empty, we can clear the map. + if (!this.pluginCrashData.has(runID)) { + this.pluginCrashData.set(runID, { + state, + message, + instances: new WeakSet(), + }); + } + let crashData = this.pluginCrashData.get(runID); + crashData.instances.add(plugin); + } + } + } + }, + + setCrashedNPAPIPluginState({plugin, state, message}) { + // Force a layout flush so the binding is attached. + plugin.clientTop; + let overlay = this.getPluginUI(plugin, "main"); + let statusDiv = this.getPluginUI(plugin, "submitStatus"); + let optInCB = this.getPluginUI(plugin, "submitURLOptIn"); + + this.getPluginUI(plugin, "submitButton") + .addEventListener("click", (event) => { + if (event.button != 0 || !event.isTrusted) + return; + this.submitReport(plugin); + }); + + let pref = Services.prefs.getBranch("dom.ipc.plugins.reportCrashURL"); + optInCB.checked = pref.getBoolPref(""); + + statusDiv.setAttribute("status", state); + + let helpIcon = this.getPluginUI(plugin, "helpIcon"); + this.addLinkClickCallback(helpIcon, "openHelpPage"); + + let crashText = this.getPluginUI(plugin, "crashedText"); + crashText.textContent = message; + + let link = this.getPluginUI(plugin, "reloadLink"); + this.addLinkClickCallback(link, "reloadPage"); + + let isShowing = this.shouldShowOverlay(plugin, overlay); + + // Is the <object>'s size too small to hold what we want to show? + if (!isShowing) { + // First try hiding the crash report submission UI. + statusDiv.removeAttribute("status"); + + isShowing = this.shouldShowOverlay(plugin, overlay); + } + this.setVisibility(plugin, overlay, isShowing); + + let doc = plugin.ownerDocument; + let runID = plugin.runID; + + if (isShowing) { + // If a previous plugin on the page was too small and resulted in adding a + // notification bar, then remove it because this plugin instance it big + // enough to serve as in-content notification. + this.hideNotificationBar("plugin-crashed"); + doc.mozNoPluginCrashedNotification = true; + + // Notify others that the crash reporter UI is now ready. + // Currently, this event is only used by tests. + let winUtils = this.content.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + let event = new this.content.CustomEvent("PluginCrashReporterDisplayed", {bubbles: true}); + winUtils.dispatchEventToChromeOnly(plugin, 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.global.sendAsyncMessage("PluginContent:ShowPluginCrashedNotification", + { messageString: message, pluginID: runID }); + // Remove the notification when the page is reloaded. + doc.defaultView.top.addEventListener("unload", event => { + this.hideNotificationBar("plugin-crashed"); + }); + } + }, + + NPAPIPluginCrashReportSubmitted({ runID, state }) { + this.pluginCrashData.delete(runID); + let contentWindow = this.global.content; + let cwu = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + let plugins = cwu.plugins; + + for (let plugin of plugins) { + if (plugin instanceof Ci.nsIObjectLoadingContent && + plugin.runID == runID) { + let statusDiv = this.getPluginUI(plugin, "submitStatus"); + statusDiv.setAttribute("status", state); + } + } + }, + + GMPCrashed(aEvent) { + let target = aEvent.target; + let pluginName = aEvent.pluginName; + let gmpPlugin = aEvent.gmpPlugin; + let pluginID = aEvent.pluginID; + let doc = target.document; + + if (!gmpPlugin || !doc) { + // TODO: Throw exception? How did we get here? + return; + } + + let messageString = + gNavigatorBundle.formatStringFromName("crashedpluginsMessage.title", + [pluginName], 1); + + this.global.sendAsyncMessage("PluginContent:ShowPluginCrashedNotification", + { messageString, pluginID }); + + // Remove the notification when the page is reloaded. + doc.defaultView.top.addEventListener("unload", event => { + this.hideNotificationBar("plugin-crashed"); + }); + }, +}; |