Source code

Revision control

Other Tools

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/. */
5
6
"use strict";
7
8
var EXPORTED_SYMBOLS = ["PluginParent", "PluginManager"];
9
10
const { AppConstants } = ChromeUtils.import(
12
);
13
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
14
const { XPCOMUtils } = ChromeUtils.import(
16
);
17
18
XPCOMUtils.defineLazyServiceGetter(
19
this,
20
"gPluginHost",
21
"@mozilla.org/plugin/host;1",
22
"nsIPluginHost"
23
);
24
25
ChromeUtils.defineModuleGetter(
26
this,
27
"BrowserUtils",
29
);
30
ChromeUtils.defineModuleGetter(
31
this,
32
"CrashSubmit",
34
);
35
36
XPCOMUtils.defineLazyGetter(this, "gNavigatorBundle", function() {
38
return Services.strings.createBundle(url);
39
});
40
41
const kNotificationId = "click-to-play-plugins";
42
43
const {
44
PLUGIN_ACTIVE,
45
PLUGIN_VULNERABLE_NO_UPDATE,
46
PLUGIN_VULNERABLE_UPDATABLE,
47
PLUGIN_CLICK_TO_PLAY_QUIET,
48
} = Ci.nsIObjectLoadingContent;
49
50
const PluginManager = {
51
_initialized: false,
52
53
pluginMap: new Map(),
54
crashReports: new Map(),
55
gmpCrashes: new Map(),
56
_pendingCrashQueries: new Map(),
57
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
},
71
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
},
82
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
},
101
102
getPluginTagById(id) {
103
return this.pluginMap.get(id);
104
},
105
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
},
113
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
}
129
130
let runID = propertyBag.getPropertyAsUint32("runID");
131
let uglyPluginName = propertyBag.getPropertyAsAString("pluginName");
132
let pluginName = BrowserUtils.makeNicePluginName(uglyPluginName);
133
let pluginDumpID = propertyBag.getPropertyAsAString("pluginDumpID");
134
let browserDumpID = propertyBag.getPropertyAsAString("browserDumpID");
135
136
let state;
137
let crashReporter = Services.appinfo.QueryInterface(Ci.nsICrashReporter);
138
if (!AppConstants.MOZ_CRASHREPORTER || !crashReporter.enabled) {
139
// This state tells the user that crash reporting is disabled, so we
140
// cannot send a report.
141
state = "noSubmit";
142
} else if (!pluginDumpID) {
143
// If we don't have a minidumpID, we can't submit anything.
144
// This can happen if the plugin is killed from the task manager.
145
// This state tells the user that this is the case.
146
state = "noReport";
147
} else {
148
// This state asks the user to submit a crash report.
149
state = "please";
150
}
151
152
let crashInfo = { runID, state, pluginName, pluginDumpID, browserDumpID };
153
this.crashReports.set(runID, crashInfo);
154
let listeners = this._pendingCrashQueries.get(runID) || [];
155
for (let listener of listeners) {
156
listener(crashInfo);
157
}
158
this._pendingCrashQueries.delete(runID);
159
},
160
161
_registerGMPCrash(subject) {
162
let propertyBag = subject;
163
if (
164
!(propertyBag instanceof Ci.nsIWritablePropertyBag2) ||
165
!propertyBag.hasKey("pluginID") ||
166
!propertyBag.hasKey("pluginDumpID") ||
167
!propertyBag.hasKey("pluginName")
168
) {
169
Cu.reportError("PluginManager can not read plugin information.");
170
return;
171
}
172
173
let pluginID = propertyBag.getPropertyAsUint32("pluginID");
174
let pluginDumpID = propertyBag.getPropertyAsAString("pluginDumpID");
175
if (pluginDumpID) {
176
this.gmpCrashes.set(pluginID, { pluginDumpID, pluginID });
177
}
178
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
let pluginName = propertyBag.getPropertyAsAString("pluginName");
187
Services.ppmm.broadcastAsyncMessage("gmp-plugin-crash", {
188
pluginName,
189
pluginID,
190
});
191
}
192
},
193
194
/**
195
* Submit a crash report for a crashed NPAPI plugin.
196
*
197
* @param pluginCrashID
198
* An object with either a runID (for NPAPI crashes) or a pluginID
199
* property (for GMP plugin crashes).
200
* A run ID is a unique identifier for a particular run of a plugin
201
* process - and is analogous to a process ID (though it is managed
202
* by Gecko instead of the operating system).
203
* @param keyVals
204
* An object whose key-value pairs will be merged
205
* with the ".extra" file submitted with the report.
206
* The properties of htis object will override properties
207
* of the same name in the .extra file.
208
*/
209
submitCrashReport(pluginCrashID, keyVals = {}) {
210
let report = this.getCrashReport(pluginCrashID);
211
if (!report) {
212
Cu.reportError(
213
`Could not find plugin dump IDs for ${JSON.stringify(pluginCrashID)}.` +
214
`It is possible that a report was already submitted.`
215
);
216
return;
217
}
218
219
let { pluginDumpID, browserDumpID } = report;
220
let submissionPromise = CrashSubmit.submit(pluginDumpID, {
221
recordSubmission: true,
222
extraExtraKeyVals: keyVals,
223
});
224
225
if (browserDumpID) {
226
CrashSubmit.submit(browserDumpID).catch(Cu.reportError);
227
}
228
229
this.broadcastState(pluginCrashID, "submitting");
230
231
submissionPromise.then(
232
() => {
233
this.broadcastState(pluginCrashID, "success");
234
},
235
() => {
236
this.broadcastState(pluginCrashID, "failed");
237
}
238
);
239
240
if (pluginCrashID.hasOwnProperty("runID")) {
241
this.crashReports.delete(pluginCrashID.runID);
242
} else {
243
this.gmpCrashes.delete(pluginCrashID.pluginID);
244
}
245
},
246
247
broadcastState(pluginCrashID, state) {
248
if (!pluginCrashID.hasOwnProperty("runID")) {
249
return;
250
}
251
let { runID } = pluginCrashID;
252
Services.ppmm.broadcastAsyncMessage(
253
"PluginParent:NPAPIPluginCrashReportSubmitted",
254
{ runID, state }
255
);
256
},
257
258
getCrashReport(pluginCrashID) {
259
if (pluginCrashID.hasOwnProperty("pluginID")) {
260
return this.gmpCrashes.get(pluginCrashID.pluginID);
261
}
262
return this.crashReports.get(pluginCrashID.runID);
263
},
264
265
/**
266
* Called by actors when they want crash info on behalf of the child.
267
* Will either return such info immediately if we have it, or return
268
* a promise, which resolves when we do have it. The promise resolution
269
* function is kept around for when we get the `plugin-crashed` observer
270
* notification.
271
*/
272
awaitPluginCrashInfo(runID) {
273
if (this.crashReports.has(runID)) {
274
return this.crashReports.get(runID);
275
}
276
let listeners = this._pendingCrashQueries.get(runID);
277
if (!listeners) {
278
listeners = [];
279
this._pendingCrashQueries.set(runID, listeners);
280
}
281
return new Promise(resolve => listeners.push(resolve));
282
},
283
284
/**
285
* This allows dependency injection, where an automated test can
286
* dictate how and when we respond to a child's inquiry about a crash.
287
* This is helpful when testing different orderings for plugin crash
288
* notifications (ie race conditions).
289
*
290
* Concretely, for the toplevel browsingContext of the `browser` we're
291
* passed, call the passed `handler` function the next time the child
292
* asks for crash data (using PluginContent:GetCrashData). We'll return
293
* the result of the function to the child. The message in question
294
* uses the actor query API, so promises and/or async functions will
295
* Just Work.
296
*/
297
mockResponse(browser, handler) {
298
let { currentWindowGlobal } = browser.frameLoader.browsingContext;
299
currentWindowGlobal.getActor("Plugin")._mockedResponder = handler;
300
},
301
};
302
303
class PluginParent extends JSWindowActorParent {
304
constructor() {
305
super();
306
PluginManager.ensureInitialized();
307
}
308
309
receiveMessage(msg) {
310
let browser = this.manager.rootFrameLoader.ownerElement;
311
let win = browser.ownerGlobal;
312
switch (msg.name) {
313
case "PluginContent:ShowClickToPlayNotification":
314
this.showClickToPlayNotification(
315
browser,
316
msg.data.plugin,
317
msg.data.showNow
318
);
319
break;
320
case "PluginContent:RemoveNotification":
321
this.removeNotification(browser);
322
break;
323
case "PluginContent:ShowPluginCrashedNotification":
324
this.showPluginCrashedNotification(browser, msg.data.pluginCrashID);
325
break;
326
case "PluginContent:SubmitReport":
327
if (AppConstants.MOZ_CRASHREPORTER) {
328
this.submitReport(
329
msg.data.runID,
330
msg.data.keyVals,
331
msg.data.submitURLOptIn
332
);
333
}
334
break;
335
case "PluginContent:LinkClickCallback":
336
switch (msg.data.name) {
337
case "managePlugins":
338
case "openHelpPage":
339
this[msg.data.name](win);
340
break;
341
case "openPluginUpdatePage":
342
this.openPluginUpdatePage(win, msg.data.pluginId);
343
break;
344
}
345
break;
346
case "PluginContent:GetCrashData":
347
if (this._mockedResponder) {
348
let rv = this._mockedResponder(msg.data);
349
delete this._mockedResponder;
350
return rv;
351
}
352
return PluginManager.awaitPluginCrashInfo(msg.data.runID);
353
354
default:
355
Cu.reportError(
356
"PluginParent did not expect to handle message " + msg.name
357
);
358
break;
359
}
360
361
return null;
362
}
363
364
// Callback for user clicking on a disabled plugin
365
managePlugins(window) {
366
window.BrowserOpenAddonsMgr("addons://list/plugin");
367
}
368
369
// Callback for user clicking on the link in a click-to-play plugin
370
// (where the plugin has an update)
371
async openPluginUpdatePage(window, pluginId) {
372
let pluginTag = PluginManager.getPluginTagById(pluginId);
373
if (!pluginTag) {
374
return;
375
}
376
let { Blocklist } = ChromeUtils.import(
378
);
379
let url = await Blocklist.getPluginBlockURL(pluginTag);
380
window.openTrustedLinkIn(url, "tab");
381
}
382
383
submitReport(runID, keyVals, submitURLOptIn) {
384
if (!AppConstants.MOZ_CRASHREPORTER) {
385
return;
386
}
387
Services.prefs.setBoolPref(
388
"dom.ipc.plugins.reportCrashURL",
389
!!submitURLOptIn
390
);
391
PluginManager.submitCrashReport({ runID }, keyVals);
392
}
393
394
// Callback for user clicking a "reload page" link
395
reloadPage(browser) {
396
browser.reload();
397
}
398
399
// Callback for user clicking the help icon
400
openHelpPage(window) {
401
window.openHelpLink("plugin-crashed", false);
402
}
403
404
_clickToPlayNotificationEventCallback(event) {
405
if (event == "showing") {
406
Services.telemetry
407
.getHistogramById("PLUGINS_NOTIFICATION_SHOWN")
408
.add(!this.options.showNow);
409
} else if (event == "dismissed") {
410
// Once the popup is dismissed, clicking the icon should show the full
411
// list again
412
this.options.showNow = false;
413
}
414
}
415
416
/**
417
* Called from the plugin doorhanger to set the new permissions for a plugin
418
* and activate plugins if necessary.
419
* aNewState should be one of:
420
* - "allownow"
421
* - "block"
422
* - "continue"
423
* - "continueblocking"
424
*/
425
_updatePluginPermission(aBrowser, aActivationInfo, aNewState) {
426
let permission;
427
let histogram = Services.telemetry.getHistogramById(
428
"PLUGINS_NOTIFICATION_USER_ACTION_2"
429
);
430
431
let window = aBrowser.ownerGlobal;
432
let notification = window.PopupNotifications.getNotification(
433
kNotificationId,
434
aBrowser
435
);
436
437
// Update the permission manager.
438
// Also update the current state of activationInfo.fallbackType so that
439
// subsequent opening of the notification shows the current state.
440
switch (aNewState) {
441
case "allownow":
442
permission = Ci.nsIPermissionManager.ALLOW_ACTION;
443
histogram.add(0);
444
aActivationInfo.fallbackType = PLUGIN_ACTIVE;
445
notification.options.extraAttr = "active";
446
break;
447
448
case "block":
449
permission = Ci.nsIPermissionManager.PROMPT_ACTION;
450
histogram.add(2);
451
let pluginTag = PluginManager.getPluginTagById(aActivationInfo.id);
452
switch (pluginTag.blocklistState) {
453
case Ci.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE:
454
aActivationInfo.fallbackType = PLUGIN_VULNERABLE_UPDATABLE;
455
break;
456
case Ci.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE:
457
aActivationInfo.fallbackType = PLUGIN_VULNERABLE_NO_UPDATE;
458
break;
459
default:
460
// PLUGIN_CLICK_TO_PLAY_QUIET will only last until they reload the page, at
461
// which point it will be PLUGIN_CLICK_TO_PLAY (the overlays will appear)
462
aActivationInfo.fallbackType = PLUGIN_CLICK_TO_PLAY_QUIET;
463
}
464
notification.options.extraAttr = "inactive";
465
break;
466
467
// In case a plugin has already been allowed/disallowed in another tab, the
468
// buttons matching the existing block state shouldn't change any permissions
469
// but should run the plugin-enablement code below.
470
case "continue":
471
aActivationInfo.fallbackType = PLUGIN_ACTIVE;
472
notification.options.extraAttr = "active";
473
break;
474
475
case "continueblocking":
476
aActivationInfo.fallbackType = PLUGIN_CLICK_TO_PLAY_QUIET;
477
notification.options.extraAttr = "inactive";
478
break;
479
480
default:
481
Cu.reportError(Error("Unexpected plugin state: " + aNewState));
482
return;
483
}
484
485
if (aNewState != "continue" && aNewState != "continueblocking") {
486
let { principal } = notification.options;
487
Services.perms.addFromPrincipal(
488
principal,
489
aActivationInfo.permissionString,
490
permission,
491
Ci.nsIPermissionManager.EXPIRE_SESSION,
492
0 // do not expire (only expire at the end of the session)
493
);
494
}
495
496
this.sendAsyncMessage("PluginParent:ActivatePlugins", {
497
activationInfo: aActivationInfo,
498
newState: aNewState,
499
});
500
}
501
502
showClickToPlayNotification(browser, plugin, showNow) {
503
let window = browser.ownerGlobal;
504
if (!window.PopupNotifications) {
505
return;
506
}
507
let notification = window.PopupNotifications.getNotification(
508
kNotificationId,
509
browser
510
);
511
512
if (!plugin) {
513
this.removeNotification(browser);
514
return;
515
}
516
517
// We assume that we can only have 1 notification at a time anyway.
518
if (notification) {
519
if (showNow) {
520
notification.options.showNow = true;
521
notification.reshow();
522
}
523
return;
524
}
525
526
// Construct a notification for the plugin:
527
let { id, fallbackType } = plugin;
528
let pluginTag = PluginManager.getPluginTagById(id);
529
if (!pluginTag) {
530
return;
531
}
532
let permissionString = gPluginHost.getPermissionStringForTag(pluginTag);
533
let active = fallbackType == PLUGIN_ACTIVE;
534
535
let options = {
536
dismissed: !showNow,
537
hideClose: true,
538
persistent: showNow,
539
eventCallback: this._clickToPlayNotificationEventCallback,
540
showNow,
541
popupIconClass: "plugin-icon",
542
extraAttr: active ? "active" : "inactive",
543
principal: this.browsingContext.currentWindowGlobal.documentPrincipal,
544
};
545
546
let description;
547
if (
548
fallbackType == Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_UPDATABLE
549
) {
550
description = gNavigatorBundle.GetStringFromName(
551
"flashActivate.outdated.message"
552
);
553
} else {
554
description = gNavigatorBundle.GetStringFromName("flashActivate.message");
555
}
556
557
let badge = window.document.getElementById("plugin-icon-badge");
558
badge.setAttribute("animate", "true");
559
badge.addEventListener("animationend", function animListener(event) {
560
if (
561
event.animationName == "blink-badge" &&
562
badge.hasAttribute("animate")
563
) {
564
badge.removeAttribute("animate");
565
badge.removeEventListener("animationend", animListener);
566
}
567
});
568
569
let weakBrowser = Cu.getWeakReference(browser);
570
571
let activationInfo = { id, fallbackType, permissionString };
572
// Note: in both of these action callbacks, we check the fallbackType on the
573
// activationInfo object, not the local variable. This is important because
574
// the activationInfo object is effectively read/write - the notification
575
// will stay up, and for blocking after allowing (or vice versa) to work, we
576
// need to always read the updated value.
577
let mainAction = {
578
callback: () => {
579
let browserRef = weakBrowser.get();
580
if (!browserRef) {
581
return;
582
}
583
let perm =
584
activationInfo.fallbackType == PLUGIN_ACTIVE
585
? "continue"
586
: "allownow";
587
this._updatePluginPermission(browserRef, activationInfo, perm);
588
},
589
label: gNavigatorBundle.GetStringFromName("flashActivate.allow"),
590
accessKey: gNavigatorBundle.GetStringFromName(
591
"flashActivate.allow.accesskey"
592
),
593
dismiss: true,
594
};
595
let secondaryActions = [
596
{
597
callback: () => {
598
let browserRef = weakBrowser.get();
599
if (!browserRef) {
600
return;
601
}
602
let perm =
603
activationInfo.fallbackType == PLUGIN_ACTIVE
604
? "block"
605
: "continueblocking";
606
this._updatePluginPermission(browserRef, activationInfo, perm);
607
},
608
label: gNavigatorBundle.GetStringFromName("flashActivate.noAllow"),
609
accessKey: gNavigatorBundle.GetStringFromName(
610
"flashActivate.noAllow.accesskey"
611
),
612
dismiss: true,
613
},
614
];
615
616
window.PopupNotifications.show(
617
browser,
618
kNotificationId,
619
description,
620
"plugins-notification-icon",
621
mainAction,
622
secondaryActions,
623
options
624
);
625
626
// Check if the plugin is insecure and update the notification icon accordingly.
627
let haveInsecure = false;
628
switch (fallbackType) {
629
// haveInsecure will trigger the red flashing icon and the infobar
630
// styling below
631
case PLUGIN_VULNERABLE_UPDATABLE:
632
case PLUGIN_VULNERABLE_NO_UPDATE:
633
haveInsecure = true;
634
}
635
636
window.document
637
.getElementById("plugins-notification-icon")
638
.classList.toggle("plugin-blocked", haveInsecure);
639
}
640
641
removeNotification(browser) {
642
let { PopupNotifications } = browser.ownerGlobal;
643
let notification = PopupNotifications.getNotification(
644
kNotificationId,
645
browser
646
);
647
if (notification) {
648
PopupNotifications.remove(notification);
649
}
650
}
651
652
/**
653
* Shows a plugin-crashed notification bar for a browser that has had an
654
* invisible NPAPI plugin crash, or a GMP plugin crash.
655
*
656
* @param browser
657
* The browser to show the notification for.
658
* @param pluginCrashID
659
* The unique-per-process identifier for the NPAPI plugin or GMP.
660
* This will have either a runID or pluginID property, identifying
661
* an npapi plugin or gmp plugin crash, respectively.
662
*/
663
showPluginCrashedNotification(browser, pluginCrashID) {
664
// If there's already an existing notification bar, don't do anything.
665
let notificationBox = browser.getTabBrowser().getNotificationBox(browser);
666
let notification = notificationBox.getNotificationWithValue(
667
"plugin-crashed"
668
);
669
670
let report = PluginManager.getCrashReport(pluginCrashID);
671
if (notification || !report) {
672
return;
673
}
674
675
// Configure the notification bar
676
let priority = notificationBox.PRIORITY_WARNING_MEDIUM;
678
let reloadLabel = gNavigatorBundle.GetStringFromName(
679
"crashedpluginsMessage.reloadButton.label"
680
);
681
let reloadKey = gNavigatorBundle.GetStringFromName(
682
"crashedpluginsMessage.reloadButton.accesskey"
683
);
684
685
let buttons = [
686
{
687
label: reloadLabel,
688
accessKey: reloadKey,
689
popup: null,
690
callback() {
691
browser.reload();
692
},
693
},
694
];
695
696
if (AppConstants.MOZ_CRASHREPORTER) {
697
let submitLabel = gNavigatorBundle.GetStringFromName(
698
"crashedpluginsMessage.submitButton.label"
699
);
700
let submitKey = gNavigatorBundle.GetStringFromName(
701
"crashedpluginsMessage.submitButton.accesskey"
702
);
703
let submitButton = {
704
label: submitLabel,
705
accessKey: submitKey,
706
popup: null,
707
callback: () => {
708
PluginManager.submitCrashReport(pluginCrashID);
709
},
710
};
711
712
buttons.push(submitButton);
713
}
714
715
let messageString = gNavigatorBundle.formatStringFromName(
716
"crashedpluginsMessage.title",
717
[report.pluginName]
718
);
719
notification = notificationBox.appendNotification(
720
messageString,
721
"plugin-crashed",
722
iconURL,
723
priority,
724
buttons
725
);
726
727
// Add the "learn more" link.
728
let link = notification.ownerDocument.createXULElement("label", {
729
is: "text-link",
730
});
731
link.setAttribute(
732
"value",
733
gNavigatorBundle.GetStringFromName("crashedpluginsMessage.learnMore")
734
);
735
let crashurl = Services.urlFormatter.formatURLPref("app.support.baseURL");
736
crashurl += "plugin-crashed-notificationbar";
737
link.href = crashurl;
738
notification.messageText.appendChild(link);
739
}
740
}