 |
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 var gPluginHandler = {
7 PREF_SESSION_PERSIST_MINUTES: "plugin.sessionPermissionNow.intervalInMinutes",
8 PREF_PERSISTENT_DAYS: "plugin.persistentPermissionAlways.intervalInDays",
9 MESSAGES: [
10 "PluginContent:ShowClickToPlayNotification",
11 "PluginContent:RemoveNotification",
12 "PluginContent:UpdateHiddenPluginUI",
13 "PluginContent:HideNotificationBar",
14 "PluginContent:ShowInstallNotification",
15 "PluginContent:InstallSinglePlugin",
16 "PluginContent:ShowPluginCrashedNotification",
17 "PluginContent:SubmitReport",
18 "PluginContent:LinkClickCallback",
19 ],
20
21 init: function () {
22 const mm = window.messageManager;
23 for (let msg of this.MESSAGES) {
24 mm.addMessageListener(msg, this);
25 }
26 window.addEventListener("unload", this);
27 },
28
29 uninit: function () {
30 const mm = window.messageManager;
31 for (let msg of this.MESSAGES) {
32 mm.removeMessageListener(msg, this);
33 }
34 window.removeEventListener("unload", this);
35 },
36
37 handleEvent: function (event) {
38 if (event.type == "unload") {
39 this.uninit();
40 }
41 },
42
43 receiveMessage: function (msg) {
44 switch (msg.name) {
45 case "PluginContent:ShowClickToPlayNotification":
46 this.showClickToPlayNotification(msg.target, msg.data.plugins, msg.data.showNow,
47 msg.principal, msg.data.host, msg.data.location);
48 break;
49 case "PluginContent:RemoveNotification":
50 this.removeNotification(msg.target, msg.data.name);
51 break;
52 case "PluginContent:UpdateHiddenPluginUI":
53 this.updateHiddenPluginUI(msg.target, msg.data.haveInsecure, msg.data.actions,
54 msg.principal, msg.data.host, msg.data.location);
55 break;
56 case "PluginContent:HideNotificationBar":
57 this.hideNotificationBar(msg.target, msg.data.name);
58 break;
59 case "PluginContent:ShowInstallNotification":
60 return this.showInstallNotification(msg.target, msg.data.pluginInfo);
61 case "PluginContent:InstallSinglePlugin":
62 this.installSinglePlugin(msg.data.pluginInfo);
63 break;
64 case "PluginContent:ShowPluginCrashedNotification":
65 this.showPluginCrashedNotification(msg.target, msg.data.messageString,
66 msg.data.pluginDumpID, msg.data.browserDumpID);
67 break;
68 case "PluginContent:SubmitReport":
69 this.submitReport(msg.data.pluginDumpID, msg.data.browserDumpID, msg.data.keyVals);
70 break;
71 case "PluginContent:LinkClickCallback":
72 switch (msg.data.name) {
73 case "managePlugins":
74 case "openHelpPage":
75 case "openPluginUpdatePage":
76 this[msg.data.name].apply(this);
77 break;
78 }
79 break;
80 default:
81 Cu.reportError("gPluginHandler did not expect to handle message " + msg.name);
82 break;
83 }
84 },
85
86 #ifdef MOZ_CRASHREPORTER
87 get CrashSubmit() {
88 delete this.CrashSubmit;
89 Cu.import("resource://gre/modules/CrashSubmit.jsm", this);
90 return this.CrashSubmit;
91 },
92 #endif
93
94 // Callback for user clicking on a disabled plugin
95 managePlugins: function () {
96 BrowserOpenAddonsMgr("addons://list/plugin");
97 },
98
99 // Callback for user clicking on the link in a click-to-play plugin
100 // (where the plugin has an update)
101 openPluginUpdatePage: function () {
102 openUILinkIn(Services.urlFormatter.formatURLPref("plugins.update.url"), "tab");
103 },
104
105 #ifdef MOZ_CRASHREPORTER
106 submitReport: function submitReport(pluginDumpID, browserDumpID, keyVals) {
107 keyVals = keyVals || {};
108 this.CrashSubmit.submit(pluginDumpID, { recordSubmission: true,
109 extraExtraKeyVals: keyVals });
110 if (browserDumpID)
111 this.CrashSubmit.submit(browserDumpID);
112 },
113 #endif
114
115 // Callback for user clicking a "reload page" link
116 reloadPage: function (browser) {
117 browser.reload();
118 },
119
120 // Callback for user clicking the help icon
121 openHelpPage: function () {
122 openHelpLink("plugin-crashed", false);
123 },
124
125 _clickToPlayNotificationEventCallback: function PH_ctpEventCallback(event) {
126 if (event == "showing") {
127 Services.telemetry.getHistogramById("PLUGINS_NOTIFICATION_SHOWN")
128 .add(!this.options.primaryPlugin);
129 // Histograms always start at 0, even though our data starts at 1
130 let histogramCount = this.options.pluginData.size - 1;
131 if (histogramCount > 4) {
132 histogramCount = 4;
133 }
134 Services.telemetry.getHistogramById("PLUGINS_NOTIFICATION_PLUGIN_COUNT")
135 .add(histogramCount);
136 }
137 else if (event == "dismissed") {
138 // Once the popup is dismissed, clicking the icon should show the full
139 // list again
140 this.options.primaryPlugin = null;
141 }
142 },
143
144 /**
145 * Called from the plugin doorhanger to set the new permissions for a plugin
146 * and activate plugins if necessary.
147 * aNewState should be either "allownow" "allowalways" or "block"
148 */
149 _updatePluginPermission: function (aNotification, aPluginInfo, aNewState) {
150 let permission;
151 let expireType;
152 let expireTime;
153 let histogram =
154 Services.telemetry.getHistogramById("PLUGINS_NOTIFICATION_USER_ACTION");
155
156 // Update the permission manager.
157 // Also update the current state of pluginInfo.fallbackType so that
158 // subsequent opening of the notification shows the current state.
159 switch (aNewState) {
160 case "allownow":
161 permission = Ci.nsIPermissionManager.ALLOW_ACTION;
162 expireType = Ci.nsIPermissionManager.EXPIRE_SESSION;
163 expireTime = Date.now() + Services.prefs.getIntPref(this.PREF_SESSION_PERSIST_MINUTES) * 60 * 1000;
164 histogram.add(0);
165 aPluginInfo.fallbackType = Ci.nsIObjectLoadingContent.PLUGIN_ACTIVE;
166 break;
167
168 case "allowalways":
169 permission = Ci.nsIPermissionManager.ALLOW_ACTION;
170 expireType = Ci.nsIPermissionManager.EXPIRE_TIME;
171 expireTime = Date.now() +
172 Services.prefs.getIntPref(this.PREF_PERSISTENT_DAYS) * 24 * 60 * 60 * 1000;
173 histogram.add(1);
174 aPluginInfo.fallbackType = Ci.nsIObjectLoadingContent.PLUGIN_ACTIVE;
175 break;
176
177 case "block":
178 permission = Ci.nsIPermissionManager.PROMPT_ACTION;
179 expireType = Ci.nsIPermissionManager.EXPIRE_NEVER;
180 expireTime = 0;
181 histogram.add(2);
182 switch (aPluginInfo.blocklistState) {
183 case Ci.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE:
184 aPluginInfo.fallbackType = Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_UPDATABLE;
185 break;
186 case Ci.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE:
187 aPluginInfo.fallbackType = Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_NO_UPDATE;
188 break;
189 default:
190 aPluginInfo.fallbackType = Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY;
191 }
192 break;
193
194 // In case a plugin has already been allowed in another tab, the "continue allowing" button
195 // shouldn't change any permissions but should run the plugin-enablement code below.
196 case "continue":
197 aPluginInfo.fallbackType = Ci.nsIObjectLoadingContent.PLUGIN_ACTIVE;
198 break;
199 default:
200 Cu.reportError(Error("Unexpected plugin state: " + aNewState));
201 return;
202 }
203
204 let browser = aNotification.browser;
205 let contentWindow = browser.contentWindow;
206 if (aNewState != "continue") {
207 let principal = aNotification.options.principal;
208 Services.perms.addFromPrincipal(principal, aPluginInfo.permissionString,
209 permission, expireType, expireTime);
210 aPluginInfo.pluginPermissionType = expireType;
211 }
212
213 browser.messageManager.sendAsyncMessage("BrowserPlugins:ActivatePlugins", {
214 pluginInfo: aPluginInfo,
215 newState: aNewState,
216 });
217 },
218
219 showClickToPlayNotification: function (browser, plugins, showNow, principal,
220 host, location) {
221 // It is possible that we've received a message from the frame script to show
222 // a click to play notification for a principal that no longer matches the one
223 // that the browser's content now has assigned (ie, the browser has browsed away
224 // after the message was sent, but before the message was received). In that case,
225 // we should just ignore the message.
226 if (!principal.equals(browser.contentPrincipal)) {
227 return;
228 }
229
230 // Data URIs, when linked to from some page, inherit the principal of that
231 // page. That means that we also need to compare the actual locations to
232 // ensure we aren't getting a message from a Data URI that we're no longer
233 // looking at.
234 let receivedURI = BrowserUtils.makeURI(location);
235 if (!browser.documentURI.equalsExceptRef(receivedURI)) {
236 return;
237 }
238
239 let notification = PopupNotifications.getNotification("click-to-play-plugins", browser);
240
241 // If this is a new notification, create a pluginData map, otherwise append
242 let pluginData;
243 if (notification) {
244 pluginData = notification.options.pluginData;
245 } else {
246 pluginData = new Map();
247 }
248
249 for (var pluginInfo of plugins) {
250 if (pluginData.has(pluginInfo.permissionString)) {
251 continue;
252 }
253
254 // If a block contains an infoURL, we should always prefer that to the default
255 // URL that we construct in-product, even for other blocklist types.
256 let url = Services.blocklist.getPluginInfoURL(pluginInfo.pluginTag);
257
258 if (pluginInfo.blocklistState == Ci.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE) {
259 if (!url) {
260 url = Services.urlFormatter.formatURLPref("plugins.update.url");
261 }
262 }
263 else if (pluginInfo.blocklistState != Ci.nsIBlocklistService.STATE_NOT_BLOCKED) {
264 if (!url) {
265 url = Services.blocklist.getPluginBlocklistURL(pluginInfo.pluginTag);
266 }
267 }
268 else {
269 url = Services.urlFormatter.formatURLPref("app.support.baseURL") + "clicktoplay";
270 }
271 pluginInfo.detailsLink = url;
272
273 pluginData.set(pluginInfo.permissionString, pluginInfo);
274 }
275
276 let primaryPluginPermission = null;
277 if (showNow) {
278 primaryPluginPermission = plugins[0].permissionString;
279 }
280
281 if (notification) {
282 // Don't modify the notification UI while it's on the screen, that would be
283 // jumpy and might allow clickjacking.
284 if (showNow) {
285 notification.options.primaryPlugin = primaryPluginPermission;
286 notification.reshow();
287 browser.messageManager.sendAsyncMessage("BrowserPlugins:NotificationShown");
288 }
289 return;
290 }
291
292 let options = {
293 dismissed: !showNow,
294 eventCallback: this._clickToPlayNotificationEventCallback,
295 primaryPlugin: primaryPluginPermission,
296 pluginData: pluginData,
297 principal: principal,
298 host: host,
299 };
300 PopupNotifications.show(browser, "click-to-play-plugins",
301 "", "plugins-notification-icon",
302 null, null, options);
303 browser.messageManager.sendAsyncMessage("BrowserPlugins:NotificationShown");
304 },
305
306 removeNotification: function (browser, name) {
307 let notification = PopupNotifications.getNotification(name, browser);
308 if (notification)
309 PopupNotifications.remove(notification);
310 },
311
312 hideNotificationBar: function (browser, name) {
313 let notificationBox = gBrowser.getNotificationBox(browser);
314 let notification = notificationBox.getNotificationWithValue(name);
315 if (notification)
316 notificationBox.removeNotification(notification, true);
317 },
318
319 updateHiddenPluginUI: function (browser, haveInsecure, actions, principal,
320 host, location) {
321 // It is possible that we've received a message from the frame script to show
322 // the hidden plugin notification for a principal that no longer matches the one
323 // that the browser's content now has assigned (ie, the browser has browsed away
324 // after the message was sent, but before the message was received). In that case,
325 // we should just ignore the message.
326 if (!principal.equals(browser.contentPrincipal)) {
327 return;
328 }
329
330 // Data URIs, when linked to from some page, inherit the principal of that
331 // page. That means that we also need to compare the actual locations to
332 // ensure we aren't getting a message from a Data URI that we're no longer
333 // looking at.
334 let receivedURI = BrowserUtils.makeURI(location);
335 if (!browser.documentURI.equalsExceptRef(receivedURI)) {
336 return;
337 }
338
339 // Set up the icon
340 document.getElementById("plugins-notification-icon").classList.
341 toggle("plugin-blocked", haveInsecure);
342
343 // Now configure the notification bar
344 let notificationBox = gBrowser.getNotificationBox(browser);
345
346 function hideNotification() {
347 let n = notificationBox.getNotificationWithValue("plugin-hidden");
348 if (n) {
349 notificationBox.removeNotification(n, true);
350 }
351 }
352
353 // There are three different cases when showing an infobar:
354 // 1. A single type of plugin is hidden on the page. Show the UI for that
355 // plugin.
356 // 2a. Multiple types of plugins are hidden on the page. Show the multi-UI
357 // with the vulnerable styling.
358 // 2b. Multiple types of plugins are hidden on the page, but none are
359 // vulnerable. Show the nonvulnerable multi-UI.
360 function showNotification() {
361 let n = notificationBox.getNotificationWithValue("plugin-hidden");
362 if (n) {
363 // If something is already shown, just keep it
364 return;
365 }
366
367 Services.telemetry.getHistogramById("PLUGINS_INFOBAR_SHOWN").
368 add(true);
369
370 let message;
371 // Icons set directly cannot be manipulated using moz-image-region, so
372 // we use CSS classes instead.
373 let brand = document.getElementById("bundle_brand").getString("brandShortName");
374
375 if (actions.length == 1) {
376 let pluginInfo = actions[0];
377 let pluginName = pluginInfo.pluginName;
378
379 switch (pluginInfo.fallbackType) {
380 case Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY:
381 message = gNavigatorBundle.getFormattedString(
382 "pluginActivateNew.message",
383 [pluginName, host]);
384 break;
385 case Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_UPDATABLE:
386 message = gNavigatorBundle.getFormattedString(
387 "pluginActivateOutdated.message",
388 [pluginName, host, brand]);
389 break;
390 case Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_NO_UPDATE:
391 message = gNavigatorBundle.getFormattedString(
392 "pluginActivateVulnerable.message",
393 [pluginName, host, brand]);
394 }
395 } else {
396 // Multi-plugin
397 message = gNavigatorBundle.getFormattedString(
398 "pluginActivateMultiple.message", [host]);
399 }
400
401 let buttons = [
402 {
403 label: gNavigatorBundle.getString("pluginContinueBlocking.label"),
404 accessKey: gNavigatorBundle.getString("pluginContinueBlocking.accesskey"),
405 callback: function() {
406 Services.telemetry.getHistogramById("PLUGINS_INFOBAR_BLOCK").
407 add(true);
408
409 Services.perms.addFromPrincipal(principal,
410 "plugin-hidden-notification",
411 Services.perms.DENY_ACTION);
412 }
413 },
414 {
415 label: gNavigatorBundle.getString("pluginActivateTrigger.label"),
416 accessKey: gNavigatorBundle.getString("pluginActivateTrigger.accesskey"),
417 callback: function() {
418 Services.telemetry.getHistogramById("PLUGINS_INFOBAR_ALLOW").
419 add(true);
420
421 let curNotification =
422 PopupNotifications.getNotification("click-to-play-plugins",
423 browser);
424 if (curNotification) {
425 curNotification.reshow();
426 }
427 }
428 }
429 ];
430 n = notificationBox.
431 appendNotification(message, "plugin-hidden", null,
432 notificationBox.PRIORITY_INFO_HIGH, buttons);
433 if (haveInsecure) {
434 n.classList.add('pluginVulnerable');
435 }
436 }
437
438 if (actions.length == 0) {
439 hideNotification();
440 } else {
441 let notificationPermission = Services.perms.testPermissionFromPrincipal(
442 principal, "plugin-hidden-notification");
443 if (notificationPermission == Ci.nsIPermissionManager.DENY_ACTION) {
444 hideNotification();
445 } else {
446 showNotification();
447 }
448 }
449 },
450
451 contextMenuCommand: function (browser, plugin, command) {
452 browser.messageManager.sendAsyncMessage("BrowserPlugins:ContextMenuCommand",
453 { command: command }, { plugin: plugin });
454 },
455
456 // Crashed-plugin observer. Notified once per plugin crash, before events
457 // are dispatched to individual plugin instances.
458 pluginCrashed : function(subject, topic, data) {
459 let propertyBag = subject;
460 if (!(propertyBag instanceof Ci.nsIPropertyBag2) ||
461 !(propertyBag instanceof Ci.nsIWritablePropertyBag2))
462 return;
463
464 #ifdef MOZ_CRASHREPORTER
465 let pluginDumpID = propertyBag.getPropertyAsAString("pluginDumpID");
466 let browserDumpID= propertyBag.getPropertyAsAString("browserDumpID");
467 let shouldSubmit = gCrashReporter.submitReports;
468 let doPrompt = true; // XXX followup to get via gCrashReporter
469
470 // Submit automatically when appropriate.
471 if (pluginDumpID && shouldSubmit && !doPrompt) {
472 this.submitReport(pluginDumpID, browserDumpID);
473 // Submission is async, so we can't easily show failure UI.
474 propertyBag.setPropertyAsBool("submittedCrashReport", true);
475 }
476 #endif
477 },
478
479 showPluginCrashedNotification: function (browser, messageString, pluginDumpID, browserDumpID) {
480 // If there's already an existing notification bar, don't do anything.
481 let notificationBox = gBrowser.getNotificationBox(browser);
482 let notification = notificationBox.getNotificationWithValue("plugin-crashed");
483 if (notification)
484 return;
485
486 // Configure the notification bar
487 let priority = notificationBox.PRIORITY_WARNING_MEDIUM;
488 let iconURL = "chrome://mozapps/skin/plugins/notifyPluginCrashed.png";
489 let reloadLabel = gNavigatorBundle.getString("crashedpluginsMessage.reloadButton.label");
490 let reloadKey = gNavigatorBundle.getString("crashedpluginsMessage.reloadButton.accesskey");
491 let submitLabel = gNavigatorBundle.getString("crashedpluginsMessage.submitButton.label");
492 let submitKey = gNavigatorBundle.getString("crashedpluginsMessage.submitButton.accesskey");
493
494 let buttons = [{
495 label: reloadLabel,
496 accessKey: reloadKey,
497 popup: null,
498 callback: function() { browser.reload(); },
499 }];
500
501 #ifdef MOZ_CRASHREPORTER
502 let submitButton = {
503 label: submitLabel,
504 accessKey: submitKey,
505 popup: null,
506 callback: function() { gPluginHandler.submitReport(pluginDumpID, browserDumpID); },
507 };
508 if (pluginDumpID)
509 buttons.push(submitButton);
510 #endif
511
512 notification = notificationBox.appendNotification(messageString, "plugin-crashed",
513 iconURL, priority, buttons);
514
515 // Add the "learn more" link.
516 let XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
517 let link = notification.ownerDocument.createElementNS(XULNS, "label");
518 link.className = "text-link";
519 link.setAttribute("value", gNavigatorBundle.getString("crashedpluginsMessage.learnMore"));
520 let crashurl = formatURL("app.support.baseURL", true);
521 crashurl += "plugin-crashed-notificationbar";
522 link.href = crashurl;
523
524 let description = notification.ownerDocument.getAnonymousElementByAttribute(notification, "anonid", "messageText");
525 description.appendChild(link);
526 },
527 };
528
529 gPluginHandler.init();
530
531