Source code

Revision control

Other Tools

1
/* This Source Code Form is subject to the terms of the Mozilla Public
2
* License, v. 2.0. If a copy of the MPL was not distributed with this
3
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5
"use strict";
6
7
var EXPORTED_SYMBOLS = ["PluginChild"];
8
9
const { ActorChild } = ChromeUtils.import(
11
);
12
13
const { XPCOMUtils } = ChromeUtils.import(
15
);
16
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
17
const { BrowserUtils } = ChromeUtils.import(
19
);
20
21
ChromeUtils.defineModuleGetter(
22
this,
23
"ContextMenuChild",
25
);
26
27
XPCOMUtils.defineLazyGetter(this, "gNavigatorBundle", function() {
29
return Services.strings.createBundle(url);
30
});
31
32
ChromeUtils.defineModuleGetter(
33
this,
34
"AppConstants",
36
);
37
38
const OVERLAY_DISPLAY = {
39
HIDDEN: 0, // The overlay will be transparent
40
BLANK: 1, // The overlay will be just a grey box
41
TINY: 2, // The overlay with a 16x16 plugin icon
42
REDUCED: 3, // The overlay with a 32x32 plugin icon
43
NOTEXT: 4, // The overlay with a 48x48 plugin icon and the close button
44
FULL: 5, // The full overlay: 48x48 plugin icon, close button and label
45
};
46
47
class PluginChild extends ActorChild {
48
constructor(dispatcher) {
49
super(dispatcher);
50
51
// Cache of plugin actions for the current page.
52
this.pluginData = new Map();
53
// Cache of plugin crash information sent from the parent
54
this.pluginCrashData = new Map();
55
56
this.mm.addEventListener("pagehide", this, {
57
capture: true,
58
mozSystemGroup: true,
59
});
60
this.mm.addEventListener("pageshow", this, {
61
capture: true,
62
mozSystemGroup: true,
63
});
64
}
65
66
receiveMessage(msg) {
67
switch (msg.name) {
68
case "BrowserPlugins:ActivatePlugins":
69
this.activatePlugins(msg.data.pluginInfo, msg.data.newState);
70
break;
71
case "BrowserPlugins:ContextMenuCommand":
72
switch (msg.data.command) {
73
case "play":
74
this._showClickToPlayNotification(
75
ContextMenuChild.getTarget(
76
this.docShell.browsingContext,
77
msg,
78
"plugin"
79
),
80
true
81
);
82
break;
83
case "hide":
84
this.hideClickToPlayOverlay(
85
ContextMenuChild.getTarget(
86
this.docShell.browsingContext,
87
msg,
88
"plugin"
89
)
90
);
91
break;
92
}
93
break;
94
case "BrowserPlugins:NPAPIPluginProcessCrashed":
95
this.NPAPIPluginProcessCrashed({
96
pluginName: msg.data.pluginName,
97
runID: msg.data.runID,
98
state: msg.data.state,
99
});
100
break;
101
case "BrowserPlugins:CrashReportSubmitted":
102
this.NPAPIPluginCrashReportSubmitted({
103
runID: msg.data.runID,
104
state: msg.data.state,
105
});
106
break;
107
case "BrowserPlugins:Test:ClearCrashData":
108
// This message should ONLY ever be sent by automated tests.
109
if (Services.prefs.getBoolPref("plugins.testmode")) {
110
this.pluginCrashData.clear();
111
}
112
}
113
}
114
115
observe(aSubject, aTopic, aData) {
116
switch (aTopic) {
117
case "decoder-doctor-notification":
118
let data = JSON.parse(aData);
119
let type = data.type.toLowerCase();
120
if (
121
type == "cannot-play" &&
122
this.haveShownNotification &&
123
aSubject.top.document == this.content.document &&
124
data.formats.toLowerCase().includes("application/x-mpegurl", 0)
125
) {
126
this.content.pluginRequiresReload = true;
127
}
128
}
129
}
130
131
onPageShow(event) {
132
// Ignore events that aren't from the main document.
133
if (!this.content || event.target != this.content.document) {
134
return;
135
}
136
137
// The PluginClickToPlay events are not fired when navigating using the
138
// BF cache. |persisted| is true when the page is loaded from the
139
// BF cache, so this code reshows the notification if necessary.
140
if (event.persisted) {
141
this.reshowClickToPlayNotification();
142
}
143
}
144
145
onPageHide(event) {
146
// Ignore events that aren't from the main document.
147
if (!this.content || event.target != this.content.document) {
148
return;
149
}
150
151
this.clearPluginCaches();
152
this.haveShownNotification = false;
153
}
154
155
getPluginUI(plugin, anonid) {
156
if (
157
plugin.openOrClosedShadowRoot &&
158
plugin.openOrClosedShadowRoot.isUAWidget()
159
) {
160
return plugin.openOrClosedShadowRoot.getElementById(anonid);
161
}
162
return null;
163
}
164
165
_getPluginInfo(pluginElement) {
166
let pluginHost = Cc["@mozilla.org/plugin/host;1"].getService(
167
Ci.nsIPluginHost
168
);
169
pluginElement.QueryInterface(Ci.nsIObjectLoadingContent);
170
171
let tagMimetype;
172
let pluginName = gNavigatorBundle.GetStringFromName(
173
"pluginInfo.unknownPlugin"
174
);
175
let pluginTag = null;
176
let permissionString = null;
177
let fallbackType = null;
178
let blocklistState = null;
179
180
tagMimetype = pluginElement.actualType;
181
if (tagMimetype == "") {
182
tagMimetype = pluginElement.type;
183
}
184
185
if (this.isKnownPlugin(pluginElement)) {
186
pluginTag = pluginHost.getPluginTagForType(pluginElement.actualType);
187
pluginName = BrowserUtils.makeNicePluginName(pluginTag.name);
188
189
// Convert this from nsIPluginTag so it can be serialized.
190
let properties = [
191
"name",
192
"description",
193
"filename",
194
"version",
195
"enabledState",
196
"niceName",
197
];
198
let pluginTagCopy = {};
199
for (let prop of properties) {
200
pluginTagCopy[prop] = pluginTag[prop];
201
}
202
pluginTag = pluginTagCopy;
203
204
permissionString = pluginHost.getPermissionStringForType(
205
pluginElement.actualType
206
);
207
fallbackType = pluginElement.defaultFallbackType;
208
blocklistState = pluginHost.getBlocklistStateForType(
209
pluginElement.actualType
210
);
211
// Make state-softblocked == state-notblocked for our purposes,
212
// they have the same UI. STATE_OUTDATED should not exist for plugin
213
// items, but let's alias it anyway, just in case.
214
if (
215
blocklistState == Ci.nsIBlocklistService.STATE_SOFTBLOCKED ||
216
blocklistState == Ci.nsIBlocklistService.STATE_OUTDATED
217
) {
218
blocklistState = Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
219
}
220
}
221
222
return {
223
mimetype: tagMimetype,
224
pluginName,
225
pluginTag,
226
permissionString,
227
fallbackType,
228
blocklistState,
229
};
230
}
231
232
/**
233
* _getPluginInfoForTag is called when iterating the plugins for a document,
234
* and what we get from nsIDOMWindowUtils is an nsIPluginTag, and not an
235
* nsIObjectLoadingContent. This only should happen if the plugin is
236
* click-to-play (see bug 1186948).
237
*/
238
_getPluginInfoForTag(pluginTag, tagMimetype) {
239
let pluginHost = Cc["@mozilla.org/plugin/host;1"].getService(
240
Ci.nsIPluginHost
241
);
242
243
let pluginName = gNavigatorBundle.GetStringFromName(
244
"pluginInfo.unknownPlugin"
245
);
246
let permissionString = null;
247
let blocklistState = null;
248
249
if (pluginTag) {
250
pluginName = BrowserUtils.makeNicePluginName(pluginTag.name);
251
252
permissionString = pluginHost.getPermissionStringForTag(pluginTag);
253
blocklistState = pluginTag.blocklistState;
254
255
// Convert this from nsIPluginTag so it can be serialized.
256
let properties = [
257
"name",
258
"description",
259
"filename",
260
"version",
261
"enabledState",
262
"niceName",
263
];
264
let pluginTagCopy = {};
265
for (let prop of properties) {
266
pluginTagCopy[prop] = pluginTag[prop];
267
}
268
pluginTag = pluginTagCopy;
269
270
// Make state-softblocked == state-notblocked for our purposes,
271
// they have the same UI. STATE_OUTDATED should not exist for plugin
272
// items, but let's alias it anyway, just in case.
273
if (
274
blocklistState == Ci.nsIBlocklistService.STATE_SOFTBLOCKED ||
275
blocklistState == Ci.nsIBlocklistService.STATE_OUTDATED
276
) {
277
blocklistState = Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
278
}
279
}
280
281
return {
282
mimetype: tagMimetype,
283
pluginName,
284
pluginTag,
285
permissionString,
286
// Since we should only have entered _getPluginInfoForTag when
287
// examining a click-to-play plugin, we can safely hard-code
288
// this fallback type, since we don't actually have an
289
// nsIObjectLoadingContent to check.
290
fallbackType: Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY,
291
blocklistState,
292
};
293
}
294
295
/**
296
* Update the visibility of the plugin overlay.
297
*/
298
setVisibility(plugin, overlay, overlayDisplayState) {
299
overlay.classList.toggle(
300
"visible",
301
overlayDisplayState != OVERLAY_DISPLAY.HIDDEN
302
);
303
if (overlayDisplayState != OVERLAY_DISPLAY.HIDDEN) {
304
overlay.removeAttribute("dismissed");
305
}
306
}
307
308
/**
309
* Adjust the style in which the overlay will be displayed. It might be adjusted
310
* based on its size, or if there's some other element covering all corners of
311
* the overlay.
312
*
313
* This function will handle adjusting the style of the overlay, but will
314
* not handle hiding it. That is done by setVisibility with the return value
315
* from this function.
316
*
317
* @param {Element} plugin The plug-in element
318
* @param {Element} overlay The overlay element inside the UA Shadow DOM of
319
* the plug-in element
320
* @param {boolean} flushLayout Allow flush layout during computation and
321
* adjustment.
322
* @returns A value from OVERLAY_DISPLAY.
323
*/
324
computeAndAdjustOverlayDisplay(plugin, overlay, flushLayout) {
325
let fallbackType = plugin.pluginFallbackType;
326
if (plugin.pluginFallbackTypeOverride !== undefined) {
327
fallbackType = plugin.pluginFallbackTypeOverride;
328
}
329
if (fallbackType == Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY_QUIET) {
330
return OVERLAY_DISPLAY.HIDDEN;
331
}
332
333
// If the overlay size is 0, we haven't done layout yet. Presume that
334
// plugins are visible until we know otherwise.
335
if (flushLayout && overlay.scrollWidth == 0) {
336
return OVERLAY_DISPLAY.FULL;
337
}
338
339
let overlayDisplay = OVERLAY_DISPLAY.FULL;
340
let contentWindow = plugin.ownerGlobal;
341
let cwu = contentWindow.windowUtils;
342
343
// Is the <object>'s size too small to hold what we want to show?
344
let pluginRect = flushLayout
345
? plugin.getBoundingClientRect()
346
: cwu.getBoundsWithoutFlushing(plugin);
347
let pluginWidth = Math.ceil(pluginRect.width);
348
let pluginHeight = Math.ceil(pluginRect.height);
349
350
let layoutNeedsFlush =
351
!flushLayout &&
352
cwu.needsFlush(cwu.FLUSH_STYLE) &&
353
cwu.needsFlush(cwu.FLUSH_LAYOUT);
354
355
// We must set the attributes while here inside this function in order
356
// for a possible re-style to occur, which will make the scrollWidth/Height
357
// checks below correct. Otherwise, we would be requesting e.g. a TINY
358
// overlay here, but the default styling would be used, and that would make
359
// it overflow, causing it to change to BLANK instead of remaining as TINY.
360
361
if (layoutNeedsFlush) {
362
// Set the content to be oversized when we the overlay size is 0,
363
// so that we could receive an overflow event afterwards when there is
364
// a layout.
365
overlayDisplay = OVERLAY_DISPLAY.FULL;
366
overlay.setAttribute("sizing", "oversized");
367
overlay.removeAttribute("notext");
368
} else if (pluginWidth <= 32 || pluginHeight <= 32) {
369
overlay.setAttribute("sizing", "blank");
370
overlayDisplay = OVERLAY_DISPLAY.BLANK;
371
} else if (pluginWidth <= 80 || pluginHeight <= 60) {
372
overlayDisplay = OVERLAY_DISPLAY.TINY;
373
overlay.setAttribute("sizing", "tiny");
374
overlay.setAttribute("notext", "notext");
375
} else if (pluginWidth <= 120 || pluginHeight <= 80) {
376
overlayDisplay = OVERLAY_DISPLAY.REDUCED;
377
overlay.setAttribute("sizing", "reduced");
378
overlay.setAttribute("notext", "notext");
379
} else if (pluginWidth <= 240 || pluginHeight <= 160) {
380
overlayDisplay = OVERLAY_DISPLAY.NOTEXT;
381
overlay.removeAttribute("sizing");
382
overlay.setAttribute("notext", "notext");
383
} else {
384
overlayDisplay = OVERLAY_DISPLAY.FULL;
385
overlay.removeAttribute("sizing");
386
overlay.removeAttribute("notext");
387
}
388
389
// The hit test below only works with correct layout information,
390
// don't do it if layout needs flush.
391
// We also don't want to access scrollWidth/scrollHeight if
392
// the layout needs flush.
393
if (layoutNeedsFlush) {
394
return overlayDisplay;
395
}
396
397
// XXX bug 446693. The text-shadow on the submitted-report text at
398
// the bottom causes scrollHeight to be larger than it should be.
399
let overflows =
400
overlay.scrollWidth > pluginWidth ||
401
overlay.scrollHeight - 5 > pluginHeight;
402
if (overflows) {
403
overlay.setAttribute("sizing", "blank");
404
return OVERLAY_DISPLAY.BLANK;
405
}
406
407
// Is the plugin covered up by other content so that it is not clickable?
408
// Floating point can confuse .elementFromPoint, so inset just a bit
409
let left = pluginRect.left + 2;
410
let right = pluginRect.right - 2;
411
let top = pluginRect.top + 2;
412
let bottom = pluginRect.bottom - 2;
413
let centerX = left + (right - left) / 2;
414
let centerY = top + (bottom - top) / 2;
415
let points = [
416
[left, top],
417
[left, bottom],
418
[right, top],
419
[right, bottom],
420
[centerX, centerY],
421
];
422
423
for (let [x, y] of points) {
424
if (x < 0 || y < 0) {
425
continue;
426
}
427
let el = cwu.elementFromPoint(x, y, true, true);
428
if (el === plugin) {
429
return overlayDisplay;
430
}
431
}
432
433
overlay.setAttribute("sizing", "blank");
434
return OVERLAY_DISPLAY.BLANK;
435
}
436
437
addLinkClickCallback(linkNode, callbackName /* callbackArgs...*/) {
438
// XXX just doing (callback)(arg) was giving a same-origin error. bug?
439
let self = this;
440
let callbackArgs = Array.prototype.slice.call(arguments).slice(2);
441
linkNode.addEventListener(
442
"click",
443
function(evt) {
444
if (!evt.isTrusted) {
445
return;
446
}
447
evt.preventDefault();
448
if (callbackArgs.length == 0) {
449
callbackArgs = [evt];
450
}
451
self[callbackName].apply(self, callbackArgs);
452
},
453
true
454
);
455
456
linkNode.addEventListener(
457
"keydown",
458
function(evt) {
459
if (!evt.isTrusted) {
460
return;
461
}
462
if (evt.keyCode == evt.DOM_VK_RETURN) {
463
evt.preventDefault();
464
if (callbackArgs.length == 0) {
465
callbackArgs = [evt];
466
}
467
evt.preventDefault();
468
self[callbackName].apply(self, callbackArgs);
469
}
470
},
471
true
472
);
473
}
474
475
// Helper to get the binding handler type from a plugin object
476
_getBindingType(plugin) {
477
if (!(plugin instanceof Ci.nsIObjectLoadingContent)) {
478
return null;
479
}
480
481
switch (plugin.pluginFallbackType) {
482
case Ci.nsIObjectLoadingContent.PLUGIN_UNSUPPORTED:
483
return "PluginNotFound";
484
case Ci.nsIObjectLoadingContent.PLUGIN_DISABLED:
485
return "PluginDisabled";
486
case Ci.nsIObjectLoadingContent.PLUGIN_BLOCKLISTED:
487
return "PluginBlocklisted";
488
case Ci.nsIObjectLoadingContent.PLUGIN_OUTDATED:
489
return "PluginOutdated";
490
case Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY:
491
case Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY_QUIET:
492
return "PluginClickToPlay";
493
case Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_UPDATABLE:
494
return "PluginVulnerableUpdatable";
495
case Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_NO_UPDATE:
496
return "PluginVulnerableNoUpdate";
497
default:
498
// Not all states map to a handler
499
return null;
500
}
501
}
502
503
handleEvent(event) {
504
let eventType = event.type;
505
506
if (eventType == "pagehide") {
507
this.onPageHide(event);
508
return;
509
}
510
511
if (eventType == "pageshow") {
512
this.onPageShow(event);
513
return;
514
}
515
516
if (eventType == "click") {
517
this.onOverlayClick(event);
518
return;
519
}
520
521
if (
522
eventType == "PluginCrashed" &&
523
!(event.target instanceof Ci.nsIObjectLoadingContent)
524
) {
525
// If the event target is not a plugin object (i.e., an <object> or
526
// <embed> element), this call is for a window-global plugin.
527
this.onPluginCrashed(event.target, event);
528
return;
529
}
530
531
if (eventType == "HiddenPlugin") {
532
let pluginTag = event.tag.QueryInterface(Ci.nsIPluginTag);
533
if (event.target.defaultView.top.document != this.content.document) {
534
return;
535
}
536
this._showClickToPlayNotification(pluginTag, false);
537
}
538
539
let plugin = event.target;
540
541
if (!(plugin instanceof Ci.nsIObjectLoadingContent)) {
542
return;
543
}
544
545
if (eventType == "PluginBindingAttached") {
546
// The plugin binding fires this event when it is created.
547
// As an untrusted event, ensure that this object actually has a binding
548
// and make sure we don't handle it twice
549
let overlay = this.getPluginUI(plugin, "main");
550
if (!overlay || overlay._bindingHandled) {
551
return;
552
}
553
overlay._bindingHandled = true;
554
555
// Lookup the handler for this binding
556
eventType = this._getBindingType(plugin);
557
if (!eventType) {
558
// Not all bindings have handlers
559
return;
560
}
561
}
562
563
let shouldShowNotification = false;
564
switch (eventType) {
565
case "PluginCrashed":
566
this.onPluginCrashed(plugin, event);
567
break;
568
569
case "PluginNotFound": {
570
/* NOP */
571
break;
572
}
573
574
case "PluginBlocklisted":
575
case "PluginOutdated":
576
shouldShowNotification = true;
577
break;
578
579
case "PluginVulnerableUpdatable":
580
let updateLink = this.getPluginUI(plugin, "checkForUpdatesLink");
581
let { pluginTag } = this._getPluginInfo(plugin);
582
this.addLinkClickCallback(
583
updateLink,
584
"forwardCallback",
585
"openPluginUpdatePage",
586
pluginTag
587
);
588
/* FALLTHRU */
589
590
case "PluginVulnerableNoUpdate":
591
case "PluginClickToPlay":
592
this._handleClickToPlayEvent(plugin);
593
let pluginName = this._getPluginInfo(plugin).pluginName;
594
let messageString = gNavigatorBundle.formatStringFromName(
595
"PluginClickToActivate2",
596
[pluginName]
597
);
598
let overlayText = this.getPluginUI(plugin, "clickToPlay");
599
overlayText.textContent = messageString;
600
if (
601
eventType == "PluginVulnerableUpdatable" ||
602
eventType == "PluginVulnerableNoUpdate"
603
) {
604
let vulnerabilityString = gNavigatorBundle.GetStringFromName(
605
eventType
606
);
607
let vulnerabilityText = this.getPluginUI(
608
plugin,
609
"vulnerabilityStatus"
610
);
611
vulnerabilityText.textContent = vulnerabilityString;
612
}
613
shouldShowNotification = true;
614
break;
615
616
case "PluginDisabled":
617
let manageLink = this.getPluginUI(plugin, "managePluginsLink");
618
this.addLinkClickCallback(
619
manageLink,
620
"forwardCallback",
621
"managePlugins"
622
);
623
shouldShowNotification = true;
624
break;
625
626
case "PluginInstantiated":
627
shouldShowNotification = true;
628
break;
629
}
630
631
// Show the in-content UI if it's not too big. The crashed plugin handler already did this.
632
let overlay = this.getPluginUI(plugin, "main");
633
if (eventType != "PluginCrashed") {
634
if (overlay != null) {
635
this.setVisibility(
636
plugin,
637
overlay,
638
this.computeAndAdjustOverlayDisplay(plugin, overlay, false)
639
);
640
641
let resizeListener = () => {
642
this.setVisibility(
643
plugin,
644
overlay,
645
this.computeAndAdjustOverlayDisplay(plugin, overlay, true)
646
);
647
};
648
plugin.addEventListener("overflow", resizeListener);
649
plugin.addEventListener("underflow", resizeListener);
650
}
651
}
652
653
let closeIcon = this.getPluginUI(plugin, "closeIcon");
654
if (closeIcon) {
655
closeIcon.addEventListener(
656
"click",
657
clickEvent => {
658
if (clickEvent.button == 0 && clickEvent.isTrusted) {
659
this.hideClickToPlayOverlay(plugin);
660
overlay.setAttribute("dismissed", "true");
661
}
662
},
663
true
664
);
665
}
666
667
if (shouldShowNotification) {
668
this._showClickToPlayNotification(plugin, false);
669
}
670
}
671
672
isKnownPlugin(objLoadingContent) {
673
return (
674
objLoadingContent.getContentTypeForMIMEType(
675
objLoadingContent.actualType
676
) == Ci.nsIObjectLoadingContent.TYPE_PLUGIN
677
);
678
}
679
680
canActivatePlugin(objLoadingContent) {
681
// if this isn't a known plugin, we can't activate it
682
// (this also guards pluginHost.getPermissionStringForType against
683
// unexpected input)
684
if (!this.isKnownPlugin(objLoadingContent)) {
685
return false;
686
}
687
688
let pluginHost = Cc["@mozilla.org/plugin/host;1"].getService(
689
Ci.nsIPluginHost
690
);
691
let permissionString = pluginHost.getPermissionStringForType(
692
objLoadingContent.actualType
693
);
694
let principal = objLoadingContent.ownerGlobal.top.document.nodePrincipal;
695
let pluginPermission = Services.perms.testPermissionFromPrincipal(
696
principal,
697
permissionString
698
);
699
700
let isFallbackTypeValid =
701
objLoadingContent.pluginFallbackType >=
702
Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY &&
703
objLoadingContent.pluginFallbackType <=
704
Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY_QUIET;
705
706
return (
707
!objLoadingContent.activated &&
708
pluginPermission != Ci.nsIPermissionManager.DENY_ACTION &&
709
isFallbackTypeValid
710
);
711
}
712
713
hideClickToPlayOverlay(plugin) {
714
let overlay = this.getPluginUI(plugin, "main");
715
if (overlay) {
716
overlay.classList.remove("visible");
717
}
718
}
719
720
// Forward a link click callback to the chrome process.
721
forwardCallback(name, pluginTag) {
722
this.mm.sendAsyncMessage("PluginContent:LinkClickCallback", {
723
name,
724
pluginTag,
725
});
726
}
727
728
submitReport(plugin) {
729
if (!AppConstants.MOZ_CRASHREPORTER) {
730
return;
731
}
732
if (!plugin) {
733
Cu.reportError(
734
"Attempted to submit crash report without an associated plugin."
735
);
736
return;
737
}
738
if (!(plugin instanceof Ci.nsIObjectLoadingContent)) {
739
Cu.reportError(
740
"Attempted to submit crash report on plugin that does not" +
741
"implement nsIObjectLoadingContent."
742
);
743
return;
744
}
745
746
let runID = plugin.runID;
747
let submitURLOptIn = this.getPluginUI(plugin, "submitURLOptIn").checked;
748
let keyVals = {};
749
let userComment = this.getPluginUI(plugin, "submitComment").value.trim();
750
if (userComment) {
751
keyVals.PluginUserComment = userComment;
752
}
753
if (submitURLOptIn) {
754
keyVals.PluginContentURL = plugin.ownerDocument.URL;
755
}
756
757
this.mm.sendAsyncMessage("PluginContent:SubmitReport", {
758
runID,
759
keyVals,
760
submitURLOptIn,
761
});
762
}
763
764
reloadPage() {
765
this.content.location.reload();
766
}
767
768
// Event listener for click-to-play plugins.
769
_handleClickToPlayEvent(plugin) {
770
let doc = plugin.ownerDocument;
771
let pluginHost = Cc["@mozilla.org/plugin/host;1"].getService(
772
Ci.nsIPluginHost
773
);
774
let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
775
// guard against giving pluginHost.getPermissionStringForType a type
776
// not associated with any known plugin
777
if (!this.isKnownPlugin(objLoadingContent)) {
778
return;
779
}
780
let permissionString = pluginHost.getPermissionStringForType(
781
objLoadingContent.actualType
782
);
783
let principal = doc.defaultView.top.document.nodePrincipal;
784
let pluginPermission = Services.perms.testPermissionFromPrincipal(
785
principal,
786
permissionString
787
);
788
789
let overlay = this.getPluginUI(plugin, "main");
790
791
if (
792
pluginPermission == Ci.nsIPermissionManager.DENY_ACTION ||
793
pluginPermission ==
794
Ci.nsIObjectLoadingContent.PLUGIN_PERMISSION_PROMPT_ACTION_QUIET
795
) {
796
if (overlay) {
797
overlay.classList.remove("visible");
798
}
799
return;
800
}
801
802
if (overlay) {
803
overlay.addEventListener("click", this, true);
804
}
805
}
806
807
onOverlayClick(event) {
808
let document = event.target.ownerDocument;
809
let plugin = document.getBindingParent(event.target);
810
let overlay = this.getPluginUI(plugin, "main");
811
// Have to check that the target is not the link to update the plugin
812
if (
813
!(
814
ChromeUtils.getClassName(event.originalTarget) === "HTMLAnchorElement"
815
) &&
816
event.originalTarget.getAttribute("anonid") != "closeIcon" &&
817
event.originalTarget.id != "closeIcon" &&
818
!overlay.hasAttribute("dismissed") &&
819
event.button == 0 &&
820
event.isTrusted
821
) {
822
this._showClickToPlayNotification(plugin, true);
823
event.stopPropagation();
824
event.preventDefault();
825
}
826
}
827
828
reshowClickToPlayNotification() {
829
let contentWindow = this.content;
830
let cwu = contentWindow.windowUtils;
831
let plugins = cwu.plugins;
832
for (let plugin of plugins) {
833
let overlay = this.getPluginUI(plugin, "main");
834
if (overlay) {
835
overlay.removeEventListener("click", this, true);
836
}
837
let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
838
if (this.canActivatePlugin(objLoadingContent)) {
839
this._handleClickToPlayEvent(plugin);
840
}
841
}
842
this._showClickToPlayNotification(null, false);
843
}
844
845
/**
846
* Activate the plugins that the user has specified.
847
*/
848
activatePlugins(pluginInfo, newState) {
849
let contentWindow = this.content;
850
let cwu = contentWindow.windowUtils;
851
let plugins = cwu.plugins;
852
let pluginHost = Cc["@mozilla.org/plugin/host;1"].getService(
853
Ci.nsIPluginHost
854
);
855
856
let pluginFound = false;
857
for (let plugin of plugins) {
858
plugin.QueryInterface(Ci.nsIObjectLoadingContent);
859
if (!this.isKnownPlugin(plugin)) {
860
continue;
861
}
862
if (
863
pluginInfo.permissionString ==
864
pluginHost.getPermissionStringForType(plugin.actualType)
865
) {
866
let overlay = this.getPluginUI(plugin, "main");
867
pluginFound = true;
868
if (
869
newState == "block" ||
870
newState == "blockalways" ||
871
newState == "continueblocking"
872
) {
873
if (overlay) {
874
overlay.addEventListener("click", this, true);
875
}
876
plugin.pluginFallbackTypeOverride = pluginInfo.fallbackType;
877
plugin.reload(true);
878
} else if (this.canActivatePlugin(plugin)) {
879
if (overlay) {
880
overlay.removeEventListener("click", this, true);
881
}
882
plugin.playPlugin();
883
}
884
}
885
}
886
887
// If there are no instances of the plugin on the page any more or if we've
888
// noted that the content needs to be reloaded due to replacing HLS, what the
889
// user probably needs is for us to allow and then refresh.
890
if (
891
newState != "block" &&
892
newState != "blockalways" &&
893
newState != "continueblocking" &&
894
(!pluginFound || contentWindow.pluginRequiresReload)
895
) {
896
this.reloadPage();
897
}
898
}
899
900
_showClickToPlayNotification(plugin, showNow) {
901
let plugins = [];
902
903
// If plugin is null, that means the user has navigated back to a page with
904
// plugins, and we need to collect all the plugins.
905
if (plugin === null) {
906
let contentWindow = this.content;
907
let cwu = contentWindow.windowUtils;
908
// cwu.plugins may contain non-plugin <object>s, filter them out
909
plugins = cwu.plugins.filter(
910
p =>
911
p.getContentTypeForMIMEType(p.actualType) ==
912
Ci.nsIObjectLoadingContent.TYPE_PLUGIN
913
);
914
915
if (plugins.length == 0) {
916
this.removeNotification("click-to-play-plugins");
917
return;
918
}
919
} else {
920
plugins = [plugin];
921
}
922
923
let pluginData = this.pluginData;
924
925
let principal = this.content.document.nodePrincipal;
926
let location = this.content.document.location.href;
927
928
for (let p of plugins) {
929
let pluginInfo;
930
if (p instanceof Ci.nsIPluginTag) {
931
let mimeType = p.getMimeTypes() > 0 ? p.getMimeTypes()[0] : null;
932
pluginInfo = this._getPluginInfoForTag(p, mimeType);
933
} else {
934
pluginInfo = this._getPluginInfo(p);
935
}
936
if (pluginInfo.permissionString === null) {
937
Cu.reportError("No permission string for active plugin.");
938
continue;
939
}
940
if (pluginData.has(pluginInfo.permissionString)) {
941
continue;
942
}
943
944
let permissionObj = Services.perms.getPermissionObject(
945
principal,
946
pluginInfo.permissionString,
947
false
948
);
949
if (permissionObj) {
950
pluginInfo.pluginPermissionPrePath =
951
permissionObj.principal.originNoSuffix;
952
pluginInfo.pluginPermissionType = permissionObj.expireType;
953
} else {
954
pluginInfo.pluginPermissionPrePath = principal.originNoSuffix;
955
pluginInfo.pluginPermissionType = undefined;
956
}
957
958
this.pluginData.set(pluginInfo.permissionString, pluginInfo);
959
}
960
961
this.haveShownNotification = true;
962
963
this.mm.sendAsyncMessage(
964
"PluginContent:ShowClickToPlayNotification",
965
{
966
plugins: [...this.pluginData.values()],
967
showNow,
968
location,
969
},
970
null,
971
principal
972
);
973
}
974
975
removeNotification(name) {
976
this.mm.sendAsyncMessage("PluginContent:RemoveNotification", { name });
977
}
978
979
clearPluginCaches() {
980
this.pluginData.clear();
981
this.pluginCrashData.clear();
982
}
983
984
/**
985
* Determines whether or not the crashed plugin is contained within current
986
* full screen DOM element.
987
* @param fullScreenElement (DOM element)
988
* The DOM element that is currently full screen, or null.
989
* @param domElement
990
* The DOM element which contains the crashed plugin, or the crashed plugin
991
* itself.
992
* @returns bool
993
* True if the plugin is a descendant of the full screen DOM element, false otherwise.
994
**/
995
isWithinFullScreenElement(fullScreenElement, domElement) {
996
/**
997
* Traverses down iframes until it find a non-iframe full screen DOM element.
998
* @param fullScreenIframe
999
* Target iframe to begin searching from.
1000
* @returns DOM element
1001
* The full screen DOM element contained within the iframe (could be inner iframe), or the original iframe if no inner DOM element is found.
1002
**/
1003
let getTrueFullScreenElement = fullScreenIframe => {
1004
if (
1005
typeof fullScreenIframe.contentDocument !== "undefined" &&
1006
fullScreenIframe.contentDocument.mozFullScreenElement
1007
) {
1008
return getTrueFullScreenElement(
1009
fullScreenIframe.contentDocument.mozFullScreenElement
1010
);
1011
}
1012
return fullScreenIframe;
1013
};
1014
1015
if (fullScreenElement.tagName === "IFRAME") {
1016
fullScreenElement = getTrueFullScreenElement(fullScreenElement);
1017
}
1018
1019
if (fullScreenElement.contains(domElement)) {
1020
return true;
1021
}
1022
let parentIframe = domElement.ownerGlobal.frameElement;
1023
if (parentIframe) {
1024
return this.isWithinFullScreenElement(fullScreenElement, parentIframe);
1025
}
1026
return false;
1027
}
1028
1029
/**
1030
* The PluginCrashed event handler. Note that the PluginCrashed event is
1031
* fired for both NPAPI and Gecko Media plugins. In the latter case, the
1032
* target of the event is the document that the GMP is being used in.
1033
*/
1034
onPluginCrashed(target, aEvent) {
1035
if (!(aEvent instanceof this.content.PluginCrashedEvent)) {
1036
return;
1037
}
1038
1039
let fullScreenElement = this.content.document.mozFullScreenElement;
1040
if (fullScreenElement) {
1041
if (this.isWithinFullScreenElement(fullScreenElement, target)) {
1042
this.content.document.mozCancelFullScreen();
1043
}
1044
}
1045
1046
if (aEvent.gmpPlugin) {
1047
this.GMPCrashed(aEvent);
1048
return;
1049
}
1050
1051
if (!(target instanceof Ci.nsIObjectLoadingContent)) {
1052
return;
1053
}
1054
1055
let crashData = this.pluginCrashData.get(target.runID);
1056
if (!crashData) {
1057
// We haven't received information from the parent yet about
1058
// this crash, so we should hold off showing the crash report
1059
// UI.
1060
return;
1061
}
1062
1063
crashData.instances.delete(target);
1064
if (crashData.instances.length == 0) {
1065
this.pluginCrashData.delete(target.runID);
1066
}
1067
1068
this.setCrashedNPAPIPluginState({
1069
plugin: target,
1070
state: crashData.state,
1071
message: crashData.message,
1072
});
1073
}
1074
1075
NPAPIPluginProcessCrashed({ pluginName, runID, state }) {
1076
let message = gNavigatorBundle.formatStringFromName(
1077
"crashedpluginsMessage.title",
1078
[pluginName]
1079
);
1080
1081
let contentWindow = this.content;
1082
let cwu = contentWindow.windowUtils;
1083
let plugins = cwu.plugins;
1084
1085
for (let plugin of plugins) {
1086
if (
1087
plugin instanceof Ci.nsIObjectLoadingContent &&
1088
plugin.runID == runID
1089
) {
1090
// The parent has told us that the plugin process has died.
1091
// It's possible that this content process hasn't yet noticed,
1092
// in which case we need to stash this data around until the
1093
// PluginCrashed events get sent up.
1094
if (
1095
plugin.pluginFallbackType == Ci.nsIObjectLoadingContent.PLUGIN_CRASHED
1096
) {
1097
// This plugin has already been put into the crashed state by the
1098
// content process, so we can tweak its crash UI without delay.
1099
this.setCrashedNPAPIPluginState({ plugin, state, message });
1100
} else {
1101
// The content process hasn't yet determined that the plugin has crashed.
1102
// Stash the data in our map, and throw the plugin into a WeakSet. When
1103
// the PluginCrashed event fires on the <object>/<embed>, we'll retrieve
1104
// the information we need from the Map and remove the instance from the
1105
// WeakSet. Once the WeakSet is empty, we can clear the map.
1106
if (!this.pluginCrashData.has(runID)) {
1107
this.pluginCrashData.set(runID, {
1108
state,
1109
message,
1110
instances: new WeakSet(),
1111
});
1112
}
1113
let crashData = this.pluginCrashData.get(runID);
1114
crashData.instances.add(plugin);
1115
}
1116
}
1117
}
1118
}
1119
1120
setCrashedNPAPIPluginState({ plugin, state, message }) {
1121
// Force a layout flush so the binding is attached.
1122
plugin.clientTop;
1123
let overlay = this.getPluginUI(plugin, "main");
1124
let statusDiv = this.getPluginUI(plugin, "submitStatus");
1125
let optInCB = this.getPluginUI(plugin, "submitURLOptIn");
1126
1127
this.getPluginUI(plugin, "submitButton").addEventListener(
1128
"click",
1129
event => {
1130
if (event.button != 0 || !event.isTrusted) {
1131
return;
1132
}
1133
this.submitReport(plugin);
1134
}
1135
);
1136
1137
let pref = Services.prefs.getBranch("dom.ipc.plugins.reportCrashURL");
1138
optInCB.checked = pref.getBoolPref("");
1139
1140
statusDiv.setAttribute("status", state);
1141
1142
let helpIcon = this.getPluginUI(plugin, "helpIcon");
1143
this.addLinkClickCallback(helpIcon, "openHelpPage");
1144
1145
let crashText = this.getPluginUI(plugin, "crashedText");
1146
crashText.textContent = message;
1147
1148
let link = this.getPluginUI(plugin, "reloadLink");
1149
this.addLinkClickCallback(link, "reloadPage");
1150
1151
// This might trigger force reflow, but plug-in crashing code path shouldn't be hot.
1152
let overlayDisplayState = this.computeAndAdjustOverlayDisplay(
1153
plugin,
1154
overlay,
1155
true
1156
);
1157
1158
// Is the <object>'s size too small to hold what we want to show?
1159
if (overlayDisplayState != OVERLAY_DISPLAY.FULL) {
1160
// First try hiding the crash report submission UI.
1161
statusDiv.removeAttribute("status");
1162
1163
overlayDisplayState = this.computeAndAdjustOverlayDisplay(
1164
plugin,
1165
overlay,
1166
true
1167
);
1168
}
1169
this.setVisibility(plugin, overlay, overlayDisplayState);
1170
1171
let doc = plugin.ownerDocument;
1172
let runID = plugin.runID;
1173
1174
if (overlayDisplayState == OVERLAY_DISPLAY.FULL) {
1175
doc.mozNoPluginCrashedNotification = true;
1176
1177
// Notify others that the crash reporter UI is now ready.
1178
// Currently, this event is only used by tests.
1179
let winUtils = this.content.windowUtils;
1180
let event = new this.content.CustomEvent("PluginCrashReporterDisplayed", {
1181
bubbles: true,
1182
});
1183
winUtils.dispatchEventToChromeOnly(plugin, event);
1184
} else if (!doc.mozNoPluginCrashedNotification) {
1185
// If another plugin on the page was large enough to show our UI, we don't
1186
// want to show a notification bar.
1187
this.mm.sendAsyncMessage("PluginContent:ShowPluginCrashedNotification", {
1188
messageString: message,
1189
pluginID: runID,
1190
});
1191
}
1192
}
1193
1194
NPAPIPluginCrashReportSubmitted({ runID, state }) {
1195
this.pluginCrashData.delete(runID);
1196
let contentWindow = this.content;
1197
let cwu = contentWindow.windowUtils;
1198
let plugins = cwu.plugins;
1199
1200
for (let plugin of plugins) {
1201
if (
1202
plugin instanceof Ci.nsIObjectLoadingContent &&
1203
plugin.runID == runID
1204
) {
1205
let statusDiv = this.getPluginUI(plugin, "submitStatus");
1206
statusDiv.setAttribute("status", state);
1207
}
1208
}
1209
}
1210
1211
GMPCrashed(aEvent) {
1212
let target = aEvent.target;
1213
let pluginName = aEvent.pluginName;
1214
let gmpPlugin = aEvent.gmpPlugin;
1215
let pluginID = aEvent.pluginID;
1216
let doc = target.document;
1217
1218
if (!gmpPlugin || !doc) {
1219
// TODO: Throw exception? How did we get here?
1220
return;
1221
}
1222
1223
let messageString = gNavigatorBundle.formatStringFromName(
1224
"crashedpluginsMessage.title",
1225
[pluginName]
1226
);
1227
1228
this.mm.sendAsyncMessage("PluginContent:ShowPluginCrashedNotification", {
1229
messageString,
1230
pluginID,
1231
});
1232
}
1233
}