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