Mozilla Cross-Reference mozilla-central
mozilla/ browser/ base/ content/ browser-addons.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 const gXPInstallObserver = {
7   _findChildShell: function (aDocShell, aSoughtShell)
8   {
9     if (aDocShell == aSoughtShell)
10       return aDocShell;
11 
12     var node = aDocShell.QueryInterface(Components.interfaces.nsIDocShellTreeItem);
13     for (var i = 0; i < node.childCount; ++i) {
14       var docShell = node.getChildAt(i);
15       docShell = this._findChildShell(docShell, aSoughtShell);
16       if (docShell == aSoughtShell)
17         return docShell;
18     }
19     return null;
20   },
21 
22   _getBrowser: function (aDocShell)
23   {
24     for (let browser of gBrowser.browsers) {
25       if (this._findChildShell(browser.docShell, aDocShell))
26         return browser;
27     }
28     return null;
29   },
30 
31   observe: function (aSubject, aTopic, aData)
32   {
33     var brandBundle = document.getElementById("bundle_brand");
34     var installInfo = aSubject.QueryInterface(Components.interfaces.amIWebInstallInfo);
35     var browser = installInfo.browser;
36 
37     // Make sure the browser is still alive.
38     if (!browser || gBrowser.browsers.indexOf(browser) == -1)
39       return;
40 
41     const anchorID = "addons-notification-icon";
42     var messageString, action;
43     var brandShortName = brandBundle.getString("brandShortName");
44 
45     var notificationID = aTopic;
46     // Make notifications persist a minimum of 30 seconds
47     var options = {
48       timeout: Date.now() + 30000
49     };
50 
51     try {
52       options.originHost = installInfo.originatingURI.host;
53     } catch (e) {
54       // originatingURI might be missing or 'host' might throw for non-nsStandardURL nsIURIs.
55     }
56 
57     switch (aTopic) {
58     case "addon-install-disabled": {
59       notificationID = "xpinstall-disabled";
60 
61       if (gPrefService.prefIsLocked("xpinstall.enabled")) {
62         messageString = gNavigatorBundle.getString("xpinstallDisabledMessageLocked");
63         buttons = [];
64       }
65       else {
66         messageString = gNavigatorBundle.getString("xpinstallDisabledMessage");
67 
68         action = {
69           label: gNavigatorBundle.getString("xpinstallDisabledButton"),
70           accessKey: gNavigatorBundle.getString("xpinstallDisabledButton.accesskey"),
71           callback: function editPrefs() {
72             gPrefService.setBoolPref("xpinstall.enabled", true);
73           }
74         };
75       }
76 
77       PopupNotifications.show(browser, notificationID, messageString, anchorID,
78                               action, null, options);
79       break; }
80     case "addon-install-blocked": {
81       if (!options.originHost) {
82         // Need to deal with missing originatingURI and with about:/data: URIs more gracefully,
83         // see bug 1063418 - but for now, bail:
84         return;
85       }
86       messageString = gNavigatorBundle.getFormattedString("xpinstallPromptMessage",
87                         [brandShortName]);
88 
89       let secHistogram = Components.classes["@mozilla.org/base/telemetry;1"].getService(Ci.nsITelemetry).getHistogramById("SECURITY_UI");
90       action = {
91         label: gNavigatorBundle.getString("xpinstallPromptAllowButton"),
92         accessKey: gNavigatorBundle.getString("xpinstallPromptAllowButton.accesskey"),
93         callback: function() {
94           secHistogram.add(Ci.nsISecurityUITelemetry.WARNING_ADDON_ASKING_PREVENTED_CLICK_THROUGH);
95           installInfo.install();
96         }
97       };
98 
99       secHistogram.add(Ci.nsISecurityUITelemetry.WARNING_ADDON_ASKING_PREVENTED);
100       PopupNotifications.show(browser, notificationID, messageString, anchorID,
101                               action, null, options);
102       break; }
103     case "addon-install-started": {
104       let needsDownload = function needsDownload(aInstall) {
105         return aInstall.state != AddonManager.STATE_DOWNLOADED;
106       }
107       // If all installs have already been downloaded then there is no need to
108       // show the download progress
109       if (!installInfo.installs.some(needsDownload))
110         return;
111       notificationID = "addon-progress";
112       messageString = gNavigatorBundle.getString("addonDownloadingAndVerifying");
113       messageString = PluralForm.get(installInfo.installs.length, messageString);
114       messageString = messageString.replace("#1", installInfo.installs.length);
115       options.installs = installInfo.installs;
116       options.contentWindow = browser.contentWindow;
117       options.sourceURI = browser.currentURI;
118       options.eventCallback = (aEvent) => {
119         switch (aEvent) {
120           case "removed":
121             options.contentWindow = null;
122             options.sourceURI = null;
123             break;
124         }
125       };
126       let notification = PopupNotifications.show(browser, notificationID, messageString,
127                                                  anchorID, null, null, options);
128       notification._startTime = Date.now();
129 
130       let cancelButton = document.getElementById("addon-progress-cancel");
131       cancelButton.label = gNavigatorBundle.getString("addonInstall.cancelButton.label");
132       cancelButton.accessKey = gNavigatorBundle.getString("addonInstall.cancelButton.accesskey");
133 
134       let acceptButton = document.getElementById("addon-progress-accept");
135       acceptButton.label = gNavigatorBundle.getString("addonInstall.acceptButton.label");
136       acceptButton.accessKey = gNavigatorBundle.getString("addonInstall.acceptButton.accesskey");
137       break; }
138     case "addon-install-failed": {
139       // TODO This isn't terribly ideal for the multiple failure case
140       for (let install of installInfo.installs) {
141         let host = options.originHost;
142         if (!host)
143           host = (install.sourceURI instanceof Ci.nsIStandardURL) &&
144                  install.sourceURI.host;
145 
146         let error = (host || install.error == 0) ? "addonError" : "addonLocalError";
147         if (install.error != 0)
148           error += install.error;
149         else if (install.addon.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED)
150           error += "Blocklisted";
151         else
152           error += "Incompatible";
153 
154         messageString = gNavigatorBundle.getString(error);
155         messageString = messageString.replace("#1", install.name);
156         if (host)
157           messageString = messageString.replace("#2", host);
158         messageString = messageString.replace("#3", brandShortName);
159         messageString = messageString.replace("#4", Services.appinfo.version);
160 
161         PopupNotifications.show(browser, notificationID, messageString, anchorID,
162                                 action, null, options);
163       }
164       this._removeProgressNotification(browser);
165       break; }
166     case "addon-install-confirmation": {
167       options.eventCallback = (aEvent) => {
168         switch (aEvent) {
169           case "removed":
170             if (installInfo) {
171               for (let install of installInfo.installs)
172                 install.cancel();
173             }
174             this.acceptInstallation = null;
175             break;
176           case "shown":
177             let addonList = document.getElementById("addon-install-confirmation-content");
178             while (addonList.firstChild)
179               addonList.firstChild.remove();
180 
181             for (let install of installInfo.installs) {
182               let name = document.createElement("label");
183               name.setAttribute("value", install.addon.name);
184               name.setAttribute("class", "addon-install-confirmation-name");
185               addonList.appendChild(name);
186             }
187 
188             this.acceptInstallation = () => {
189               for (let install of installInfo.installs)
190                 install.install();
191               installInfo = null;
192 
193               Services.telemetry
194                       .getHistogramById("SECURITY_UI")
195                       .add(Ci.nsISecurityUITelemetry.WARNING_CONFIRM_ADDON_INSTALL_CLICK_THROUGH);
196             };
197             break;
198         }
199       };
200 
201       messageString = gNavigatorBundle.getString("addonConfirmInstall.message");
202       messageString = PluralForm.get(installInfo.installs.length, messageString);
203       messageString = messageString.replace("#1", brandShortName);
204       messageString = messageString.replace("#2", installInfo.installs.length);
205 
206       let cancelButton = document.getElementById("addon-install-confirmation-cancel");
207       cancelButton.label = gNavigatorBundle.getString("addonInstall.cancelButton.label");
208       cancelButton.accessKey = gNavigatorBundle.getString("addonInstall.cancelButton.accesskey");
209 
210       let acceptButton = document.getElementById("addon-install-confirmation-accept");
211       acceptButton.label = gNavigatorBundle.getString("addonInstall.acceptButton.label");
212       acceptButton.accessKey = gNavigatorBundle.getString("addonInstall.acceptButton.accesskey");
213 
214       let showNotification = () => {
215         let tab = gBrowser.getTabForBrowser(browser);
216         if (tab)
217           gBrowser.selectedTab = tab;
218 
219         if (PopupNotifications.isPanelOpen) {
220           let rect = document.getElementById("addon-progress-notification").getBoundingClientRect();
221           let notification = document.getElementById("addon-install-confirmation-notification");
222           notification.style.minHeight = rect.height + "px";
223         }
224 
225         PopupNotifications.show(browser, notificationID, messageString, anchorID,
226                                 action, null, options);
227 
228         this._removeProgressNotification(browser);
229 
230         Services.telemetry
231                 .getHistogramById("SECURITY_UI")
232                 .add(Ci.nsISecurityUITelemetry.WARNING_CONFIRM_ADDON_INSTALL);
233       };
234 
235       let progressNotification = PopupNotifications.getNotification("addon-progress", browser);
236       if (progressNotification) {
237         let downloadDuration = Date.now() - progressNotification._startTime;
238         let securityDelay = Services.prefs.getIntPref("security.dialog_enable_delay") - downloadDuration;
239         if (securityDelay > 0) {
240           setTimeout(() => {
241             // The download may have been cancelled during the security delay
242             if (PopupNotifications.getNotification("addon-progress", browser))
243               showNotification();
244           }, securityDelay);
245           break;
246         }
247       }
248       showNotification();
249       break; }
250     case "addon-install-complete": {
251       let needsRestart = installInfo.installs.some(function(i) {
252         return i.addon.pendingOperations != AddonManager.PENDING_NONE;
253       });
254 
255       if (needsRestart) {
256         messageString = gNavigatorBundle.getString("addonsInstalledNeedsRestart");
257         action = {
258           label: gNavigatorBundle.getString("addonInstallRestartButton"),
259           accessKey: gNavigatorBundle.getString("addonInstallRestartButton.accesskey"),
260           callback: function() {
261             Application.restart();
262           }
263         };
264       }
265       else {
266         messageString = gNavigatorBundle.getString("addonsInstalled");
267         action = null;
268       }
269 
270       messageString = PluralForm.get(installInfo.installs.length, messageString);
271       messageString = messageString.replace("#1", installInfo.installs[0].name);
272       messageString = messageString.replace("#2", installInfo.installs.length);
273       messageString = messageString.replace("#3", brandShortName);
274 
275       // Remove notificaion on dismissal, since it's possible to cancel the
276       // install through the addons manager UI, making the "restart" prompt
277       // irrelevant.
278       options.removeOnDismissal = true;
279 
280       PopupNotifications.show(browser, notificationID, messageString, anchorID,
281                               action, null, options);
282       break; }
283     }
284   },
285   _removeProgressNotification(aBrowser) {
286     let notification = PopupNotifications.getNotification("addon-progress", aBrowser);
287     if (notification)
288       notification.remove();
289   }
290 };
291 
292 var LightWeightThemeWebInstaller = {
293   handleEvent: function (event) {
294     switch (event.type) {
295       case "InstallBrowserTheme":
296       case "PreviewBrowserTheme":
297       case "ResetBrowserThemePreview":
298         // ignore requests from background tabs
299         if (event.target.ownerDocument.defaultView.top != content)
300           return;
301     }
302     switch (event.type) {
303       case "InstallBrowserTheme":
304         this._installRequest(event);
305         break;
306       case "PreviewBrowserTheme":
307         this._preview(event);
308         break;
309       case "ResetBrowserThemePreview":
310         this._resetPreview(event);
311         break;
312       case "pagehide":
313       case "TabSelect":
314         this._resetPreview();
315         break;
316     }
317   },
318 
319   get _manager () {
320     var temp = {};
321     Cu.import("resource://gre/modules/LightweightThemeManager.jsm", temp);
322     delete this._manager;
323     return this._manager = temp.LightweightThemeManager;
324   },
325 
326   _installRequest: function (event) {
327     var node = event.target;
328     var data = this._getThemeFromNode(node);
329     if (!data)
330       return;
331 
332     if (this._isAllowed(node)) {
333       this._install(data);
334       return;
335     }
336 
337     var allowButtonText =
338       gNavigatorBundle.getString("lwthemeInstallRequest.allowButton");
339     var allowButtonAccesskey =
340       gNavigatorBundle.getString("lwthemeInstallRequest.allowButton.accesskey");
341     var message =
342       gNavigatorBundle.getFormattedString("lwthemeInstallRequest.message",
343                                           [node.ownerDocument.location.host]);
344     var buttons = [{
345       label: allowButtonText,
346       accessKey: allowButtonAccesskey,
347       callback: function () {
348         LightWeightThemeWebInstaller._install(data);
349       }
350     }];
351 
352     this._removePreviousNotifications();
353 
354     var notificationBox = gBrowser.getNotificationBox();
355     var notificationBar =
356       notificationBox.appendNotification(message, "lwtheme-install-request", "",
357                                          notificationBox.PRIORITY_INFO_MEDIUM,
358                                          buttons);
359     notificationBar.persistence = 1;
360   },
361 
362   _install: function (newLWTheme) {
363     var previousLWTheme = this._manager.currentTheme;
364 
365     var listener = {
366       onEnabling: function(aAddon, aRequiresRestart) {
367         if (!aRequiresRestart)
368           return;
369 
370         let messageString = gNavigatorBundle.getFormattedString("lwthemeNeedsRestart.message",
371           [aAddon.name], 1);
372 
373         let action = {
374           label: gNavigatorBundle.getString("lwthemeNeedsRestart.button"),
375           accessKey: gNavigatorBundle.getString("lwthemeNeedsRestart.accesskey"),
376           callback: function () {
377             Application.restart();
378           }
379         };
380 
381         let options = {
382           timeout: Date.now() + 30000
383         };
384 
385         PopupNotifications.show(gBrowser.selectedBrowser, "addon-theme-change",
386                                 messageString, "addons-notification-icon",
387                                 action, null, options);
388       },
389 
390       onEnabled: function(aAddon) {
391         LightWeightThemeWebInstaller._postInstallNotification(newLWTheme, previousLWTheme);
392       }
393     };
394 
395     AddonManager.addAddonListener(listener);
396     this._manager.currentTheme = newLWTheme;
397     AddonManager.removeAddonListener(listener);
398   },
399 
400   _postInstallNotification: function (newTheme, previousTheme) {
401     function text(id) {
402       return gNavigatorBundle.getString("lwthemePostInstallNotification." + id);
403     }
404 
405     var buttons = [{
406       label: text("undoButton"),
407       accessKey: text("undoButton.accesskey"),
408       callback: function () {
409         LightWeightThemeWebInstaller._manager.forgetUsedTheme(newTheme.id);
410         LightWeightThemeWebInstaller._manager.currentTheme = previousTheme;
411       }
412     }, {
413       label: text("manageButton"),
414       accessKey: text("manageButton.accesskey"),
415       callback: function () {
416         BrowserOpenAddonsMgr("addons://list/theme");
417       }
418     }];
419 
420     this._removePreviousNotifications();
421 
422     var notificationBox = gBrowser.getNotificationBox();
423     var notificationBar =
424       notificationBox.appendNotification(text("message"),
425                                          "lwtheme-install-notification", "",
426                                          notificationBox.PRIORITY_INFO_MEDIUM,
427                                          buttons);
428     notificationBar.persistence = 1;
429     notificationBar.timeout = Date.now() + 20000; // 20 seconds
430   },
431 
432   _removePreviousNotifications: function () {
433     var box = gBrowser.getNotificationBox();
434 
435     ["lwtheme-install-request",
436      "lwtheme-install-notification"].forEach(function (value) {
437         var notification = box.getNotificationWithValue(value);
438         if (notification)
439           box.removeNotification(notification);
440       });
441   },
442 
443   _previewWindow: null,
444   _preview: function (event) {
445     if (!this._isAllowed(event.target))
446       return;
447 
448     var data = this._getThemeFromNode(event.target);
449     if (!data)
450       return;
451 
452     this._resetPreview();
453 
454     this._previewWindow = event.target.ownerDocument.defaultView;
455     this._previewWindow.addEventListener("pagehide", this, true);
456     gBrowser.tabContainer.addEventListener("TabSelect", this, false);
457 
458     this._manager.previewTheme(data);
459   },
460 
461   _resetPreview: function (event) {
462     if (!this._previewWindow ||
463         event && !this._isAllowed(event.target))
464       return;
465 
466     this._previewWindow.removeEventListener("pagehide", this, true);
467     this._previewWindow = null;
468     gBrowser.tabContainer.removeEventListener("TabSelect", this, false);
469 
470     this._manager.resetPreview();
471   },
472 
473   _isAllowed: function (node) {
474     var pm = Services.perms;
475 
476     var uri = node.ownerDocument.documentURIObject;
477 
478     if (!uri.schemeIs("https"))
479       return false;
480 
481     return pm.testPermission(uri, "install") == pm.ALLOW_ACTION;
482   },
483 
484   _getThemeFromNode: function (node) {
485     return this._manager.parseTheme(node.getAttribute("data-browsertheme"),
486                                     node.baseURI);
487   }
488 }
489 
490 /*
491  * Listen for Lightweight Theme styling changes and update the browser's theme accordingly.
492  */
493 let LightweightThemeListener = {
494   _modifiedStyles: [],
495 
496   init: function () {
497     XPCOMUtils.defineLazyGetter(this, "styleSheet", function() {
498       for (let i = document.styleSheets.length - 1; i >= 0; i--) {
499         let sheet = document.styleSheets[i];
500         if (sheet.href == "chrome://browser/skin/browser-lightweightTheme.css")
501           return sheet;
502       }
503     });
504 
505     Services.obs.addObserver(this, "lightweight-theme-styling-update", false);
506     Services.obs.addObserver(this, "lightweight-theme-optimized", false);
507     if (document.documentElement.hasAttribute("lwtheme"))
508       this.updateStyleSheet(document.documentElement.style.backgroundImage);
509   },
510 
511   uninit: function () {
512     Services.obs.removeObserver(this, "lightweight-theme-styling-update");
513     Services.obs.removeObserver(this, "lightweight-theme-optimized");
514   },
515 
516   /**
517    * Append the headerImage to the background-image property of all rulesets in
518    * browser-lightweightTheme.css.
519    *
520    * @param headerImage - a string containing a CSS image for the lightweight theme header.
521    */
522   updateStyleSheet: function(headerImage) {
523     if (!this.styleSheet)
524       return;
525     this.substituteRules(this.styleSheet.cssRules, headerImage);
526   },
527 
528   substituteRules: function(ruleList, headerImage, existingStyleRulesModified = 0) {
529     let styleRulesModified = 0;
530     for (let i = 0; i < ruleList.length; i++) {
531       let rule = ruleList[i];
532       if (rule instanceof Ci.nsIDOMCSSGroupingRule) {
533         // Add the number of modified sub-rules to the modified count
534         styleRulesModified += this.substituteRules(rule.cssRules, headerImage, existingStyleRulesModified + styleRulesModified);
535       } else if (rule instanceof Ci.nsIDOMCSSStyleRule) {
536         if (!rule.style.backgroundImage)
537           continue;
538         let modifiedIndex = existingStyleRulesModified + styleRulesModified;
539         if (!this._modifiedStyles[modifiedIndex])
540           this._modifiedStyles[modifiedIndex] = { backgroundImage: rule.style.backgroundImage };
541 
542         rule.style.backgroundImage = this._modifiedStyles[modifiedIndex].backgroundImage + ", " + headerImage;
543         styleRulesModified++;
544       } else {
545         Cu.reportError("Unsupported rule encountered");
546       }
547     }
548     return styleRulesModified;
549   },
550 
551   // nsIObserver
552   observe: function (aSubject, aTopic, aData) {
553     if ((aTopic != "lightweight-theme-styling-update" && aTopic != "lightweight-theme-optimized") ||
554           !this.styleSheet)
555       return;
556 
557     if (aTopic == "lightweight-theme-optimized" && aSubject != window)
558       return;
559 
560     let themeData = JSON.parse(aData);
561     if (!themeData)
562       return;
563     this.updateStyleSheet("url(" + themeData.headerURL + ")");
564   },
565 };
566 
view http://hg.mozilla.org/mozilla-central/rev/ /browser/base/content/browser-addons.js