# -*- 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 = { PLUGIN_SCRIPTED_STATE_NONE: 0, PLUGIN_SCRIPTED_STATE_FIRED: 1, PLUGIN_SCRIPTED_STATE_DONE: 2, PREF_NOTIFY_MISSING_FLASH: "plugins.notifyMissingFlash", PREF_SESSION_PERSIST_MINUTES: "plugin.sessionPermissionNow.intervalInMinutes", PREF_PERSISTENT_DAYS: "plugin.persistentPermissionAlways.intervalInDays", getPluginUI: function (plugin, className) { return plugin.ownerDocument. getAnonymousElementByAttribute(plugin, "class", className); }, #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; if (pluginElement instanceof HTMLAppletElement) { tagMimetype = "application/x-java-vm"; } else { 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 any trailing version numbers // or "plugin". EG, "Foo Bar 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(/[\s\d\.\-\_\(\)]+$/, "").replace(/\bplug-?in\b/i, "").trim(); return newName; }, isTooSmall : function (plugin, overlay) { // 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); return overflows; }, 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 plugin; let doc; let eventType = event.type; if (eventType === "PluginRemoved") { doc = event.target; } else { plugin = event.target; 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 = doc.getAnonymousElementByAttribute(plugin, "class", "mainBox"); 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); 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 = doc.getAnonymousElementByAttribute(plugin, "class", "installStatus"); installStatus.setAttribute("installable", "true"); let iconStatus = doc.getAnonymousElementByAttribute(plugin, "class", "icon"); iconStatus.setAttribute("installable", "true"); let installLink = doc.getAnonymousElementByAttribute(plugin, "anonid", "installPluginLink"); this.addLinkClickCallback(installLink, "installSinglePlugin", plugin); } break; case "PluginBlocklisted": case "PluginOutdated": shouldShowNotification = true; break; case "PluginVulnerableUpdatable": let updateLink = doc.getAnonymousElementByAttribute(plugin, "anonid", "checkForUpdatesLink"); this.addLinkClickCallback(updateLink, "openPluginUpdatePage"); /* FALLTHRU */ case "PluginVulnerableNoUpdate": case "PluginClickToPlay": this._handleClickToPlayEvent(plugin); let overlay = doc.getAnonymousElementByAttribute(plugin, "class", "mainBox"); let pluginName = this._getPluginInfo(plugin).pluginName; let messageString = gNavigatorBundle.getFormattedString("PluginClickToActivate", [pluginName]); let overlayText = doc.getAnonymousElementByAttribute(plugin, "class", "msg msgClickToPlay"); overlayText.textContent = messageString; if (eventType == "PluginVulnerableUpdatable" || eventType == "PluginVulnerableNoUpdate") { let vulnerabilityString = gNavigatorBundle.getString(eventType); let vulnerabilityText = doc.getAnonymousElementByAttribute(plugin, "anonid", "vulnerabilityStatus"); vulnerabilityText.textContent = vulnerabilityString; } shouldShowNotification = true; break; case "PluginPlayPreview": this._handlePlayPreviewEvent(plugin); break; case "PluginDisabled": let manageLink = doc.getAnonymousElementByAttribute(plugin, "anonid", "managePluginsLink"); this.addLinkClickCallback(manageLink, "managePlugins"); shouldShowNotification = true; break; case "PluginInstantiated": case "PluginRemoved": shouldShowNotification = true; break; } // Hide the in-content UI if it's too big. The crashed plugin handler already did this. if (eventType != "PluginCrashed" && eventType != "PluginRemoved") { let overlay = doc.getAnonymousElementByAttribute(plugin, "class", "mainBox"); if (overlay != null && this.isTooSmall(plugin, overlay)) overlay.style.visibility = "hidden"; } // Only show the notification after we've done the isTooSmall check, so // that the notification can decide whether to show the "alert" icon if (shouldShowNotification) { this._showClickToPlayNotification(browser); } }, 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 = aPlugin.ownerDocument.getAnonymousElementByAttribute(aPlugin, "class", "mainBox"); if (overlay) overlay.style.visibility = "hidden"; }, 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) { openURL(Services.urlFormatter.formatURLPref("plugins.update.url")); }, #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 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 = doc.getAnonymousElementByAttribute(aPlugin, "class", "mainBox"); if (pluginPermission == Ci.nsIPermissionManager.DENY_ACTION) { if (overlay) overlay.style.visibility = "hidden"; return; } if (overlay) { overlay.addEventListener("click", gPluginHandler._overlayClickListener, true); let closeIcon = doc.getAnonymousElementByAttribute(aPlugin, "anonid", "closeIcon"); closeIcon.addEventListener("click", function(aEvent) { if (aEvent.button == 0 && aEvent.isTrusted) gPluginHandler.hideClickToPlayOverlay(aPlugin); }, 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); 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 = doc.getAnonymousElementByAttribute(aPlugin, "class", "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); } }, reshowClickToPlayNotification: function PH_reshowClickToPlayNotification() { let browser = gBrowser.selectedBrowser; let contentWindow = browser.contentWindow; let cwu = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIDOMWindowUtils); let doc = contentWindow.document; let plugins = cwu.plugins; for (let plugin of plugins) { let overlay = doc.getAnonymousElementByAttribute(plugin, "class", "mainBox"); if (overlay) overlay.removeEventListener("click", gPluginHandler._overlayClickListener, true); let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); if (gPluginHandler.canActivatePlugin(objLoadingContent)) gPluginHandler._handleClickToPlayEvent(plugin); } gPluginHandler._showClickToPlayNotification(browser); }, _clickToPlayNotificationEventCallback: function PH_ctpEventCallback(event) { if (event == "showing") { gPluginHandler._makeCenterActions(this); } 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; }, _makeCenterActions: function PH_makeCenterActions(notification) { let contentWindow = notification.browser.contentWindow; let cwu = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIDOMWindowUtils); let principal = contentWindow.document.nodePrincipal; // This matches the behavior of nsPermssionManager, used for display purposes only let principalHost = this._getHostFromPrincipal(principal); let centerActions = []; let pluginsFound = new Set(); for (let plugin of cwu.plugins) { plugin.QueryInterface(Ci.nsIObjectLoadingContent); if (plugin.getContentTypeForMIMEType(plugin.actualType) != Ci.nsIObjectLoadingContent.TYPE_PLUGIN) { continue; } let pluginInfo = this._getPluginInfo(plugin); if (pluginInfo.permissionString === null) { Components.utils.reportError("No permission string for active plugin."); continue; } if (pluginsFound.has(pluginInfo.permissionString)) { continue; } pluginsFound.add(pluginInfo.permissionString); // Add the per-site permissions and details URLs to pluginInfo here // because they are more expensive to compute and so we avoid it in // the tighter loop above. 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); } pluginInfo.detailsLink = url; centerActions.push(pluginInfo); } centerActions.sort(function(a, b) { return a.pluginName.localeCompare(b.pluginName); }); notification.options.centerActions = centerActions; }, /** * 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; 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; 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; break; case "block": permission = Ci.nsIPermissionManager.PROMPT_ACTION; expireType = Ci.nsIPermissionManager.EXPIRE_NEVER; expireTime = 0; 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": 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); if (aNewState == "block") { return; } } // 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); for (let plugin of plugins) { plugin.QueryInterface(Ci.nsIObjectLoadingContent); // canActivatePlugin will return false if this isn't a known plugin type, // so the pluginHost.getPermissionStringForType call is protected if (gPluginHandler.canActivatePlugin(plugin) && aPluginInfo.permissionString == pluginHost.getPermissionStringForType(plugin.actualType)) { plugin.playPlugin(); } } }, _showClickToPlayNotification: function PH_showClickToPlayNotification(aBrowser, aPrimaryPlugin) { let notification = PopupNotifications.getNotification("click-to-play-plugins", aBrowser); let contentWindow = aBrowser.contentWindow; let contentDoc = aBrowser.contentDocument; let cwu = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIDOMWindowUtils); let plugins = cwu.plugins; if (plugins.length == 0) { if (notification) { PopupNotifications.remove(notification); } return; } let icon = 'plugins-notification-icon'; for (let plugin of plugins) { let fallbackType = plugin.pluginFallbackType; if (fallbackType == plugin.PLUGIN_VULNERABLE_UPDATABLE || fallbackType == plugin.PLUGIN_VULNERABLE_NO_UPDATE || fallbackType == plugin.PLUGIN_BLOCKLISTED) { icon = 'blocked-plugins-notification-icon'; break; } if (fallbackType == plugin.PLUGIN_CLICK_TO_PLAY) { let overlay = contentDoc.getAnonymousElementByAttribute(plugin, "class", "mainBox"); if (!overlay || overlay.style.visibility == 'hidden') { icon = 'alert-plugins-notification-icon'; } } } let dismissed = notification ? notification.dismissed : true; if (aPrimaryPlugin) dismissed = false; let primaryPluginPermission = null; if (aPrimaryPlugin) { primaryPluginPermission = this._getPluginInfo(aPrimaryPlugin).permissionString; } let options = { dismissed: dismissed, eventCallback: this._clickToPlayNotificationEventCallback, primaryPlugin: primaryPluginPermission }; PopupNotifications.show(aBrowser, "click-to-play-plugins", "", icon, null, null, options); }, // 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.nsIDOMDataContainerEvent)) return; let submittedReport = aEvent.getData("submittedCrashReport"); let doPrompt = true; // XXX followup for .getData("doPrompt"); let submitReports = true; // XXX followup for .getData("submitReports"); let pluginName = aEvent.getData("pluginName"); let pluginDumpID = aEvent.getData("pluginDumpID"); let browserDumpID = aEvent.getData("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 doc = plugin.ownerDocument; let overlay = doc.getAnonymousElementByAttribute(plugin, "class", "mainBox"); let statusDiv = doc.getAnonymousElementByAttribute(plugin, "class", "submitStatus"); #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 = doc.getAnonymousElementByAttribute(plugin, "class", "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 = doc.getAnonymousElementByAttribute(plugin, "class", "msgCrashedText"); crashText.textContent = messageString; let browser = gBrowser.getBrowserForDocument(doc.defaultView.top.document); let link = doc.getAnonymousElementByAttribute(plugin, "class", "reloadLink"); this.addLinkClickCallback(link, "reloadPage", browser); let notificationBox = gBrowser.getNotificationBox(browser); let isShowing = true; // Is the 's size too small to hold what we want to show? if (this.isTooSmall(plugin, overlay)) { // First try hiding the crash report submission UI. statusDiv.removeAttribute("status"); if (this.isTooSmall(plugin, overlay)) { // Hide the overlay's contents. Use visibility style, so that it doesn't // collapse down to 0x0. overlay.style.visibility = "hidden"; isShowing = false; } } 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); } } };