Mozilla Cross-Reference mozilla-central
mozilla/ browser/ base/ content/ browser-plugins.js
Hg Log
Hg Blame
Diff file
Raw file
view using tree:
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 
view http://hg.mozilla.org/mozilla-central/rev/ /browser/base/content/browser-plugins.js