 |
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