/* -*- indent-tabs-mode: nil; js-indent-level: 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_SESSION_PERSIST_MINUTES: "plugin.sessionPermissionNow.intervalInMinutes", PREF_PERSISTENT_DAYS: "plugin.persistentPermissionAlways.intervalInDays", MESSAGES: [ "PluginContent:ShowClickToPlayNotification", "PluginContent:RemoveNotification", "PluginContent:UpdateHiddenPluginUI", "PluginContent:HideNotificationBar", "PluginContent:InstallSinglePlugin", "PluginContent:ShowPluginCrashedNotification", "PluginContent:SubmitReport", "PluginContent:LinkClickCallback", ], init: function () { const mm = window.messageManager; for (let msg of this.MESSAGES) { mm.addMessageListener(msg, this); } window.addEventListener("unload", this); }, uninit: function () { const mm = window.messageManager; for (let msg of this.MESSAGES) { mm.removeMessageListener(msg, this); } window.removeEventListener("unload", this); }, handleEvent: function (event) { if (event.type == "unload") { this.uninit(); } }, receiveMessage: function (msg) { switch (msg.name) { case "PluginContent:ShowClickToPlayNotification": this.showClickToPlayNotification(msg.target, msg.data.plugins, msg.data.showNow, msg.principal, msg.data.location); break; case "PluginContent:RemoveNotification": this.removeNotification(msg.target, msg.data.name); break; case "PluginContent:UpdateHiddenPluginUI": this.updateHiddenPluginUI(msg.target, msg.data.haveInsecure, msg.data.actions, msg.principal, msg.data.location); break; case "PluginContent:HideNotificationBar": this.hideNotificationBar(msg.target, msg.data.name); break; case "PluginContent:InstallSinglePlugin": this.installSinglePlugin(msg.data.pluginInfo); break; case "PluginContent:ShowPluginCrashedNotification": this.showPluginCrashedNotification(msg.target, msg.data.messageString, msg.data.pluginID); break; case "PluginContent:SubmitReport": // Nothing to do here break; case "PluginContent:LinkClickCallback": switch (msg.data.name) { case "managePlugins": case "openHelpPage": case "openPluginUpdatePage": this[msg.data.name].call(this, msg.data.pluginTag); break; } break; default: Cu.reportError("gPluginHandler did not expect to handle message " + msg.name); break; } }, // Callback for user clicking on a disabled plugin managePlugins: function () { 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(pluginTag) { let url = Services.blocklist.getPluginInfoURL(pluginTag); if (!url) { url = Services.blocklist.getPluginBlocklistURL(pluginTag); } openUILinkIn(url, "tab"); }, submitReport: function submitReport(runID, keyVals, submitURLOptIn) { /*** STUB ***/ return; }, // 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); }, _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; } }, /** * 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 (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; if (aNewState != "continue") { let principal = aNotification.options.principal; Services.perms.addFromPrincipal(principal, aPluginInfo.permissionString, permission, expireType, expireTime); aPluginInfo.pluginPermissionType = expireType; } browser.messageManager.sendAsyncMessage("BrowserPlugins:ActivatePlugins", { pluginInfo: aPluginInfo, newState: aNewState, }); }, showClickToPlayNotification: function (browser, plugins, showNow, principal, location) { // It is possible that we've received a message from the frame script to show // a click to play notification for a principal that no longer matches the one // that the browser's content now has assigned (ie, the browser has browsed away // after the message was sent, but before the message was received). In that case, // we should just ignore the message. if (!principal.equals(browser.contentPrincipal)) { return; } // Data URIs, when linked to from some page, inherit the principal of that // page. That means that we also need to compare the actual locations to // ensure we aren't getting a message from a Data URI that we're no longer // looking at. let receivedURI = BrowserUtils.makeURI(location); if (!browser.documentURI.equalsExceptRef(receivedURI)) { return; } let notification = PopupNotifications.getNotification("click-to-play-plugins", browser); // If this is a new notification, create a pluginData map, otherwise append let pluginData; if (notification) { pluginData = notification.options.pluginData; } else { pluginData = new Map(); } for (var pluginInfo of plugins) { if (pluginData.has(pluginInfo.permissionString)) { continue; } // If a block contains an infoURL, we should always prefer that to the default // URL that we construct in-product, even for other blocklist types. let url = Services.blocklist.getPluginInfoURL(pluginInfo.pluginTag); if (pluginInfo.blocklistState != Ci.nsIBlocklistService.STATE_NOT_BLOCKED) { if (!url) { 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 (showNow) { primaryPluginPermission = plugins[0].permissionString; } if (notification) { // Don't modify the notification UI while it's on the screen, that would be // jumpy and might allow clickjacking. if (showNow) { notification.options.primaryPlugin = primaryPluginPermission; notification.reshow(); browser.messageManager.sendAsyncMessage("BrowserPlugins:NotificationShown"); } return; } let options = { dismissed: !showNow, eventCallback: this._clickToPlayNotificationEventCallback, primaryPlugin: primaryPluginPermission, pluginData: pluginData, principal: principal, }; PopupNotifications.show(browser, "click-to-play-plugins", "", "plugins-notification-icon", null, null, options); browser.messageManager.sendAsyncMessage("BrowserPlugins:NotificationShown"); }, removeNotification: function (browser, name) { let notification = PopupNotifications.getNotification(name, browser); if (notification) PopupNotifications.remove(notification); }, hideNotificationBar: function (browser, name) { let notificationBox = gBrowser.getNotificationBox(browser); let notification = notificationBox.getNotificationWithValue(name); if (notification) notificationBox.removeNotification(notification, true); }, updateHiddenPluginUI: function (browser, haveInsecure, actions, principal, location) { let origin = principal.originNoSuffix; // It is possible that we've received a message from the frame script to show // the hidden plugin notification for a principal that no longer matches the one // that the browser's content now has assigned (ie, the browser has browsed away // after the message was sent, but before the message was received). In that case, // we should just ignore the message. if (!principal.equals(browser.contentPrincipal)) { return; } // Data URIs, when linked to from some page, inherit the principal of that // page. That means that we also need to compare the actual locations to // ensure we aren't getting a message from a Data URI that we're no longer // looking at. let receivedURI = BrowserUtils.makeURI(location); if (!browser.documentURI.equalsExceptRef(receivedURI)) { return; } // Set up the icon document.getElementById("plugins-notification-icon").classList. toggle("plugin-blocked", haveInsecure); // Now configure the notification bar let notificationBox = gBrowser.getNotificationBox(browser); 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 brand = document.getElementById("bundle_brand").getString("brandShortName"); if (actions.length == 1) { let pluginInfo = actions[0]; let pluginName = pluginInfo.pluginName; switch (pluginInfo.fallbackType) { case Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY: message = gNavigatorBundle.getFormattedString( "pluginActivateNew.message", [pluginName, origin]); break; case Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_UPDATABLE: message = gNavigatorBundle.getFormattedString( "pluginActivateOutdated.message", [pluginName, origin, brand]); break; case Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_NO_UPDATE: message = gNavigatorBundle.getFormattedString( "pluginActivateVulnerable.message", [pluginName, origin, brand]); } } else { // Multi-plugin message = gNavigatorBundle.getFormattedString( "pluginActivateMultiple.message", [origin]); } 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(principal, "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", browser); if (curNotification) { curNotification.reshow(); } } } ]; n = notificationBox. appendNotification(message, "plugin-hidden", null, notificationBox.PRIORITY_INFO_HIGH, buttons); if (haveInsecure) { n.classList.add('pluginVulnerable'); } } if (actions.length == 0) { hideNotification(); } else { let notificationPermission = Services.perms.testPermissionFromPrincipal( principal, "plugin-hidden-notification"); if (notificationPermission == Ci.nsIPermissionManager.DENY_ACTION) { hideNotification(); } else { showNotification(); } } }, contextMenuCommand: function (browser, plugin, command) { browser.messageManager.sendAsyncMessage("BrowserPlugins:ContextMenuCommand", { command: command }, { plugin: plugin }); }, // Crashed-plugin observer. Notified once per plugin crash, before events // are dispatched to individual plugin instances. NPAPIPluginCrashed : function(subject, topic, data) { let propertyBag = subject; if (!(propertyBag instanceof Ci.nsIPropertyBag2) || !(propertyBag instanceof Ci.nsIWritablePropertyBag2) || !propertyBag.hasKey("runID") || !propertyBag.hasKey("pluginName")) { Cu.reportError("A NPAPI plugin crashed, but the properties of this plugin " + "cannot be read."); return; } let runID = propertyBag.getPropertyAsUint32("runID"); let uglyPluginName = propertyBag.getPropertyAsAString("pluginName"); let pluginName = BrowserUtils.makeNicePluginName(uglyPluginName); let pluginDumpID = propertyBag.getPropertyAsAString("pluginDumpID"); // 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. let state = "noSubmit"; let mm = window.getGroupMessageManager("browsers"); mm.broadcastAsyncMessage("BrowserPlugins:NPAPIPluginProcessCrashed", { pluginName, runID, state }); }, /** * Shows a plugin-crashed notification bar for a browser that has had an * invisiable NPAPI plugin crash, or a GMP plugin crash. * * @param browser * The browser to show the notification for. * @param messageString * The string to put in the notification bar * @param pluginID * The unique-per-process identifier for the NPAPI plugin or GMP. * For a GMP, this is the pluginID. For NPAPI plugins (where "pluginID" * means something different), this is the runID. */ showPluginCrashedNotification: function (browser, messageString, pluginID) { // If there's already an existing notification bar, don't do anything. let notificationBox = gBrowser.getNotificationBox(browser); 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 buttons = [{ label: reloadLabel, accessKey: reloadKey, popup: null, callback: function() { browser.reload(); }, }]; 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); }, }; gPluginHandler.init();