1 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
2 * This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
8 var EXPORTED_SYMBOLS = ["PluginParent", "PluginManager"];
10 const { AppConstants } = ChromeUtils.import(
11 "resource://gre/modules/AppConstants.jsm"
13 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
14 const { XPCOMUtils } = ChromeUtils.import(
15 "resource://gre/modules/XPCOMUtils.jsm"
18 XPCOMUtils.defineLazyServiceGetter(
21 "@mozilla.org/plugin/host;1",
25 ChromeUtils.defineModuleGetter(
28 "resource://gre/modules/BrowserUtils.jsm"
30 ChromeUtils.defineModuleGetter(
33 "resource://gre/modules/CrashSubmit.jsm"
36 XPCOMUtils.defineLazyGetter(this, "gNavigatorBundle", function() {
37 const url = "chrome://browser/locale/browser.properties";
38 return Services.strings.createBundle(url);
41 const kNotificationId = "click-to-play-plugins";
45 PLUGIN_VULNERABLE_NO_UPDATE,
46 PLUGIN_VULNERABLE_UPDATABLE,
47 PLUGIN_CLICK_TO_PLAY_QUIET,
48 } = Ci.nsIObjectLoadingContent;
50 const PluginManager = {
54 crashReports: new Map(),
55 gmpCrashes: new Map(),
56 _pendingCrashQueries: new Map(),
58 // BrowserGlue.jsm ensures we catch all plugin crashes.
59 // Barring crashes, we don't need to do anything until/unless an
60 // actor gets instantiated, in which case we also care about the
61 // plugin list changing.
63 if (this._initialized) {
66 this._initialized = true;
67 this._updatePluginMap();
68 Services.obs.addObserver(this, "plugins-list-updated");
69 Services.obs.addObserver(this, "profile-after-change");
73 if (!this._initialized) {
76 Services.obs.removeObserver(this, "plugins-list-updated");
77 Services.obs.removeObserver(this, "profile-after-change");
78 this.crashReports = new Map();
79 this.gmpCrashes = new Map();
80 this.pluginMap = new Map();
83 observe(subject, topic, data) {
85 case "plugins-list-updated":
86 this._updatePluginMap();
88 case "plugin-crashed":
89 this.ensureInitialized();
90 this._registerNPAPICrash(subject);
92 case "gmp-plugin-crash":
93 this.ensureInitialized();
94 this._registerGMPCrash(subject);
96 case "profile-after-change":
102 getPluginTagById(id) {
103 return this.pluginMap.get(id);
107 this.pluginMap = new Map();
108 let plugins = gPluginHost.getPluginTags();
109 for (let plugin of plugins) {
110 this.pluginMap.set(plugin.id, plugin);
114 // Crashed-plugin observer. Notified once per plugin crash, before events
115 // are dispatched to individual plugin instances. However, because of IPC,
116 // the event and the observer notification may still race.
117 _registerNPAPICrash(subject) {
118 let propertyBag = subject;
120 !(propertyBag instanceof Ci.nsIWritablePropertyBag2) ||
121 !propertyBag.hasKey("runID") ||
122 !propertyBag.hasKey("pluginName")
125 "A NPAPI plugin crashed, but the notification is incomplete."
130 let runID = propertyBag.getPropertyAsUint32("runID");
131 let uglyPluginName = propertyBag.getPropertyAsAString("pluginName");
132 let pluginName = BrowserUtils.makeNicePluginName(uglyPluginName);
133 let pluginDumpID = propertyBag.getPropertyAsAString("pluginDumpID");
136 let crashReporter = Services.appinfo.QueryInterface(Ci.nsICrashReporter);
137 if (!AppConstants.MOZ_CRASHREPORTER || !crashReporter.enabled) {
138 // This state tells the user that crash reporting is disabled, so we
139 // cannot send a report.
141 } else if (!pluginDumpID) {
142 // If we don't have a minidumpID, we can't submit anything.
143 // This can happen if the plugin is killed from the task manager.
144 // This state tells the user that this is the case.
147 // This state asks the user to submit a crash report.
151 let crashInfo = { runID, state, pluginName, pluginDumpID };
152 this.crashReports.set(runID, crashInfo);
153 let listeners = this._pendingCrashQueries.get(runID) || [];
154 for (let listener of listeners) {
157 this._pendingCrashQueries.delete(runID);
160 _registerGMPCrash(subject) {
161 let propertyBag = subject;
163 !(propertyBag instanceof Ci.nsIWritablePropertyBag2) ||
164 !propertyBag.hasKey("pluginID") ||
165 !propertyBag.hasKey("pluginDumpID") ||
166 !propertyBag.hasKey("pluginName")
168 Cu.reportError("PluginManager can not read plugin information.");
172 let pluginID = propertyBag.getPropertyAsUint32("pluginID");
173 let pluginDumpID = propertyBag.getPropertyAsAString("pluginDumpID");
174 let pluginName = propertyBag.getPropertyAsACString("pluginName");
176 this.gmpCrashes.set(pluginID, { pluginDumpID, pluginID, pluginName });
179 // Only the parent process gets the gmp-plugin-crash observer
180 // notification, so we need to inform any content processes that
181 // the GMP has crashed. This then fires PluginCrashed events in
182 // all the relevant windows, which will trigger child actors being
183 // created, which will contact us again, when we'll use the
184 // gmpCrashes collection to respond.
186 Services.ppmm.broadcastAsyncMessage("gmp-plugin-crash", {
194 * Submit a crash report for a crashed NPAPI plugin.
196 * @param pluginCrashID
197 * An object with either a runID (for NPAPI crashes) or a pluginID
198 * property (for GMP plugin crashes).
199 * A run ID is a unique identifier for a particular run of a plugin
200 * process - and is analogous to a process ID (though it is managed
201 * by Gecko instead of the operating system).
203 * An object whose key-value pairs will be merged
204 * with the ".extra" file submitted with the report.
205 * The properties of htis object will override properties
206 * of the same name in the .extra file.
208 submitCrashReport(pluginCrashID, keyVals = {}) {
209 let report = this.getCrashReport(pluginCrashID);
212 `Could not find plugin dump IDs for ${JSON.stringify(pluginCrashID)}.` +
213 `It is possible that a report was already submitted.`
218 let { pluginDumpID } = report;
219 let submissionPromise = CrashSubmit.submit(pluginDumpID, {
220 recordSubmission: true,
221 extraExtraKeyVals: keyVals,
224 this.broadcastState(pluginCrashID, "submitting");
226 submissionPromise.then(
228 this.broadcastState(pluginCrashID, "success");
231 this.broadcastState(pluginCrashID, "failed");
235 if (pluginCrashID.hasOwnProperty("runID")) {
236 this.crashReports.delete(pluginCrashID.runID);
238 this.gmpCrashes.delete(pluginCrashID.pluginID);
242 broadcastState(pluginCrashID, state) {
243 if (!pluginCrashID.hasOwnProperty("runID")) {
246 let { runID } = pluginCrashID;
247 Services.ppmm.broadcastAsyncMessage(
248 "PluginParent:NPAPIPluginCrashReportSubmitted",
253 getCrashReport(pluginCrashID) {
254 if (pluginCrashID.hasOwnProperty("pluginID")) {
255 return this.gmpCrashes.get(pluginCrashID.pluginID);
257 return this.crashReports.get(pluginCrashID.runID);
261 * Called by actors when they want crash info on behalf of the child.
262 * Will either return such info immediately if we have it, or return
263 * a promise, which resolves when we do have it. The promise resolution
264 * function is kept around for when we get the `plugin-crashed` observer
267 awaitPluginCrashInfo(runID) {
268 if (this.crashReports.has(runID)) {
269 return this.crashReports.get(runID);
271 let listeners = this._pendingCrashQueries.get(runID);
274 this._pendingCrashQueries.set(runID, listeners);
276 return new Promise(resolve => listeners.push(resolve));
280 * This allows dependency injection, where an automated test can
281 * dictate how and when we respond to a child's inquiry about a crash.
282 * This is helpful when testing different orderings for plugin crash
283 * notifications (ie race conditions).
285 * Concretely, for the toplevel browsingContext of the `browser` we're
286 * passed, call the passed `handler` function the next time the child
287 * asks for crash data (using PluginContent:GetCrashData). We'll return
288 * the result of the function to the child. The message in question
289 * uses the actor query API, so promises and/or async functions will
292 mockResponse(browser, handler) {
293 let { currentWindowGlobal } = browser.frameLoader.browsingContext;
294 currentWindowGlobal.getActor("Plugin")._mockedResponder = handler;
298 class PluginParent extends JSWindowActorParent {
301 PluginManager.ensureInitialized();
304 receiveMessage(msg) {
305 let browser = this.manager.rootFrameLoader.ownerElement;
306 let win = browser.ownerGlobal;
308 case "PluginContent:ShowClickToPlayNotification":
309 this.showClickToPlayNotification(
315 case "PluginContent:RemoveNotification":
316 this.removeNotification(browser);
318 case "PluginContent:ShowPluginCrashedNotification":
319 this.showPluginCrashedNotification(browser, msg.data.pluginCrashID);
321 case "PluginContent:SubmitReport":
322 if (AppConstants.MOZ_CRASHREPORTER) {
326 msg.data.submitURLOptIn
330 case "PluginContent:LinkClickCallback":
331 switch (msg.data.name) {
332 case "managePlugins":
334 this[msg.data.name](win);
336 case "openPluginUpdatePage":
337 this.openPluginUpdatePage(win, msg.data.pluginId);
341 case "PluginContent:GetCrashData":
342 if (this._mockedResponder) {
343 let rv = this._mockedResponder(msg.data);
344 delete this._mockedResponder;
347 return PluginManager.awaitPluginCrashInfo(msg.data.runID);
351 "PluginParent did not expect to handle message " + msg.name
359 // Callback for user clicking on a disabled plugin
360 managePlugins(window) {
361 window.BrowserOpenAddonsMgr("addons://list/plugin");
364 // Callback for user clicking on the link in a click-to-play plugin
365 // (where the plugin has an update)
366 async openPluginUpdatePage(window, pluginId) {
367 let pluginTag = PluginManager.getPluginTagById(pluginId);
371 let { Blocklist } = ChromeUtils.import(
372 "resource://gre/modules/Blocklist.jsm"
374 let url = await Blocklist.getPluginBlockURL(pluginTag);
375 window.openTrustedLinkIn(url, "tab");
378 submitReport(runID, keyVals, submitURLOptIn) {
379 if (!AppConstants.MOZ_CRASHREPORTER) {
382 Services.prefs.setBoolPref(
383 "dom.ipc.plugins.reportCrashURL",
386 PluginManager.submitCrashReport({ runID }, keyVals);
389 // Callback for user clicking a "reload page" link
390 reloadPage(browser) {
394 // Callback for user clicking the help icon
395 openHelpPage(window) {
396 window.openHelpLink("plugin-crashed", false);
399 _clickToPlayNotificationEventCallback(event) {
400 if (event == "showing") {
402 .getHistogramById("PLUGINS_NOTIFICATION_SHOWN")
403 .add(!this.options.showNow);
404 } else if (event == "dismissed") {
405 // Once the popup is dismissed, clicking the icon should show the full
407 this.options.showNow = false;
412 * Called from the plugin doorhanger to set the new permissions for a plugin
413 * and activate plugins if necessary.
414 * aNewState should be one of:
418 * - "continueblocking"
420 _updatePluginPermission(aBrowser, aActivationInfo, aNewState) {
422 let histogram = Services.telemetry.getHistogramById(
423 "PLUGINS_NOTIFICATION_USER_ACTION_2"
426 let window = aBrowser.ownerGlobal;
427 let notification = window.PopupNotifications.getNotification(
432 // Update the permission manager.
433 // Also update the current state of activationInfo.fallbackType so that
434 // subsequent opening of the notification shows the current state.
437 permission = Ci.nsIPermissionManager.ALLOW_ACTION;
439 aActivationInfo.fallbackType = PLUGIN_ACTIVE;
440 notification.options.extraAttr = "active";
444 permission = Ci.nsIPermissionManager.PROMPT_ACTION;
446 let pluginTag = PluginManager.getPluginTagById(aActivationInfo.id);
447 switch (pluginTag.blocklistState) {
448 case Ci.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE:
449 aActivationInfo.fallbackType = PLUGIN_VULNERABLE_UPDATABLE;
451 case Ci.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE:
452 aActivationInfo.fallbackType = PLUGIN_VULNERABLE_NO_UPDATE;
455 // PLUGIN_CLICK_TO_PLAY_QUIET will only last until they reload the page, at
456 // which point it will be PLUGIN_CLICK_TO_PLAY (the overlays will appear)
457 aActivationInfo.fallbackType = PLUGIN_CLICK_TO_PLAY_QUIET;
459 notification.options.extraAttr = "inactive";
462 // In case a plugin has already been allowed/disallowed in another tab, the
463 // buttons matching the existing block state shouldn't change any permissions
464 // but should run the plugin-enablement code below.
466 aActivationInfo.fallbackType = PLUGIN_ACTIVE;
467 notification.options.extraAttr = "active";
470 case "continueblocking":
471 aActivationInfo.fallbackType = PLUGIN_CLICK_TO_PLAY_QUIET;
472 notification.options.extraAttr = "inactive";
476 Cu.reportError(Error("Unexpected plugin state: " + aNewState));
480 if (aNewState != "continue" && aNewState != "continueblocking") {
481 let { principal } = notification.options;
482 Services.perms.addFromPrincipal(
484 aActivationInfo.permissionString,
486 Ci.nsIPermissionManager.EXPIRE_SESSION,
487 0 // do not expire (only expire at the end of the session)
491 this.sendAsyncMessage("PluginParent:ActivatePlugins", {
492 activationInfo: aActivationInfo,
497 showClickToPlayNotification(browser, plugin, showNow) {
498 let window = browser.ownerGlobal;
499 if (!window.PopupNotifications) {
502 let notification = window.PopupNotifications.getNotification(
508 this.removeNotification(browser);
512 // We assume that we can only have 1 notification at a time anyway.
515 notification.options.showNow = true;
516 notification.reshow();
521 // Construct a notification for the plugin:
522 let { id, fallbackType } = plugin;
523 let pluginTag = PluginManager.getPluginTagById(id);
527 let permissionString = gPluginHost.getPermissionStringForTag(pluginTag);
528 let active = fallbackType == PLUGIN_ACTIVE;
530 let { top } = this.browsingContext;
531 if (!top.currentWindowGlobal) {
534 let principal = top.currentWindowGlobal.documentPrincipal;
540 eventCallback: this._clickToPlayNotificationEventCallback,
542 popupIconClass: "plugin-icon",
543 extraAttr: active ? "active" : "inactive",
549 fallbackType == Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_UPDATABLE
551 description = gNavigatorBundle.GetStringFromName(
552 "flashActivate.outdated.message"
555 description = gNavigatorBundle.GetStringFromName("flashActivate.message");
558 let badge = window.document.getElementById("plugin-icon-badge");
559 badge.setAttribute("animate", "true");
560 badge.addEventListener("animationend", function animListener(event) {
562 event.animationName == "blink-badge" &&
563 badge.hasAttribute("animate")
565 badge.removeAttribute("animate");
566 badge.removeEventListener("animationend", animListener);
570 let weakBrowser = Cu.getWeakReference(browser);
572 let activationInfo = { id, fallbackType, permissionString };
573 // Note: in both of these action callbacks, we check the fallbackType on the
574 // activationInfo object, not the local variable. This is important because
575 // the activationInfo object is effectively read/write - the notification
576 // will stay up, and for blocking after allowing (or vice versa) to work, we
577 // need to always read the updated value.
580 let browserRef = weakBrowser.get();
585 activationInfo.fallbackType == PLUGIN_ACTIVE
588 this._updatePluginPermission(browserRef, activationInfo, perm);
590 label: gNavigatorBundle.GetStringFromName("flashActivate.allow"),
591 accessKey: gNavigatorBundle.GetStringFromName(
592 "flashActivate.allow.accesskey"
596 let secondaryActions = [
599 let browserRef = weakBrowser.get();
604 activationInfo.fallbackType == PLUGIN_ACTIVE
606 : "continueblocking";
607 this._updatePluginPermission(browserRef, activationInfo, perm);
609 label: gNavigatorBundle.GetStringFromName("flashActivate.noAllow"),
610 accessKey: gNavigatorBundle.GetStringFromName(
611 "flashActivate.noAllow.accesskey"
617 window.PopupNotifications.show(
621 "plugins-notification-icon",
627 // Check if the plugin is insecure and update the notification icon accordingly.
628 let haveInsecure = false;
629 switch (fallbackType) {
630 // haveInsecure will trigger the red flashing icon and the infobar
632 case PLUGIN_VULNERABLE_UPDATABLE:
633 case PLUGIN_VULNERABLE_NO_UPDATE:
638 .getElementById("plugins-notification-icon")
639 .classList.toggle("plugin-blocked", haveInsecure);
642 removeNotification(browser) {
643 let { PopupNotifications } = browser.ownerGlobal;
644 let notification = PopupNotifications.getNotification(
649 PopupNotifications.remove(notification);
654 * Shows a plugin-crashed notification bar for a browser that has had an
655 * invisible NPAPI plugin crash, or a GMP plugin crash.
658 * The browser to show the notification for.
659 * @param pluginCrashID
660 * The unique-per-process identifier for the NPAPI plugin or GMP.
661 * This will have either a runID or pluginID property, identifying
662 * an npapi plugin or gmp plugin crash, respectively.
664 showPluginCrashedNotification(browser, pluginCrashID) {
665 // If there's already an existing notification bar, don't do anything.
666 let notificationBox = browser.getTabBrowser().getNotificationBox(browser);
667 let notification = notificationBox.getNotificationWithValue(
671 let report = PluginManager.getCrashReport(pluginCrashID);
672 if (notification || !report) {
676 // Configure the notification bar
677 let priority = notificationBox.PRIORITY_WARNING_MEDIUM;
678 let iconURL = "chrome://global/skin/plugins/plugin.svg";
679 let reloadLabel = gNavigatorBundle.GetStringFromName(
680 "crashedpluginsMessage.reloadButton.label"
682 let reloadKey = gNavigatorBundle.GetStringFromName(
683 "crashedpluginsMessage.reloadButton.accesskey"
689 accessKey: reloadKey,
697 if (AppConstants.MOZ_CRASHREPORTER) {
698 let submitLabel = gNavigatorBundle.GetStringFromName(
699 "crashedpluginsMessage.submitButton.label"
701 let submitKey = gNavigatorBundle.GetStringFromName(
702 "crashedpluginsMessage.submitButton.accesskey"
706 accessKey: submitKey,
709 PluginManager.submitCrashReport(pluginCrashID);
713 buttons.push(submitButton);
716 let messageString = gNavigatorBundle.formatStringFromName(
717 "crashedpluginsMessage.title",
720 notification = notificationBox.appendNotification(
728 // Add the "learn more" link.
729 let link = notification.ownerDocument.createXULElement("label", {
734 gNavigatorBundle.GetStringFromName("crashedpluginsMessage.learnMore")
736 let crashurl = Services.urlFormatter.formatURLPref("app.support.baseURL");
737 crashurl += "plugin-crashed-notificationbar";
738 link.href = crashurl;
739 // Append a blank text node to make sure we don't put
740 // the link right next to the end of the message text.
741 notification.messageText.appendChild(new Text(" "));
742 notification.messageText.appendChild(link);