Bug 1685680 [wpt PR 27099] - Add missing jsapi tests for wasm reference-types proposa...
[gecko.git] / browser / actors / PluginParent.jsm
blob05f8936cc336c4fea750145f5af06531a2c5964b
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/. */
6 "use strict";
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(
19   this,
20   "gPluginHost",
21   "@mozilla.org/plugin/host;1",
22   "nsIPluginHost"
25 ChromeUtils.defineModuleGetter(
26   this,
27   "BrowserUtils",
28   "resource://gre/modules/BrowserUtils.jsm"
30 ChromeUtils.defineModuleGetter(
31   this,
32   "CrashSubmit",
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);
39 });
41 const kNotificationId = "click-to-play-plugins";
43 const {
44   PLUGIN_ACTIVE,
45   PLUGIN_VULNERABLE_NO_UPDATE,
46   PLUGIN_VULNERABLE_UPDATABLE,
47   PLUGIN_CLICK_TO_PLAY_QUIET,
48 } = Ci.nsIObjectLoadingContent;
50 const PluginManager = {
51   _initialized: false,
53   pluginMap: new Map(),
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.
62   ensureInitialized() {
63     if (this._initialized) {
64       return;
65     }
66     this._initialized = true;
67     this._updatePluginMap();
68     Services.obs.addObserver(this, "plugins-list-updated");
69     Services.obs.addObserver(this, "profile-after-change");
70   },
72   destroy() {
73     if (!this._initialized) {
74       return;
75     }
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();
81   },
83   observe(subject, topic, data) {
84     switch (topic) {
85       case "plugins-list-updated":
86         this._updatePluginMap();
87         break;
88       case "plugin-crashed":
89         this.ensureInitialized();
90         this._registerNPAPICrash(subject);
91         break;
92       case "gmp-plugin-crash":
93         this.ensureInitialized();
94         this._registerGMPCrash(subject);
95         break;
96       case "profile-after-change":
97         this.destroy();
98         break;
99     }
100   },
102   getPluginTagById(id) {
103     return this.pluginMap.get(id);
104   },
106   _updatePluginMap() {
107     this.pluginMap = new Map();
108     let plugins = gPluginHost.getPluginTags();
109     for (let plugin of plugins) {
110       this.pluginMap.set(plugin.id, plugin);
111     }
112   },
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;
119     if (
120       !(propertyBag instanceof Ci.nsIWritablePropertyBag2) ||
121       !propertyBag.hasKey("runID") ||
122       !propertyBag.hasKey("pluginName")
123     ) {
124       Cu.reportError(
125         "A NPAPI plugin crashed, but the notification is incomplete."
126       );
127       return;
128     }
130     let runID = propertyBag.getPropertyAsUint32("runID");
131     let uglyPluginName = propertyBag.getPropertyAsAString("pluginName");
132     let pluginName = BrowserUtils.makeNicePluginName(uglyPluginName);
133     let pluginDumpID = propertyBag.getPropertyAsAString("pluginDumpID");
135     let state;
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.
140       state = "noSubmit";
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.
145       state = "noReport";
146     } else {
147       // This state asks the user to submit a crash report.
148       state = "please";
149     }
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) {
155       listener(crashInfo);
156     }
157     this._pendingCrashQueries.delete(runID);
158   },
160   _registerGMPCrash(subject) {
161     let propertyBag = subject;
162     if (
163       !(propertyBag instanceof Ci.nsIWritablePropertyBag2) ||
164       !propertyBag.hasKey("pluginID") ||
165       !propertyBag.hasKey("pluginDumpID") ||
166       !propertyBag.hasKey("pluginName")
167     ) {
168       Cu.reportError("PluginManager can not read plugin information.");
169       return;
170     }
172     let pluginID = propertyBag.getPropertyAsUint32("pluginID");
173     let pluginDumpID = propertyBag.getPropertyAsAString("pluginDumpID");
174     let pluginName = propertyBag.getPropertyAsACString("pluginName");
175     if (pluginDumpID) {
176       this.gmpCrashes.set(pluginID, { pluginDumpID, pluginID, pluginName });
177     }
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.
185     if (Services.ppmm) {
186       Services.ppmm.broadcastAsyncMessage("gmp-plugin-crash", {
187         pluginName,
188         pluginID,
189       });
190     }
191   },
193   /**
194    * Submit a crash report for a crashed NPAPI plugin.
195    *
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).
202    * @param keyVals
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.
207    */
208   submitCrashReport(pluginCrashID, keyVals = {}) {
209     let report = this.getCrashReport(pluginCrashID);
210     if (!report) {
211       Cu.reportError(
212         `Could not find plugin dump IDs for ${JSON.stringify(pluginCrashID)}.` +
213           `It is possible that a report was already submitted.`
214       );
215       return;
216     }
218     let { pluginDumpID } = report;
219     let submissionPromise = CrashSubmit.submit(pluginDumpID, {
220       recordSubmission: true,
221       extraExtraKeyVals: keyVals,
222     });
224     this.broadcastState(pluginCrashID, "submitting");
226     submissionPromise.then(
227       () => {
228         this.broadcastState(pluginCrashID, "success");
229       },
230       () => {
231         this.broadcastState(pluginCrashID, "failed");
232       }
233     );
235     if (pluginCrashID.hasOwnProperty("runID")) {
236       this.crashReports.delete(pluginCrashID.runID);
237     } else {
238       this.gmpCrashes.delete(pluginCrashID.pluginID);
239     }
240   },
242   broadcastState(pluginCrashID, state) {
243     if (!pluginCrashID.hasOwnProperty("runID")) {
244       return;
245     }
246     let { runID } = pluginCrashID;
247     Services.ppmm.broadcastAsyncMessage(
248       "PluginParent:NPAPIPluginCrashReportSubmitted",
249       { runID, state }
250     );
251   },
253   getCrashReport(pluginCrashID) {
254     if (pluginCrashID.hasOwnProperty("pluginID")) {
255       return this.gmpCrashes.get(pluginCrashID.pluginID);
256     }
257     return this.crashReports.get(pluginCrashID.runID);
258   },
260   /**
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
265    * notification.
266    */
267   awaitPluginCrashInfo(runID) {
268     if (this.crashReports.has(runID)) {
269       return this.crashReports.get(runID);
270     }
271     let listeners = this._pendingCrashQueries.get(runID);
272     if (!listeners) {
273       listeners = [];
274       this._pendingCrashQueries.set(runID, listeners);
275     }
276     return new Promise(resolve => listeners.push(resolve));
277   },
279   /**
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).
284    *
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
290    * Just Work.
291    */
292   mockResponse(browser, handler) {
293     let { currentWindowGlobal } = browser.frameLoader.browsingContext;
294     currentWindowGlobal.getActor("Plugin")._mockedResponder = handler;
295   },
298 class PluginParent extends JSWindowActorParent {
299   constructor() {
300     super();
301     PluginManager.ensureInitialized();
302   }
304   receiveMessage(msg) {
305     let browser = this.manager.rootFrameLoader.ownerElement;
306     let win = browser.ownerGlobal;
307     switch (msg.name) {
308       case "PluginContent:ShowClickToPlayNotification":
309         this.showClickToPlayNotification(
310           browser,
311           msg.data.plugin,
312           msg.data.showNow
313         );
314         break;
315       case "PluginContent:RemoveNotification":
316         this.removeNotification(browser);
317         break;
318       case "PluginContent:ShowPluginCrashedNotification":
319         this.showPluginCrashedNotification(browser, msg.data.pluginCrashID);
320         break;
321       case "PluginContent:SubmitReport":
322         if (AppConstants.MOZ_CRASHREPORTER) {
323           this.submitReport(
324             msg.data.runID,
325             msg.data.keyVals,
326             msg.data.submitURLOptIn
327           );
328         }
329         break;
330       case "PluginContent:LinkClickCallback":
331         switch (msg.data.name) {
332           case "managePlugins":
333           case "openHelpPage":
334             this[msg.data.name](win);
335             break;
336           case "openPluginUpdatePage":
337             this.openPluginUpdatePage(win, msg.data.pluginId);
338             break;
339         }
340         break;
341       case "PluginContent:GetCrashData":
342         if (this._mockedResponder) {
343           let rv = this._mockedResponder(msg.data);
344           delete this._mockedResponder;
345           return rv;
346         }
347         return PluginManager.awaitPluginCrashInfo(msg.data.runID);
349       default:
350         Cu.reportError(
351           "PluginParent did not expect to handle message " + msg.name
352         );
353         break;
354     }
356     return null;
357   }
359   // Callback for user clicking on a disabled plugin
360   managePlugins(window) {
361     window.BrowserOpenAddonsMgr("addons://list/plugin");
362   }
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);
368     if (!pluginTag) {
369       return;
370     }
371     let { Blocklist } = ChromeUtils.import(
372       "resource://gre/modules/Blocklist.jsm"
373     );
374     let url = await Blocklist.getPluginBlockURL(pluginTag);
375     window.openTrustedLinkIn(url, "tab");
376   }
378   submitReport(runID, keyVals, submitURLOptIn) {
379     if (!AppConstants.MOZ_CRASHREPORTER) {
380       return;
381     }
382     Services.prefs.setBoolPref(
383       "dom.ipc.plugins.reportCrashURL",
384       !!submitURLOptIn
385     );
386     PluginManager.submitCrashReport({ runID }, keyVals);
387   }
389   // Callback for user clicking a "reload page" link
390   reloadPage(browser) {
391     browser.reload();
392   }
394   // Callback for user clicking the help icon
395   openHelpPage(window) {
396     window.openHelpLink("plugin-crashed", false);
397   }
399   _clickToPlayNotificationEventCallback(event) {
400     if (event == "showing") {
401       Services.telemetry
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
406       // list again
407       this.options.showNow = false;
408     }
409   }
411   /**
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:
415    * - "allownow"
416    * - "block"
417    * - "continue"
418    * - "continueblocking"
419    */
420   _updatePluginPermission(aBrowser, aActivationInfo, aNewState) {
421     let permission;
422     let histogram = Services.telemetry.getHistogramById(
423       "PLUGINS_NOTIFICATION_USER_ACTION_2"
424     );
426     let window = aBrowser.ownerGlobal;
427     let notification = window.PopupNotifications.getNotification(
428       kNotificationId,
429       aBrowser
430     );
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.
435     switch (aNewState) {
436       case "allownow":
437         permission = Ci.nsIPermissionManager.ALLOW_ACTION;
438         histogram.add(0);
439         aActivationInfo.fallbackType = PLUGIN_ACTIVE;
440         notification.options.extraAttr = "active";
441         break;
443       case "block":
444         permission = Ci.nsIPermissionManager.PROMPT_ACTION;
445         histogram.add(2);
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;
450             break;
451           case Ci.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE:
452             aActivationInfo.fallbackType = PLUGIN_VULNERABLE_NO_UPDATE;
453             break;
454           default:
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;
458         }
459         notification.options.extraAttr = "inactive";
460         break;
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.
465       case "continue":
466         aActivationInfo.fallbackType = PLUGIN_ACTIVE;
467         notification.options.extraAttr = "active";
468         break;
470       case "continueblocking":
471         aActivationInfo.fallbackType = PLUGIN_CLICK_TO_PLAY_QUIET;
472         notification.options.extraAttr = "inactive";
473         break;
475       default:
476         Cu.reportError(Error("Unexpected plugin state: " + aNewState));
477         return;
478     }
480     if (aNewState != "continue" && aNewState != "continueblocking") {
481       let { principal } = notification.options;
482       Services.perms.addFromPrincipal(
483         principal,
484         aActivationInfo.permissionString,
485         permission,
486         Ci.nsIPermissionManager.EXPIRE_SESSION,
487         0 // do not expire (only expire at the end of the session)
488       );
489     }
491     this.sendAsyncMessage("PluginParent:ActivatePlugins", {
492       activationInfo: aActivationInfo,
493       newState: aNewState,
494     });
495   }
497   showClickToPlayNotification(browser, plugin, showNow) {
498     let window = browser.ownerGlobal;
499     if (!window.PopupNotifications) {
500       return;
501     }
502     let notification = window.PopupNotifications.getNotification(
503       kNotificationId,
504       browser
505     );
507     if (!plugin) {
508       this.removeNotification(browser);
509       return;
510     }
512     // We assume that we can only have 1 notification at a time anyway.
513     if (notification) {
514       if (showNow) {
515         notification.options.showNow = true;
516         notification.reshow();
517       }
518       return;
519     }
521     // Construct a notification for the plugin:
522     let { id, fallbackType } = plugin;
523     let pluginTag = PluginManager.getPluginTagById(id);
524     if (!pluginTag) {
525       return;
526     }
527     let permissionString = gPluginHost.getPermissionStringForTag(pluginTag);
528     let active = fallbackType == PLUGIN_ACTIVE;
530     let { top } = this.browsingContext;
531     if (!top.currentWindowGlobal) {
532       return;
533     }
534     let principal = top.currentWindowGlobal.documentPrincipal;
536     let options = {
537       dismissed: !showNow,
538       hideClose: true,
539       persistent: showNow,
540       eventCallback: this._clickToPlayNotificationEventCallback,
541       showNow,
542       popupIconClass: "plugin-icon",
543       extraAttr: active ? "active" : "inactive",
544       principal,
545     };
547     let description;
548     if (
549       fallbackType == Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_UPDATABLE
550     ) {
551       description = gNavigatorBundle.GetStringFromName(
552         "flashActivate.outdated.message"
553       );
554     } else {
555       description = gNavigatorBundle.GetStringFromName("flashActivate.message");
556     }
558     let badge = window.document.getElementById("plugin-icon-badge");
559     badge.setAttribute("animate", "true");
560     badge.addEventListener("animationend", function animListener(event) {
561       if (
562         event.animationName == "blink-badge" &&
563         badge.hasAttribute("animate")
564       ) {
565         badge.removeAttribute("animate");
566         badge.removeEventListener("animationend", animListener);
567       }
568     });
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.
578     let mainAction = {
579       callback: () => {
580         let browserRef = weakBrowser.get();
581         if (!browserRef) {
582           return;
583         }
584         let perm =
585           activationInfo.fallbackType == PLUGIN_ACTIVE
586             ? "continue"
587             : "allownow";
588         this._updatePluginPermission(browserRef, activationInfo, perm);
589       },
590       label: gNavigatorBundle.GetStringFromName("flashActivate.allow"),
591       accessKey: gNavigatorBundle.GetStringFromName(
592         "flashActivate.allow.accesskey"
593       ),
594       dismiss: true,
595     };
596     let secondaryActions = [
597       {
598         callback: () => {
599           let browserRef = weakBrowser.get();
600           if (!browserRef) {
601             return;
602           }
603           let perm =
604             activationInfo.fallbackType == PLUGIN_ACTIVE
605               ? "block"
606               : "continueblocking";
607           this._updatePluginPermission(browserRef, activationInfo, perm);
608         },
609         label: gNavigatorBundle.GetStringFromName("flashActivate.noAllow"),
610         accessKey: gNavigatorBundle.GetStringFromName(
611           "flashActivate.noAllow.accesskey"
612         ),
613         dismiss: true,
614       },
615     ];
617     window.PopupNotifications.show(
618       browser,
619       kNotificationId,
620       description,
621       "plugins-notification-icon",
622       mainAction,
623       secondaryActions,
624       options
625     );
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
631       // styling below
632       case PLUGIN_VULNERABLE_UPDATABLE:
633       case PLUGIN_VULNERABLE_NO_UPDATE:
634         haveInsecure = true;
635     }
637     window.document
638       .getElementById("plugins-notification-icon")
639       .classList.toggle("plugin-blocked", haveInsecure);
640   }
642   removeNotification(browser) {
643     let { PopupNotifications } = browser.ownerGlobal;
644     let notification = PopupNotifications.getNotification(
645       kNotificationId,
646       browser
647     );
648     if (notification) {
649       PopupNotifications.remove(notification);
650     }
651   }
653   /**
654    * Shows a plugin-crashed notification bar for a browser that has had an
655    * invisible NPAPI plugin crash, or a GMP plugin crash.
656    *
657    * @param browser
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.
663    */
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(
668       "plugin-crashed"
669     );
671     let report = PluginManager.getCrashReport(pluginCrashID);
672     if (notification || !report) {
673       return;
674     }
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"
681     );
682     let reloadKey = gNavigatorBundle.GetStringFromName(
683       "crashedpluginsMessage.reloadButton.accesskey"
684     );
686     let buttons = [
687       {
688         label: reloadLabel,
689         accessKey: reloadKey,
690         popup: null,
691         callback() {
692           browser.reload();
693         },
694       },
695     ];
697     if (AppConstants.MOZ_CRASHREPORTER) {
698       let submitLabel = gNavigatorBundle.GetStringFromName(
699         "crashedpluginsMessage.submitButton.label"
700       );
701       let submitKey = gNavigatorBundle.GetStringFromName(
702         "crashedpluginsMessage.submitButton.accesskey"
703       );
704       let submitButton = {
705         label: submitLabel,
706         accessKey: submitKey,
707         popup: null,
708         callback: () => {
709           PluginManager.submitCrashReport(pluginCrashID);
710         },
711       };
713       buttons.push(submitButton);
714     }
716     let messageString = gNavigatorBundle.formatStringFromName(
717       "crashedpluginsMessage.title",
718       [report.pluginName]
719     );
720     notification = notificationBox.appendNotification(
721       messageString,
722       "plugin-crashed",
723       iconURL,
724       priority,
725       buttons
726     );
728     // Add the "learn more" link.
729     let link = notification.ownerDocument.createXULElement("label", {
730       is: "text-link",
731     });
732     link.setAttribute(
733       "value",
734       gNavigatorBundle.GetStringFromName("crashedpluginsMessage.learnMore")
735     );
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);
743   }