diff -r 000000000000 -r 6474c204b198 browser/base/content/browser-plugins.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/browser/base/content/browser-plugins.js Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,1332 @@ +# -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- +# 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/. + +var gPluginHandler = { + PREF_NOTIFY_MISSING_FLASH: "plugins.notifyMissingFlash", + PREF_HIDE_MISSING_PLUGINS_NOTIFICATION: "plugins.hideMissingPluginsNotification", + PREF_SESSION_PERSIST_MINUTES: "plugin.sessionPermissionNow.intervalInMinutes", + PREF_PERSISTENT_DAYS: "plugin.persistentPermissionAlways.intervalInDays", + + getPluginUI: function (plugin, anonid) { + return plugin.ownerDocument. + getAnonymousElementByAttribute(plugin, "anonid", anonid); + }, + +#ifdef MOZ_CRASHREPORTER + get CrashSubmit() { + delete this.CrashSubmit; + Cu.import("resource://gre/modules/CrashSubmit.jsm", this); + return this.CrashSubmit; + }, +#endif + + _getPluginInfo: function (pluginElement) { + let pluginHost = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost); + pluginElement.QueryInterface(Ci.nsIObjectLoadingContent); + + let tagMimetype; + let pluginName = gNavigatorBundle.getString("pluginInfo.unknownPlugin"); + let pluginTag = null; + let permissionString = null; + let fallbackType = null; + let blocklistState = null; + + tagMimetype = pluginElement.actualType; + if (tagMimetype == "") { + tagMimetype = pluginElement.type; + } + + if (gPluginHandler.isKnownPlugin(pluginElement)) { + pluginTag = pluginHost.getPluginTagForType(pluginElement.actualType); + pluginName = gPluginHandler.makeNicePluginName(pluginTag.name); + + 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: pluginName, + pluginTag: pluginTag, + permissionString: permissionString, + fallbackType: fallbackType, + blocklistState: blocklistState, + }; + }, + + // Map the plugin's name to a filtered version more suitable for user UI. + makeNicePluginName : function (aName) { + if (aName == "Shockwave Flash") + return "Adobe Flash"; + + // Clean up the plugin name by stripping off parenthetical clauses, + // trailing version numbers or "plugin". + // EG, "Foo Bar (Linux) Plugin 1.23_02" --> "Foo Bar" + // Do this by first stripping the numbers, etc. off the end, and then + // removing "Plugin" (and then trimming to get rid of any whitespace). + // (Otherwise, something like "Java(TM) Plug-in 1.7.0_07" gets mangled) + let newName = aName.replace(/\(.*?\)/g, ""). + replace(/[\s\d\.\-\_\(\)]+$/, ""). + replace(/\bplug-?in\b/i, "").trim(); + return newName; + }, + + /** + * Update the visibility of the plugin overlay. + */ + setVisibility : function (plugin, overlay, shouldShow) { + overlay.classList.toggle("visible", shouldShow); + }, + + /** + * 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 : function (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 '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 > pluginRect.width) || + (overlay.scrollHeight - 5 > 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.ownerDocument.defaultView; + 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: function (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 : function(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"; + case Ci.nsIObjectLoadingContent.PLUGIN_PLAY_PREVIEW: + return "PluginPlayPreview"; + default: + // Not all states map to a handler + return null; + } + }, + + supportedPlugins: { + "mimetypes": { + "application/x-shockwave-flash": "flash", + "application/futuresplash": "flash", + "application/x-java-.*": "java", + "application/x-director": "shockwave", + "application/(sdp|x-(mpeg|rtsp|sdp))": "quicktime", + "audio/(3gpp(2)?|AMR|aiff|basic|mid(i)?|mp4|mpeg|vnd\.qcelp|wav|x-(aiff|m4(a|b|p)|midi|mpeg|wav))": "quicktime", + "image/(pict|png|tiff|x-(macpaint|pict|png|quicktime|sgi|targa|tiff))": "quicktime", + "video/(3gpp(2)?|flc|mp4|mpeg|quicktime|sd-video|x-mpeg)": "quicktime", + "application/x-unknown": "test", + }, + + "plugins": { + "flash": { + "displayName": "Flash", + "installWINNT": true, + "installDarwin": true, + "installLinux": true, + }, + "java": { + "displayName": "Java", + "installWINNT": true, + "installDarwin": true, + "installLinux": true, + }, + "shockwave": { + "displayName": "Shockwave", + "installWINNT": true, + "installDarwin": true, + }, + "quicktime": { + "displayName": "QuickTime", + "installWINNT": true, + }, + "test": { + "displayName": "Test plugin", + "installWINNT": true, + "installLinux": true, + "installDarwin": true, + } + } + }, + + nameForSupportedPlugin: function (aMimeType) { + for (let type in this.supportedPlugins.mimetypes) { + let re = new RegExp(type); + if (re.test(aMimeType)) { + return this.supportedPlugins.mimetypes[type]; + } + } + return null; + }, + + canInstallThisMimeType: function (aMimeType) { + let os = Services.appinfo.OS; + let pluginName = this.nameForSupportedPlugin(aMimeType); + if (pluginName && "install" + os in this.supportedPlugins.plugins[pluginName]) { + return true; + } + return false; + }, + + handleEvent : function(event) { + let eventType = event.type; + + if (eventType == "PluginRemoved") { + let doc = event.target; + let browser = gBrowser.getBrowserForDocument(doc.defaultView.top.document); + if (browser) + this._setPluginNotificationIcon(browser); + return; + } + + let plugin = event.target; + let doc = plugin.ownerDocument; + + 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; + let browser = gBrowser.getBrowserForDocument(doc.defaultView.top.document); + if (!browser) + return; + + switch (eventType) { + case "PluginCrashed": + this.pluginInstanceCrashed(plugin, event); + break; + + case "PluginNotFound": + let installable = this.showInstallNotification(plugin, eventType); + // For non-object plugin tags, register a click handler to install the + // plugin. Object tags can, and often do, deal with that themselves, + // so don't stomp on the page developers toes. + if (installable && !(plugin instanceof HTMLObjectElement)) { + let installStatus = this.getPluginUI(plugin, "installStatus"); + installStatus.setAttribute("installable", "true"); + let iconStatus = this.getPluginUI(plugin, "icon"); + iconStatus.setAttribute("installable", "true"); + + let installLink = this.getPluginUI(plugin, "installPluginLink"); + this.addLinkClickCallback(installLink, "installSinglePlugin", plugin); + } + break; + + case "PluginBlocklisted": + case "PluginOutdated": + shouldShowNotification = true; + break; + + case "PluginVulnerableUpdatable": + let updateLink = this.getPluginUI(plugin, "checkForUpdatesLink"); + this.addLinkClickCallback(updateLink, "openPluginUpdatePage"); + /* FALLTHRU */ + + case "PluginVulnerableNoUpdate": + case "PluginClickToPlay": + this._handleClickToPlayEvent(plugin); + let overlay = this.getPluginUI(plugin, "main"); + let pluginName = this._getPluginInfo(plugin).pluginName; + let messageString = gNavigatorBundle.getFormattedString("PluginClickToActivate", [pluginName]); + let overlayText = this.getPluginUI(plugin, "clickToPlay"); + overlayText.textContent = messageString; + if (eventType == "PluginVulnerableUpdatable" || + eventType == "PluginVulnerableNoUpdate") { + let vulnerabilityString = gNavigatorBundle.getString(eventType); + let vulnerabilityText = this.getPluginUI(plugin, "vulnerabilityStatus"); + vulnerabilityText.textContent = vulnerabilityString; + } + shouldShowNotification = true; + break; + + case "PluginPlayPreview": + this._handlePlayPreviewEvent(plugin); + break; + + case "PluginDisabled": + // Screw the disabled message. It messes with HTML5 fallback on YouTube + let plugin_overlay = doc.getAnonymousElementByAttribute(plugin, "class", "mainBox"); + if (plugin_overlay != null) + plugin_overlay.style.visibility = "hidden"; + break; + + case "PluginInstantiated": + shouldShowNotification = true; + break; + } + + // Show the in-content UI if it's not too big. The crashed plugin handler already did this. + if (eventType != "PluginCrashed") { + let overlay = this.getPluginUI(plugin, "main"); + if (overlay != null) { + this.setVisibility(plugin, overlay, + this.shouldShowOverlay(plugin, overlay)); + let resizeListener = (event) => { + this.setVisibility(plugin, overlay, + this.shouldShowOverlay(plugin, overlay)); + this._setPluginNotificationIcon(browser); + }; + plugin.addEventListener("overflow", resizeListener); + plugin.addEventListener("underflow", resizeListener); + } + } + + let closeIcon = this.getPluginUI(plugin, "closeIcon"); + if (closeIcon) { + closeIcon.addEventListener("click", function(aEvent) { + if (aEvent.button == 0 && aEvent.isTrusted) + gPluginHandler.hideClickToPlayOverlay(plugin); + }, true); + } + + if (shouldShowNotification) { + this._showClickToPlayNotification(browser, plugin, false); + } + }, + + isKnownPlugin: function PH_isKnownPlugin(objLoadingContent) { + return (objLoadingContent.getContentTypeForMIMEType(objLoadingContent.actualType) == + Ci.nsIObjectLoadingContent.TYPE_PLUGIN); + }, + + canActivatePlugin: function PH_canActivatePlugin(objLoadingContent) { + // if this isn't a known plugin, we can't activate it + // (this also guards pluginHost.getPermissionStringForType against + // unexpected input) + if (!gPluginHandler.isKnownPlugin(objLoadingContent)) + return false; + + let pluginHost = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost); + let permissionString = pluginHost.getPermissionStringForType(objLoadingContent.actualType); + let principal = objLoadingContent.ownerDocument.defaultView.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; + + if (objLoadingContent.pluginFallbackType == Ci.nsIObjectLoadingContent.PLUGIN_PLAY_PREVIEW) { + // checking if play preview is subject to CTP rules + let playPreviewInfo = pluginHost.getPlayPreviewInfo(objLoadingContent.actualType); + isFallbackTypeValid = !playPreviewInfo.ignoreCTP; + } + + return !objLoadingContent.activated && + pluginPermission != Ci.nsIPermissionManager.DENY_ACTION && + isFallbackTypeValid; + }, + + hideClickToPlayOverlay: function(aPlugin) { + let overlay = this.getPluginUI(aPlugin, "main"); + if (overlay) { + overlay.classList.remove("visible"); + } + }, + + stopPlayPreview: function PH_stopPlayPreview(aPlugin, aPlayPlugin) { + let objLoadingContent = aPlugin.QueryInterface(Ci.nsIObjectLoadingContent); + if (objLoadingContent.activated) + return; + + if (aPlayPlugin) + objLoadingContent.playPlugin(); + else + objLoadingContent.cancelPlayPreview(); + }, + + newPluginInstalled : function(event) { + // browser elements are anonymous so we can't just use target. + var browser = event.originalTarget; + // clear the plugin list, now that at least one plugin has been installed + browser.missingPlugins = null; + + var notificationBox = gBrowser.getNotificationBox(browser); + var notification = notificationBox.getNotificationWithValue("missing-plugins"); + if (notification) + notificationBox.removeNotification(notification); + + // reload the browser to make the new plugin show. + browser.reload(); + }, + + // Callback for user clicking on a missing (unsupported) plugin. + installSinglePlugin: function (plugin) { + var missingPlugins = new Map(); + + var pluginInfo = this._getPluginInfo(plugin); + missingPlugins.set(pluginInfo.mimetype, pluginInfo); + + openDialog("chrome://mozapps/content/plugins/pluginInstallerWizard.xul", + "PFSWindow", "chrome,centerscreen,resizable=yes", + {plugins: missingPlugins, browser: gBrowser.selectedBrowser}); + }, + + // Callback for user clicking on a disabled plugin + managePlugins: function (aEvent) { + BrowserOpenAddonsMgr("addons://list/plugin"); + }, + + // Callback for user clicking on the link in a click-to-play plugin + // (where the plugin has an update) + openPluginUpdatePage: function (aEvent) { + openUILinkIn(Services.urlFormatter.formatURLPref("plugins.update.url"), "tab"); + }, + +#ifdef MOZ_CRASHREPORTER + submitReport: function submitReport(pluginDumpID, browserDumpID, plugin) { + let keyVals = {}; + if (plugin) { + let userComment = this.getPluginUI(plugin, "submitComment").value.trim(); + if (userComment) + keyVals.PluginUserComment = userComment; + if (this.getPluginUI(plugin, "submitURLOptIn").checked) + keyVals.PluginContentURL = plugin.ownerDocument.URL; + } + this.CrashSubmit.submit(pluginDumpID, { extraExtraKeyVals: keyVals }); + if (browserDumpID) + this.CrashSubmit.submit(browserDumpID); + }, +#endif + + // Callback for user clicking a "reload page" link + reloadPage: function (browser) { + browser.reload(); + }, + + // Callback for user clicking the help icon + openHelpPage: function () { + openHelpLink("plugin-crashed", false); + }, + + showInstallNotification: function (aPlugin) { + let hideMissingPluginsNotification = + Services.prefs.getBoolPref(this.PREF_HIDE_MISSING_PLUGINS_NOTIFICATION); + if (hideMissingPluginsNotification) { + return false; + } + + let browser = gBrowser.getBrowserForDocument(aPlugin.ownerDocument + .defaultView.top.document); + if (!browser.missingPlugins) + browser.missingPlugins = new Map(); + + let pluginInfo = this._getPluginInfo(aPlugin); + browser.missingPlugins.set(pluginInfo.mimetype, pluginInfo); + + // only show notification for small subset of plugins + let mimetype = pluginInfo.mimetype.split(";")[0]; + if (!this.canInstallThisMimeType(mimetype)) + return false; + + let pluginIdentifier = this.nameForSupportedPlugin(mimetype); + if (!pluginIdentifier) + return false; + + let displayName = this.supportedPlugins.plugins[pluginIdentifier].displayName; + + // don't show several notifications + let notification = PopupNotifications.getNotification("plugins-not-found", browser); + if (notification) + return true; + + let messageString = gNavigatorBundle.getString("installPlugin.message"); + let mainAction = { + label: gNavigatorBundle.getFormattedString("installPlugin.button.label", + [displayName]), + accessKey: gNavigatorBundle.getString("installPlugin.button.accesskey"), + callback: function () { + openDialog("chrome://mozapps/content/plugins/pluginInstallerWizard.xul", + "PFSWindow", "chrome,centerscreen,resizable=yes", + {plugins: browser.missingPlugins, browser: browser}); + } + }; + let secondaryActions = null; + let options = { dismissed: true }; + + let showForFlash = Services.prefs.getBoolPref(this.PREF_NOTIFY_MISSING_FLASH); + if (pluginIdentifier == "flash" && showForFlash) { + let prefNotifyMissingFlash = this.PREF_NOTIFY_MISSING_FLASH; + secondaryActions = [{ + label: gNavigatorBundle.getString("installPlugin.ignoreButton.label"), + accessKey: gNavigatorBundle.getString("installPlugin.ignoreButton.accesskey"), + callback: function () { + Services.prefs.setBoolPref(prefNotifyMissingFlash, false); + } + }]; + options.dismissed = false; + } + PopupNotifications.show(browser, "plugins-not-found", + messageString, "plugin-install-notification-icon", + mainAction, secondaryActions, options); + return true; + }, + // Event listener for click-to-play plugins. + _handleClickToPlayEvent: function PH_handleClickToPlayEvent(aPlugin) { + let doc = aPlugin.ownerDocument; + let browser = gBrowser.getBrowserForDocument(doc.defaultView.top.document); + let pluginHost = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost); + let objLoadingContent = aPlugin.QueryInterface(Ci.nsIObjectLoadingContent); + // guard against giving pluginHost.getPermissionStringForType a type + // not associated with any known plugin + if (!gPluginHandler.isKnownPlugin(objLoadingContent)) + return; + let permissionString = pluginHost.getPermissionStringForType(objLoadingContent.actualType); + let principal = doc.defaultView.top.document.nodePrincipal; + let pluginPermission = Services.perms.testPermissionFromPrincipal(principal, permissionString); + + let overlay = this.getPluginUI(aPlugin, "main"); + + if (pluginPermission == Ci.nsIPermissionManager.DENY_ACTION) { + if (overlay) { + overlay.classList.remove("visible"); + } + return; + } + + if (overlay) { + overlay.addEventListener("click", gPluginHandler._overlayClickListener, true); + } + }, + + _overlayClickListener: { + handleEvent: function PH_handleOverlayClick(aEvent) { + let plugin = document.getBindingParent(aEvent.target); + let contentWindow = plugin.ownerDocument.defaultView.top; + // gBrowser.getBrowserForDocument does not exist in the case where we + // drag-and-dropped a tab from a window containing only that tab. In + // that case, the window gets destroyed. + let browser = gBrowser.getBrowserForDocument ? + gBrowser.getBrowserForDocument(contentWindow.document) : + null; + // If browser is null here, we've been drag-and-dropped from another + // window, and this is the wrong click handler. + if (!browser) { + aEvent.target.removeEventListener("click", gPluginHandler._overlayClickListener, true); + return; + } + let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + // Have to check that the target is not the link to update the plugin + if (!(aEvent.originalTarget instanceof HTMLAnchorElement) && + (aEvent.originalTarget.getAttribute('anonid') != 'closeIcon') && + aEvent.button == 0 && aEvent.isTrusted) { + gPluginHandler._showClickToPlayNotification(browser, plugin, true); + aEvent.stopPropagation(); + aEvent.preventDefault(); + } + } + }, + + _handlePlayPreviewEvent: function PH_handlePlayPreviewEvent(aPlugin) { + let doc = aPlugin.ownerDocument; + let browser = gBrowser.getBrowserForDocument(doc.defaultView.top.document); + let pluginHost = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost); + let pluginInfo = this._getPluginInfo(aPlugin); + let playPreviewInfo = pluginHost.getPlayPreviewInfo(pluginInfo.mimetype); + + let previewContent = this.getPluginUI(aPlugin, "previewPluginContent"); + let iframe = previewContent.getElementsByClassName("previewPluginContentFrame")[0]; + if (!iframe) { + // lazy initialization of the iframe + iframe = doc.createElementNS("http://www.w3.org/1999/xhtml", "iframe"); + iframe.className = "previewPluginContentFrame"; + previewContent.appendChild(iframe); + + // Force a style flush, so that we ensure our binding is attached. + aPlugin.clientTop; + } + iframe.src = playPreviewInfo.redirectURL; + + // MozPlayPlugin event can be dispatched from the extension chrome + // code to replace the preview content with the native plugin + previewContent.addEventListener("MozPlayPlugin", function playPluginHandler(aEvent) { + if (!aEvent.isTrusted) + return; + + previewContent.removeEventListener("MozPlayPlugin", playPluginHandler, true); + + let playPlugin = !aEvent.detail; + gPluginHandler.stopPlayPreview(aPlugin, playPlugin); + + // cleaning up: removes overlay iframe from the DOM + let iframe = previewContent.getElementsByClassName("previewPluginContentFrame")[0]; + if (iframe) + previewContent.removeChild(iframe); + }, true); + + if (!playPreviewInfo.ignoreCTP) { + gPluginHandler._showClickToPlayNotification(browser, aPlugin, false); + } + }, + + reshowClickToPlayNotification: function PH_reshowClickToPlayNotification() { + let browser = gBrowser.selectedBrowser; + let contentWindow = browser.contentWindow; + 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", gPluginHandler._overlayClickListener, true); + let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + if (gPluginHandler.canActivatePlugin(objLoadingContent)) + gPluginHandler._handleClickToPlayEvent(plugin); + } + gPluginHandler._showClickToPlayNotification(browser, null, false); + }, + + _clickToPlayNotificationEventCallback: function PH_ctpEventCallback(event) { + if (event == "showing") { + Services.telemetry.getHistogramById("PLUGINS_NOTIFICATION_SHOWN") + .add(!this.options.primaryPlugin); + // Histograms always start at 0, even though our data starts at 1 + let histogramCount = this.options.pluginData.size - 1; + if (histogramCount > 4) { + histogramCount = 4; + } + Services.telemetry.getHistogramById("PLUGINS_NOTIFICATION_PLUGIN_COUNT") + .add(histogramCount); + } + else if (event == "dismissed") { + // Once the popup is dismissed, clicking the icon should show the full + // list again + this.options.primaryPlugin = null; + } + }, + + // Match the behaviour of nsPermissionManager + _getHostFromPrincipal: function PH_getHostFromPrincipal(principal) { + if (!principal.URI || principal.URI.schemeIs("moz-nullprincipal")) { + return "(null)"; + } + + try { + if (principal.URI.host) + return principal.URI.host; + } catch (e) {} + + return principal.origin; + }, + + /** + * Called from the plugin doorhanger to set the new permissions for a plugin + * and activate plugins if necessary. + * aNewState should be either "allownow" "allowalways" or "block" + */ + _updatePluginPermission: function PH_setPermissionForPlugins(aNotification, aPluginInfo, aNewState) { + let permission; + let expireType; + let expireTime; + let histogram = + Services.telemetry.getHistogramById("PLUGINS_NOTIFICATION_USER_ACTION"); + + // Update the permission manager. + // Also update the current state of pluginInfo.fallbackType so that + // subsequent opening of the notification shows the current state. + switch (aNewState) { + case "allownow": + permission = Ci.nsIPermissionManager.ALLOW_ACTION; + expireType = Ci.nsIPermissionManager.EXPIRE_SESSION; + expireTime = Date.now() + Services.prefs.getIntPref(this.PREF_SESSION_PERSIST_MINUTES) * 60 * 1000; + histogram.add(0); + aPluginInfo.fallbackType = Ci.nsIObjectLoadingContent.PLUGIN_ACTIVE; + break; + + case "allowalways": + permission = Ci.nsIPermissionManager.ALLOW_ACTION; + expireType = Ci.nsIPermissionManager.EXPIRE_TIME; + expireTime = Date.now() + + Services.prefs.getIntPref(this.PREF_PERSISTENT_DAYS) * 24 * 60 * 60 * 1000; + histogram.add(1); + aPluginInfo.fallbackType = Ci.nsIObjectLoadingContent.PLUGIN_ACTIVE; + break; + + case "block": + permission = Ci.nsIPermissionManager.PROMPT_ACTION; + expireType = Ci.nsIPermissionManager.EXPIRE_NEVER; + expireTime = 0; + histogram.add(2); + switch (aPluginInfo.blocklistState) { + case Ci.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE: + aPluginInfo.fallbackType = Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_UPDATABLE; + break; + case Ci.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE: + aPluginInfo.fallbackType = Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_NO_UPDATE; + break; + default: + aPluginInfo.fallbackType = Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY; + } + break; + + // In case a plugin has already been allowed in another tab, the "continue allowing" button + // shouldn't change any permissions but should run the plugin-enablement code below. + case "continue": + aPluginInfo.fallbackType = Ci.nsIObjectLoadingContent.PLUGIN_ACTIVE; + break; + default: + Cu.reportError(Error("Unexpected plugin state: " + aNewState)); + return; + } + + let browser = aNotification.browser; + let contentWindow = browser.contentWindow; + if (aNewState != "continue") { + let principal = contentWindow.document.nodePrincipal; + Services.perms.addFromPrincipal(principal, aPluginInfo.permissionString, + permission, expireType, expireTime); + aPluginInfo.pluginPermissionType = expireType; + } + + // Manually activate the plugins that would have been automatically + // activated. + 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; + for (let plugin of plugins) { + plugin.QueryInterface(Ci.nsIObjectLoadingContent); + if (!gPluginHandler.isKnownPlugin(plugin)) { + continue; + } + if (aPluginInfo.permissionString == pluginHost.getPermissionStringForType(plugin.actualType)) { + pluginFound = true; + if (aNewState == "block") { + plugin.reload(true); + } else { + if (gPluginHandler.canActivatePlugin(plugin)) { + let overlay = this.getPluginUI(plugin, "main"); + if (overlay) { + overlay.removeEventListener("click", gPluginHandler._overlayClickListener, 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. + if (aNewState != "block" && !pluginFound) { + browser.reload(); + } + + this._setPluginNotificationIcon(browser); + }, + + _showClickToPlayNotification: function PH_showClickToPlayNotification(aBrowser, aPlugin, aShowNow) { + let notification = PopupNotifications.getNotification("click-to-play-plugins", aBrowser); + let plugins = []; + + // if aPlugin is null, that means the user has navigated back to a page with plugins, and we need + // to collect all the plugins + if (aPlugin === null) { + let contentWindow = aBrowser.contentWindow; + let contentDoc = aBrowser.contentDocument; + let cwu = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + // cwu.plugins may contain non-plugin s, filter them out + plugins = cwu.plugins.filter((plugin) => + plugin.getContentTypeForMIMEType(plugin.actualType) == Ci.nsIObjectLoadingContent.TYPE_PLUGIN); + + if (plugins.length == 0) { + if (notification) { + PopupNotifications.remove(notification); + } + return; + } + } else { + plugins = [aPlugin]; + } + + // If this is a new notification, create a pluginData map, otherwise append + let pluginData; + if (notification) { + pluginData = notification.options.pluginData; + } else { + pluginData = new Map(); + } + + let principal = aBrowser.contentDocument.nodePrincipal; + let principalHost = this._getHostFromPrincipal(principal); + + for (var plugin of plugins) { + let pluginInfo = this._getPluginInfo(plugin); + 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.pluginPermissionHost = permissionObj.host; + pluginInfo.pluginPermissionType = permissionObj.expireType; + } + else { + pluginInfo.pluginPermissionHost = principalHost; + pluginInfo.pluginPermissionType = undefined; + } + + let url; + // TODO: allow the blocklist to specify a better link, bug 873093 + if (pluginInfo.blocklistState == Ci.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE) { + url = Services.urlFormatter.formatURLPref("plugins.update.url"); + } + else if (pluginInfo.blocklistState != Ci.nsIBlocklistService.STATE_NOT_BLOCKED) { + url = Services.blocklist.getPluginBlocklistURL(pluginInfo.pluginTag); + } + else { + url = Services.urlFormatter.formatURLPref("app.support.baseURL") + "clicktoplay"; + } + pluginInfo.detailsLink = url; + + pluginData.set(pluginInfo.permissionString, pluginInfo); + } + + let primaryPluginPermission = null; + if (aShowNow) { + primaryPluginPermission = this._getPluginInfo(aPlugin).permissionString; + } + + if (notification) { + // Don't modify the notification UI while it's on the screen, that would be + // jumpy and might allow clickjacking. + if (aShowNow) { + notification.options.primaryPlugin = primaryPluginPermission; + notification.reshow(); + setTimeout(() => { this._setPluginNotificationIcon(aBrowser); }, 0); + } + return; + } + + let options = { + dismissed: !aShowNow, + eventCallback: this._clickToPlayNotificationEventCallback, + primaryPlugin: primaryPluginPermission, + pluginData: pluginData + }; + PopupNotifications.show(aBrowser, "click-to-play-plugins", + "", "plugins-notification-icon", + null, null, options); + setTimeout(() => { this._setPluginNotificationIcon(aBrowser); }, 0); + }, + + _setPluginNotificationIcon : function PH_setPluginNotificationIcon(aBrowser) { + // Because this is called on a timeout, sanity-check before continuing + if (!aBrowser.docShell || !aBrowser.contentWindow) { + return; + } + + let notification = PopupNotifications.getNotification("click-to-play-plugins", aBrowser); + if (!notification) + return; + + // Make a copy of the actions, removing active plugins and checking for + // outdated plugins. + let haveInsecure = false; + let actions = new Map(); + for (let action of notification.options.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; + } + } + + // check for hidden plugins + let contentWindow = aBrowser.contentWindow; + let contentDoc = aBrowser.contentDocument; + let cwu = contentWindow.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; + } + } + } + + // Set up the icon + document.getElementById("plugins-notification-icon").classList. + toggle("plugin-blocked", haveInsecure); + + // Now configure the notification bar + + let notificationBox = gBrowser.getNotificationBox(aBrowser); + + function hideNotification() { + let n = notificationBox.getNotificationWithValue("plugin-hidden"); + if (n) { + notificationBox.removeNotification(n, true); + } + } + + // There are three different cases when showing an infobar: + // 1. A single type of plugin is hidden on the page. Show the UI for that + // plugin. + // 2a. Multiple types of plugins are hidden on the page. Show the multi-UI + // with the vulnerable styling. + // 2b. Multiple types of plugins are hidden on the page, but none are + // vulnerable. Show the nonvulnerable multi-UI. + function showNotification() { + let n = notificationBox.getNotificationWithValue("plugin-hidden"); + if (n) { + // If something is already shown, just keep it + return; + } + + Services.telemetry.getHistogramById("PLUGINS_INFOBAR_SHOWN"). + add(true); + + let message; + // Icons set directly cannot be manipulated using moz-image-region, so + // we use CSS classes instead. + let host = gPluginHandler._getHostFromPrincipal(aBrowser.contentDocument.nodePrincipal); + let brand = document.getElementById("bundle_brand").getString("brandShortName"); + + if (actions.size == 1) { + let pluginInfo = [...actions.values()][0]; + let pluginName = pluginInfo.pluginName; + + switch (pluginInfo.fallbackType) { + case Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY: + message = gNavigatorBundle.getFormattedString( + "pluginActivateNew.message", + [pluginName, host]); + break; + case Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_UPDATABLE: + message = gNavigatorBundle.getFormattedString( + "pluginActivateOutdated.message", + [pluginName, host, brand]); + break; + case Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_NO_UPDATE: + message = gNavigatorBundle.getFormattedString( + "pluginActivateVulnerable.message", + [pluginName, host, brand]); + } + } else { + // Multi-plugin + message = gNavigatorBundle.getFormattedString( + "pluginActivateMultiple.message", [host]); + + for (let action of actions.values()) { + if (action.fallbackType != Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY) { + break; + } + } + } + + let buttons = [ + { + label: gNavigatorBundle.getString("pluginContinueBlocking.label"), + accessKey: gNavigatorBundle.getString("pluginContinueBlocking.accesskey"), + callback: function() { + Services.telemetry.getHistogramById("PLUGINS_INFOBAR_BLOCK"). + add(true); + + Services.perms.addFromPrincipal(aBrowser.contentDocument.nodePrincipal, + "plugin-hidden-notification", + Services.perms.DENY_ACTION); + } + }, + { + label: gNavigatorBundle.getString("pluginActivateTrigger.label"), + accessKey: gNavigatorBundle.getString("pluginActivateTrigger.accesskey"), + callback: function() { + Services.telemetry.getHistogramById("PLUGINS_INFOBAR_ALLOW"). + add(true); + + let curNotification = + PopupNotifications.getNotification("click-to-play-plugins", + aBrowser); + if (curNotification) { + curNotification.reshow(); + } + } + } + ]; + n = notificationBox. + appendNotification(message, "plugin-hidden", null, + notificationBox.PRIORITY_INFO_HIGH, buttons); + if (haveInsecure) { + n.classList.add('pluginVulnerable'); + } + } + + if (actions.size == 0) { + hideNotification(); + } else { + let notificationPermission = Services.perms.testPermissionFromPrincipal( + aBrowser.contentDocument.nodePrincipal, "plugin-hidden-notification"); + if (notificationPermission == Ci.nsIPermissionManager.DENY_ACTION) { + hideNotification(); + } else { + showNotification(); + } + } + }, + + // Crashed-plugin observer. Notified once per plugin crash, before events + // are dispatched to individual plugin instances. + pluginCrashed : function(subject, topic, data) { + let propertyBag = subject; + if (!(propertyBag instanceof Ci.nsIPropertyBag2) || + !(propertyBag instanceof Ci.nsIWritablePropertyBag2)) + return; + +#ifdef MOZ_CRASHREPORTER + let pluginDumpID = propertyBag.getPropertyAsAString("pluginDumpID"); + let browserDumpID= propertyBag.getPropertyAsAString("browserDumpID"); + let shouldSubmit = gCrashReporter.submitReports; + let doPrompt = true; // XXX followup to get via gCrashReporter + + // Submit automatically when appropriate. + if (pluginDumpID && shouldSubmit && !doPrompt) { + this.submitReport(pluginDumpID, browserDumpID); + // Submission is async, so we can't easily show failure UI. + propertyBag.setPropertyAsBool("submittedCrashReport", true); + } +#endif + }, + + // Crashed-plugin event listener. Called for every instance of a + // plugin in content. + pluginInstanceCrashed: function (plugin, aEvent) { + // Ensure the plugin and event are of the right type. + if (!(aEvent instanceof Ci.nsIDOMCustomEvent)) + return; + + let propBag = aEvent.detail.QueryInterface(Ci.nsIPropertyBag2); + let submittedReport = propBag.getPropertyAsBool("submittedCrashReport"); + let doPrompt = true; // XXX followup for .getPropertyAsBool("doPrompt"); + let submitReports = true; // XXX followup for .getPropertyAsBool("submitReports"); + let pluginName = propBag.getPropertyAsAString("pluginName"); + let pluginDumpID = propBag.getPropertyAsAString("pluginDumpID"); + let browserDumpID = propBag.getPropertyAsAString("browserDumpID"); + + // Remap the plugin name to a more user-presentable form. + pluginName = this.makeNicePluginName(pluginName); + + let messageString = gNavigatorBundle.getFormattedString("crashedpluginsMessage.title", [pluginName]); + + // + // Configure the crashed-plugin placeholder. + // + + // Force a layout flush so the binding is attached. + plugin.clientTop; + let overlay = this.getPluginUI(plugin, "main"); + let statusDiv = this.getPluginUI(plugin, "submitStatus"); + let doc = plugin.ownerDocument; +#ifdef MOZ_CRASHREPORTER + let status; + + // Determine which message to show regarding crash reports. + if (submittedReport) { // submitReports && !doPrompt, handled in observer + status = "submitted"; + } + else if (!submitReports && !doPrompt) { + status = "noSubmit"; + } + else { // doPrompt + status = "please"; + this.getPluginUI(plugin, "submitButton").addEventListener("click", + function (event) { + if (event.button != 0 || !event.isTrusted) + return; + this.submitReport(pluginDumpID, browserDumpID, plugin); + pref.setBoolPref("", optInCB.checked); + }.bind(this)); + let optInCB = this.getPluginUI(plugin, "submitURLOptIn"); + let pref = Services.prefs.getBranch("dom.ipc.plugins.reportCrashURL"); + optInCB.checked = pref.getBoolPref(""); + } + + // If we don't have a minidumpID, we can't (or didn't) submit anything. + // This can happen if the plugin is killed from the task manager. + if (!pluginDumpID) { + status = "noReport"; + } + + statusDiv.setAttribute("status", status); + + let helpIcon = this.getPluginUI(plugin, "helpIcon"); + this.addLinkClickCallback(helpIcon, "openHelpPage"); + + // If we're showing the link to manually trigger report submission, we'll + // want to be able to update all the instances of the UI for this crash to + // show an updated message when a report is submitted. + if (doPrompt) { + let observer = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, + Ci.nsISupportsWeakReference]), + observe : function(subject, topic, data) { + let propertyBag = subject; + if (!(propertyBag instanceof Ci.nsIPropertyBag2)) + return; + // Ignore notifications for other crashes. + if (propertyBag.get("minidumpID") != pluginDumpID) + return; + statusDiv.setAttribute("status", data); + }, + + handleEvent : function(event) { + // Not expected to be called, just here for the closure. + } + } + + // Use a weak reference, so we don't have to remove it... + Services.obs.addObserver(observer, "crash-report-status", true); + // ...alas, now we need something to hold a strong reference to prevent + // it from being GC. But I don't want to manually manage the reference's + // lifetime (which should be no greater than the page). + // Clever solution? Use a closue with an event listener on the document. + // When the doc goes away, so do the listener references and the closure. + doc.addEventListener("mozCleverClosureHack", observer, false); + } +#endif + + let crashText = this.getPluginUI(plugin, "crashedText"); + crashText.textContent = messageString; + + let browser = gBrowser.getBrowserForDocument(doc.defaultView.top.document); + + let link = this.getPluginUI(plugin, "reloadLink"); + this.addLinkClickCallback(link, "reloadPage", browser); + + let notificationBox = gBrowser.getNotificationBox(browser); + + let isShowing = this.shouldShowOverlay(plugin, overlay); + + // Is the '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); + + 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. + hideNotificationBar(); + doc.mozNoPluginCrashedNotification = true; + } else { + // If another plugin on the page was large enough to show our UI, we don't + // want to show a notification bar. + if (!doc.mozNoPluginCrashedNotification) + showNotificationBar(pluginDumpID, browserDumpID); + } + + function hideNotificationBar() { + let notification = notificationBox.getNotificationWithValue("plugin-crashed"); + if (notification) + notificationBox.removeNotification(notification, true); + } + + function showNotificationBar(pluginDumpID, browserDumpID) { + // If there's already an existing notification bar, don't do anything. + let notification = notificationBox.getNotificationWithValue("plugin-crashed"); + if (notification) + return; + + // Configure the notification bar + let priority = notificationBox.PRIORITY_WARNING_MEDIUM; + let iconURL = "chrome://mozapps/skin/plugins/notifyPluginCrashed.png"; + let reloadLabel = gNavigatorBundle.getString("crashedpluginsMessage.reloadButton.label"); + let reloadKey = gNavigatorBundle.getString("crashedpluginsMessage.reloadButton.accesskey"); + let submitLabel = gNavigatorBundle.getString("crashedpluginsMessage.submitButton.label"); + let submitKey = gNavigatorBundle.getString("crashedpluginsMessage.submitButton.accesskey"); + + let buttons = [{ + label: reloadLabel, + accessKey: reloadKey, + popup: null, + callback: function() { browser.reload(); }, + }]; +#ifdef MOZ_CRASHREPORTER + let submitButton = { + label: submitLabel, + accessKey: submitKey, + popup: null, + callback: function() { gPluginHandler.submitReport(pluginDumpID, browserDumpID); }, + }; + if (pluginDumpID) + buttons.push(submitButton); +#endif + + let notification = notificationBox.appendNotification(messageString, "plugin-crashed", + iconURL, priority, buttons); + + // Add the "learn more" link. + let XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + let link = notification.ownerDocument.createElementNS(XULNS, "label"); + link.className = "text-link"; + link.setAttribute("value", gNavigatorBundle.getString("crashedpluginsMessage.learnMore")); + let crashurl = formatURL("app.support.baseURL", true); + crashurl += "plugin-crashed-notificationbar"; + link.href = crashurl; + + let description = notification.ownerDocument.getAnonymousElementByAttribute(notification, "anonid", "messageText"); + description.appendChild(link); + + // Remove the notfication when the page is reloaded. + doc.defaultView.top.addEventListener("unload", function() { + notificationBox.removeNotification(notification); + }, false); + } + + } +};