Source code

Revision control

Other Tools

/* -*- 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/. */
"use strict";
var EXPORTED_SYMBOLS = ["PluginParent", "PluginManager"];
const { AppConstants } = ChromeUtils.import(
);
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { XPCOMUtils } = ChromeUtils.import(
);
XPCOMUtils.defineLazyServiceGetter(
this,
"gPluginHost",
"@mozilla.org/plugin/host;1",
"nsIPluginHost"
);
ChromeUtils.defineModuleGetter(
this,
"BrowserUtils",
);
ChromeUtils.defineModuleGetter(
this,
"CrashSubmit",
);
XPCOMUtils.defineLazyGetter(this, "gNavigatorBundle", function() {
return Services.strings.createBundle(url);
});
const kNotificationId = "click-to-play-plugins";
const {
PLUGIN_ACTIVE,
PLUGIN_VULNERABLE_NO_UPDATE,
PLUGIN_VULNERABLE_UPDATABLE,
PLUGIN_CLICK_TO_PLAY_QUIET,
} = Ci.nsIObjectLoadingContent;
const PluginManager = {
_initialized: false,
pluginMap: new Map(),
crashReports: new Map(),
gmpCrashes: new Map(),
_pendingCrashQueries: new Map(),
// BrowserGlue.jsm ensures we catch all plugin crashes.
// Barring crashes, we don't need to do anything until/unless an
// actor gets instantiated, in which case we also care about the
// plugin list changing.
ensureInitialized() {
if (this._initialized) {
return;
}
this._initialized = true;
this._updatePluginMap();
Services.obs.addObserver(this, "plugins-list-updated");
Services.obs.addObserver(this, "profile-after-change");
},
destroy() {
if (!this._initialized) {
return;
}
Services.obs.removeObserver(this, "plugins-list-updated");
Services.obs.removeObserver(this, "profile-after-change");
this.crashReports = new Map();
this.gmpCrashes = new Map();
this.pluginMap = new Map();
},
observe(subject, topic, data) {
switch (topic) {
case "plugins-list-updated":
this._updatePluginMap();
break;
case "plugin-crashed":
this.ensureInitialized();
this._registerNPAPICrash(subject);
break;
case "gmp-plugin-crash":
this.ensureInitialized();
this._registerGMPCrash(subject);
break;
case "profile-after-change":
this.destroy();
break;
}
},
getPluginTagById(id) {
return this.pluginMap.get(id);
},
_updatePluginMap() {
this.pluginMap = new Map();
let plugins = gPluginHost.getPluginTags();
for (let plugin of plugins) {
this.pluginMap.set(plugin.id, plugin);
}
},
// Crashed-plugin observer. Notified once per plugin crash, before events
// are dispatched to individual plugin instances. However, because of IPC,
// the event and the observer notification may still race.
_registerNPAPICrash(subject) {
let propertyBag = subject;
if (
!(propertyBag instanceof Ci.nsIWritablePropertyBag2) ||
!propertyBag.hasKey("runID") ||
!propertyBag.hasKey("pluginName")
) {
Cu.reportError(
"A NPAPI plugin crashed, but the notification is incomplete."
);
return;
}
let runID = propertyBag.getPropertyAsUint32("runID");
let uglyPluginName = propertyBag.getPropertyAsAString("pluginName");
let pluginName = BrowserUtils.makeNicePluginName(uglyPluginName);
let pluginDumpID = propertyBag.getPropertyAsAString("pluginDumpID");
let state;
let crashReporter = Services.appinfo.QueryInterface(Ci.nsICrashReporter);
if (!AppConstants.MOZ_CRASHREPORTER || !crashReporter.enabled) {
// This state tells the user that crash reporting is disabled, so we
// cannot send a report.
state = "noSubmit";
} else if (!pluginDumpID) {
// If we don't have a minidumpID, we can't submit anything.
// This can happen if the plugin is killed from the task manager.
// This state tells the user that this is the case.
state = "noReport";
} else {
// This state asks the user to submit a crash report.
state = "please";
}
let crashInfo = { runID, state, pluginName, pluginDumpID };
this.crashReports.set(runID, crashInfo);
let listeners = this._pendingCrashQueries.get(runID) || [];
for (let listener of listeners) {
listener(crashInfo);
}
this._pendingCrashQueries.delete(runID);
},
_registerGMPCrash(subject) {
let propertyBag = subject;
if (
!(propertyBag instanceof Ci.nsIWritablePropertyBag2) ||
!propertyBag.hasKey("pluginID") ||
!propertyBag.hasKey("pluginDumpID") ||
!propertyBag.hasKey("pluginName")
) {
Cu.reportError("PluginManager can not read plugin information.");
return;
}
let pluginID = propertyBag.getPropertyAsUint32("pluginID");
let pluginDumpID = propertyBag.getPropertyAsAString("pluginDumpID");
if (pluginDumpID) {
this.gmpCrashes.set(pluginID, { pluginDumpID, pluginID });
}
// Only the parent process gets the gmp-plugin-crash observer
// notification, so we need to inform any content processes that
// the GMP has crashed. This then fires PluginCrashed events in
// all the relevant windows, which will trigger child actors being
// created, which will contact us again, when we'll use the
// gmpCrashes collection to respond.
if (Services.ppmm) {
let pluginName = propertyBag.getPropertyAsAString("pluginName");
Services.ppmm.broadcastAsyncMessage("gmp-plugin-crash", {
pluginName,
pluginID,
});
}
},
/**
* Submit a crash report for a crashed NPAPI plugin.
*
* @param pluginCrashID
* An object with either a runID (for NPAPI crashes) or a pluginID
* property (for GMP plugin crashes).
* A run ID is a unique identifier for a particular run of a plugin
* process - and is analogous to a process ID (though it is managed
* by Gecko instead of the operating system).
* @param keyVals
* An object whose key-value pairs will be merged
* with the ".extra" file submitted with the report.
* The properties of htis object will override properties
* of the same name in the .extra file.
*/
submitCrashReport(pluginCrashID, keyVals = {}) {
let report = this.getCrashReport(pluginCrashID);
if (!report) {
Cu.reportError(
`Could not find plugin dump IDs for ${JSON.stringify(pluginCrashID)}.` +
`It is possible that a report was already submitted.`
);
return;
}
let { pluginDumpID } = report;
let submissionPromise = CrashSubmit.submit(pluginDumpID, {
recordSubmission: true,
extraExtraKeyVals: keyVals,
});
this.broadcastState(pluginCrashID, "submitting");
submissionPromise.then(
() => {
this.broadcastState(pluginCrashID, "success");
},
() => {
this.broadcastState(pluginCrashID, "failed");
}
);
if (pluginCrashID.hasOwnProperty("runID")) {
this.crashReports.delete(pluginCrashID.runID);
} else {
this.gmpCrashes.delete(pluginCrashID.pluginID);
}
},
broadcastState(pluginCrashID, state) {
if (!pluginCrashID.hasOwnProperty("runID")) {
return;
}
let { runID } = pluginCrashID;
Services.ppmm.broadcastAsyncMessage(
"PluginParent:NPAPIPluginCrashReportSubmitted",
{ runID, state }
);
},
getCrashReport(pluginCrashID) {
if (pluginCrashID.hasOwnProperty("pluginID")) {
return this.gmpCrashes.get(pluginCrashID.pluginID);
}
return this.crashReports.get(pluginCrashID.runID);
},
/**
* Called by actors when they want crash info on behalf of the child.
* Will either return such info immediately if we have it, or return
* a promise, which resolves when we do have it. The promise resolution
* function is kept around for when we get the `plugin-crashed` observer
* notification.
*/
awaitPluginCrashInfo(runID) {
if (this.crashReports.has(runID)) {
return this.crashReports.get(runID);
}
let listeners = this._pendingCrashQueries.get(runID);
if (!listeners) {
listeners = [];
this._pendingCrashQueries.set(runID, listeners);
}
return new Promise(resolve => listeners.push(resolve));
},
/**
* This allows dependency injection, where an automated test can
* dictate how and when we respond to a child's inquiry about a crash.
* This is helpful when testing different orderings for plugin crash
* notifications (ie race conditions).
*
* Concretely, for the toplevel browsingContext of the `browser` we're
* passed, call the passed `handler` function the next time the child
* asks for crash data (using PluginContent:GetCrashData). We'll return
* the result of the function to the child. The message in question
* uses the actor query API, so promises and/or async functions will
* Just Work.
*/
mockResponse(browser, handler) {
let { currentWindowGlobal } = browser.frameLoader.browsingContext;
currentWindowGlobal.getActor("Plugin")._mockedResponder = handler;
},
};
class PluginParent extends JSWindowActorParent {
constructor() {
super();
PluginManager.ensureInitialized();
}
receiveMessage(msg) {
let browser = this.manager.rootFrameLoader.ownerElement;
let win = browser.ownerGlobal;
switch (msg.name) {
case "PluginContent:ShowClickToPlayNotification":
this.showClickToPlayNotification(
browser,
msg.data.plugin,
msg.data.showNow
);
break;
case "PluginContent:RemoveNotification":
this.removeNotification(browser);
break;
case "PluginContent:ShowPluginCrashedNotification":
this.showPluginCrashedNotification(browser, msg.data.pluginCrashID);
break;
case "PluginContent:SubmitReport":
if (AppConstants.MOZ_CRASHREPORTER) {
this.submitReport(
msg.data.runID,
msg.data.keyVals,
msg.data.submitURLOptIn
);
}
break;
case "PluginContent:LinkClickCallback":
switch (msg.data.name) {
case "managePlugins":
case "openHelpPage":
this[msg.data.name](win);
break;
case "openPluginUpdatePage":
this.openPluginUpdatePage(win, msg.data.pluginId);
break;
}
break;
case "PluginContent:GetCrashData":
if (this._mockedResponder) {
let rv = this._mockedResponder(msg.data);
delete this._mockedResponder;
return rv;
}
return PluginManager.awaitPluginCrashInfo(msg.data.runID);
default:
Cu.reportError(
"PluginParent did not expect to handle message " + msg.name
);
break;
}
return null;
}
// Callback for user clicking on a disabled plugin
managePlugins(window) {
window.BrowserOpenAddonsMgr("addons://list/plugin");
}
// Callback for user clicking on the link in a click-to-play plugin
// (where the plugin has an update)
async openPluginUpdatePage(window, pluginId) {
let pluginTag = PluginManager.getPluginTagById(pluginId);
if (!pluginTag) {
return;
}
let { Blocklist } = ChromeUtils.import(
);
let url = await Blocklist.getPluginBlockURL(pluginTag);
window.openTrustedLinkIn(url, "tab");
}
submitReport(runID, keyVals, submitURLOptIn) {
if (!AppConstants.MOZ_CRASHREPORTER) {
return;
}
Services.prefs.setBoolPref(
"dom.ipc.plugins.reportCrashURL",
!!submitURLOptIn
);
PluginManager.submitCrashReport({ runID }, keyVals);
}
// Callback for user clicking a "reload page" link
reloadPage(browser) {
browser.reload();
}
// Callback for user clicking the help icon
openHelpPage(window) {
window.openHelpLink("plugin-crashed", false);
}
_clickToPlayNotificationEventCallback(event) {
if (event == "showing") {
Services.telemetry
.getHistogramById("PLUGINS_NOTIFICATION_SHOWN")
.add(!this.options.showNow);
} else if (event == "dismissed") {
// Once the popup is dismissed, clicking the icon should show the full
// list again
this.options.showNow = false;
}
}
/**
* Called from the plugin doorhanger to set the new permissions for a plugin
* and activate plugins if necessary.
* aNewState should be one of:
* - "allownow"
* - "block"
* - "continue"
* - "continueblocking"
*/
_updatePluginPermission(aBrowser, aActivationInfo, aNewState) {
let permission;
let histogram = Services.telemetry.getHistogramById(
"PLUGINS_NOTIFICATION_USER_ACTION_2"
);
let window = aBrowser.ownerGlobal;
let notification = window.PopupNotifications.getNotification(
kNotificationId,
aBrowser
);
// Update the permission manager.
// Also update the current state of activationInfo.fallbackType so that
// subsequent opening of the notification shows the current state.
switch (aNewState) {
case "allownow":
permission = Ci.nsIPermissionManager.ALLOW_ACTION;
histogram.add(0);
aActivationInfo.fallbackType = PLUGIN_ACTIVE;
notification.options.extraAttr = "active";
break;
case "block":
permission = Ci.nsIPermissionManager.PROMPT_ACTION;
histogram.add(2);
let pluginTag = PluginManager.getPluginTagById(aActivationInfo.id);
switch (pluginTag.blocklistState) {
case Ci.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE:
aActivationInfo.fallbackType = PLUGIN_VULNERABLE_UPDATABLE;
break;
case Ci.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE:
aActivationInfo.fallbackType = PLUGIN_VULNERABLE_NO_UPDATE;
break;
default:
// PLUGIN_CLICK_TO_PLAY_QUIET will only last until they reload the page, at
// which point it will be PLUGIN_CLICK_TO_PLAY (the overlays will appear)
aActivationInfo.fallbackType = PLUGIN_CLICK_TO_PLAY_QUIET;
}
notification.options.extraAttr = "inactive";
break;
// In case a plugin has already been allowed/disallowed in another tab, the
// buttons matching the existing block state shouldn't change any permissions
// but should run the plugin-enablement code below.
case "continue":
aActivationInfo.fallbackType = PLUGIN_ACTIVE;
notification.options.extraAttr = "active";
break;
case "continueblocking":
aActivationInfo.fallbackType = PLUGIN_CLICK_TO_PLAY_QUIET;
notification.options.extraAttr = "inactive";
break;
default:
Cu.reportError(Error("Unexpected plugin state: " + aNewState));
return;
}
if (aNewState != "continue" && aNewState != "continueblocking") {
let { principal } = notification.options;
Services.perms.addFromPrincipal(
principal,
aActivationInfo.permissionString,
permission,
Ci.nsIPermissionManager.EXPIRE_SESSION,
0 // do not expire (only expire at the end of the session)
);
}
this.sendAsyncMessage("PluginParent:ActivatePlugins", {
activationInfo: aActivationInfo,
newState: aNewState,
});
}
showClickToPlayNotification(browser, plugin, showNow) {
let window = browser.ownerGlobal;
if (!window.PopupNotifications) {
return;
}
let notification = window.PopupNotifications.getNotification(
kNotificationId,
browser
);
if (!plugin) {
this.removeNotification(browser);
return;
}
// We assume that we can only have 1 notification at a time anyway.
if (notification) {
if (showNow) {
notification.options.showNow = true;
notification.reshow();
}
return;
}
// Construct a notification for the plugin:
let { id, fallbackType } = plugin;
let pluginTag = PluginManager.getPluginTagById(id);
if (!pluginTag) {
return;
}
let permissionString = gPluginHost.getPermissionStringForTag(pluginTag);
let active = fallbackType == PLUGIN_ACTIVE;
let { top } = this.browsingContext;
if (!top.currentWindowGlobal) {
return;
}
let principal = top.currentWindowGlobal.documentPrincipal;
let options = {
dismissed: !showNow,
hideClose: true,
persistent: showNow,
eventCallback: this._clickToPlayNotificationEventCallback,
showNow,
popupIconClass: "plugin-icon",
extraAttr: active ? "active" : "inactive",
principal,
};
let description;
if (
fallbackType == Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_UPDATABLE
) {
description = gNavigatorBundle.GetStringFromName(
"flashActivate.outdated.message"
);
} else {
description = gNavigatorBundle.GetStringFromName("flashActivate.message");
}
let badge = window.document.getElementById("plugin-icon-badge");
badge.setAttribute("animate", "true");
badge.addEventListener("animationend", function animListener(event) {
if (
event.animationName == "blink-badge" &&
badge.hasAttribute("animate")
) {
badge.removeAttribute("animate");
badge.removeEventListener("animationend", animListener);
}
});
let weakBrowser = Cu.getWeakReference(browser);
let activationInfo = { id, fallbackType, permissionString };
// Note: in both of these action callbacks, we check the fallbackType on the
// activationInfo object, not the local variable. This is important because
// the activationInfo object is effectively read/write - the notification
// will stay up, and for blocking after allowing (or vice versa) to work, we
// need to always read the updated value.
let mainAction = {
callback: () => {
let browserRef = weakBrowser.get();
if (!browserRef) {
return;
}
let perm =
activationInfo.fallbackType == PLUGIN_ACTIVE
? "continue"
: "allownow";
this._updatePluginPermission(browserRef, activationInfo, perm);
},
label: gNavigatorBundle.GetStringFromName("flashActivate.allow"),
accessKey: gNavigatorBundle.GetStringFromName(
"flashActivate.allow.accesskey"
),
dismiss: true,
};
let secondaryActions = [
{
callback: () => {
let browserRef = weakBrowser.get();
if (!browserRef) {
return;
}
let perm =
activationInfo.fallbackType == PLUGIN_ACTIVE
? "block"
: "continueblocking";
this._updatePluginPermission(browserRef, activationInfo, perm);
},
label: gNavigatorBundle.GetStringFromName("flashActivate.noAllow"),
accessKey: gNavigatorBundle.GetStringFromName(
"flashActivate.noAllow.accesskey"
),
dismiss: true,
},
];
window.PopupNotifications.show(
browser,
kNotificationId,
description,
"plugins-notification-icon",
mainAction,
secondaryActions,
options
);
// Check if the plugin is insecure and update the notification icon accordingly.
let haveInsecure = false;
switch (fallbackType) {
// haveInsecure will trigger the red flashing icon and the infobar
// styling below
case PLUGIN_VULNERABLE_UPDATABLE:
case PLUGIN_VULNERABLE_NO_UPDATE:
haveInsecure = true;
}
window.document
.getElementById("plugins-notification-icon")
.classList.toggle("plugin-blocked", haveInsecure);
}
removeNotification(browser) {
let { PopupNotifications } = browser.ownerGlobal;
let notification = PopupNotifications.getNotification(
kNotificationId,
browser
);
if (notification) {
PopupNotifications.remove(notification);
}
}
/**
* Shows a plugin-crashed notification bar for a browser that has had an
* invisible NPAPI plugin crash, or a GMP plugin crash.
*
* @param browser
* The browser to show the notification for.
* @param pluginCrashID
* The unique-per-process identifier for the NPAPI plugin or GMP.
* This will have either a runID or pluginID property, identifying
* an npapi plugin or gmp plugin crash, respectively.
*/
showPluginCrashedNotification(browser, pluginCrashID) {
// If there's already an existing notification bar, don't do anything.
let notificationBox = browser.getTabBrowser().getNotificationBox(browser);
let notification = notificationBox.getNotificationWithValue(
"plugin-crashed"
);
let report = PluginManager.getCrashReport(pluginCrashID);
if (notification || !report) {
return;
}
// Configure the notification bar
let priority = notificationBox.PRIORITY_WARNING_MEDIUM;
let reloadLabel = gNavigatorBundle.GetStringFromName(
"crashedpluginsMessage.reloadButton.label"
);
let reloadKey = gNavigatorBundle.GetStringFromName(
"crashedpluginsMessage.reloadButton.accesskey"
);
let buttons = [
{
label: reloadLabel,
accessKey: reloadKey,
popup: null,
callback() {
browser.reload();
},
},
];
if (AppConstants.MOZ_CRASHREPORTER) {
let submitLabel = gNavigatorBundle.GetStringFromName(
"crashedpluginsMessage.submitButton.label"
);
let submitKey = gNavigatorBundle.GetStringFromName(
"crashedpluginsMessage.submitButton.accesskey"
);
let submitButton = {
label: submitLabel,
accessKey: submitKey,
popup: null,
callback: () => {
PluginManager.submitCrashReport(pluginCrashID);
},
};
buttons.push(submitButton);
}
let messageString = gNavigatorBundle.formatStringFromName(
"crashedpluginsMessage.title",
[report.pluginName]
);
notification = notificationBox.appendNotification(
messageString,
"plugin-crashed",
iconURL,
priority,
buttons
);
// Add the "learn more" link.
let link = notification.ownerDocument.createXULElement("label", {
is: "text-link",
});
link.setAttribute(
"value",
gNavigatorBundle.GetStringFromName("crashedpluginsMessage.learnMore")
);
let crashurl = Services.urlFormatter.formatURLPref("app.support.baseURL");
crashurl += "plugin-crashed-notificationbar";
link.href = crashurl;
notification.messageText.appendChild(link);
}
}