|
1 # -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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 win = installInfo.originatingWindow; |
|
36 var shell = win.QueryInterface(Components.interfaces.nsIInterfaceRequestor) |
|
37 .getInterface(Components.interfaces.nsIWebNavigation) |
|
38 .QueryInterface(Components.interfaces.nsIDocShell); |
|
39 var browser = this._getBrowser(shell); |
|
40 if (!browser) |
|
41 return; |
|
42 const anchorID = "addons-notification-icon"; |
|
43 var messageString, action; |
|
44 var brandShortName = brandBundle.getString("brandShortName"); |
|
45 |
|
46 var notificationID = aTopic; |
|
47 // Make notifications persist a minimum of 30 seconds |
|
48 var options = { |
|
49 timeout: Date.now() + 30000 |
|
50 }; |
|
51 |
|
52 switch (aTopic) { |
|
53 case "addon-install-disabled": |
|
54 notificationID = "xpinstall-disabled" |
|
55 |
|
56 if (gPrefService.prefIsLocked("xpinstall.enabled")) { |
|
57 messageString = gNavigatorBundle.getString("xpinstallDisabledMessageLocked"); |
|
58 buttons = []; |
|
59 } |
|
60 else { |
|
61 messageString = gNavigatorBundle.getString("xpinstallDisabledMessage"); |
|
62 |
|
63 action = { |
|
64 label: gNavigatorBundle.getString("xpinstallDisabledButton"), |
|
65 accessKey: gNavigatorBundle.getString("xpinstallDisabledButton.accesskey"), |
|
66 callback: function editPrefs() { |
|
67 gPrefService.setBoolPref("xpinstall.enabled", true); |
|
68 } |
|
69 }; |
|
70 } |
|
71 |
|
72 PopupNotifications.show(browser, notificationID, messageString, anchorID, |
|
73 action, null, options); |
|
74 break; |
|
75 case "addon-install-blocked": |
|
76 messageString = gNavigatorBundle.getFormattedString("xpinstallPromptWarning", |
|
77 [brandShortName, installInfo.originatingURI.host]); |
|
78 |
|
79 let secHistogram = Components.classes["@mozilla.org/base/telemetry;1"].getService(Ci.nsITelemetry).getHistogramById("SECURITY_UI"); |
|
80 action = { |
|
81 label: gNavigatorBundle.getString("xpinstallPromptAllowButton"), |
|
82 accessKey: gNavigatorBundle.getString("xpinstallPromptAllowButton.accesskey"), |
|
83 callback: function() { |
|
84 secHistogram.add(Ci.nsISecurityUITelemetry.WARNING_ADDON_ASKING_PREVENTED_CLICK_THROUGH); |
|
85 installInfo.install(); |
|
86 } |
|
87 }; |
|
88 |
|
89 secHistogram.add(Ci.nsISecurityUITelemetry.WARNING_ADDON_ASKING_PREVENTED); |
|
90 PopupNotifications.show(browser, notificationID, messageString, anchorID, |
|
91 action, null, options); |
|
92 break; |
|
93 case "addon-install-started": |
|
94 var needsDownload = function needsDownload(aInstall) { |
|
95 return aInstall.state != AddonManager.STATE_DOWNLOADED; |
|
96 } |
|
97 // If all installs have already been downloaded then there is no need to |
|
98 // show the download progress |
|
99 if (!installInfo.installs.some(needsDownload)) |
|
100 return; |
|
101 notificationID = "addon-progress"; |
|
102 messageString = gNavigatorBundle.getString("addonDownloading"); |
|
103 messageString = PluralForm.get(installInfo.installs.length, messageString); |
|
104 options.installs = installInfo.installs; |
|
105 options.contentWindow = browser.contentWindow; |
|
106 options.sourceURI = browser.currentURI; |
|
107 options.eventCallback = function(aEvent) { |
|
108 if (aEvent != "removed") |
|
109 return; |
|
110 options.contentWindow = null; |
|
111 options.sourceURI = null; |
|
112 }; |
|
113 PopupNotifications.show(browser, notificationID, messageString, anchorID, |
|
114 null, null, options); |
|
115 break; |
|
116 case "addon-install-failed": |
|
117 // TODO This isn't terribly ideal for the multiple failure case |
|
118 for (let install of installInfo.installs) { |
|
119 let host = (installInfo.originatingURI instanceof Ci.nsIStandardURL) && |
|
120 installInfo.originatingURI.host; |
|
121 if (!host) |
|
122 host = (install.sourceURI instanceof Ci.nsIStandardURL) && |
|
123 install.sourceURI.host; |
|
124 |
|
125 let error = (host || install.error == 0) ? "addonError" : "addonLocalError"; |
|
126 if (install.error != 0) |
|
127 error += install.error; |
|
128 else if (install.addon.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED) |
|
129 error += "Blocklisted"; |
|
130 else |
|
131 error += "Incompatible"; |
|
132 |
|
133 messageString = gNavigatorBundle.getString(error); |
|
134 messageString = messageString.replace("#1", install.name); |
|
135 if (host) |
|
136 messageString = messageString.replace("#2", host); |
|
137 messageString = messageString.replace("#3", brandShortName); |
|
138 messageString = messageString.replace("#4", Services.appinfo.version); |
|
139 |
|
140 PopupNotifications.show(browser, notificationID, messageString, anchorID, |
|
141 action, null, options); |
|
142 } |
|
143 break; |
|
144 case "addon-install-complete": |
|
145 var needsRestart = installInfo.installs.some(function(i) { |
|
146 return i.addon.pendingOperations != AddonManager.PENDING_NONE; |
|
147 }); |
|
148 |
|
149 if (needsRestart) { |
|
150 messageString = gNavigatorBundle.getString("addonsInstalledNeedsRestart"); |
|
151 action = { |
|
152 label: gNavigatorBundle.getString("addonInstallRestartButton"), |
|
153 accessKey: gNavigatorBundle.getString("addonInstallRestartButton.accesskey"), |
|
154 callback: function() { |
|
155 Application.restart(); |
|
156 } |
|
157 }; |
|
158 } |
|
159 else { |
|
160 messageString = gNavigatorBundle.getString("addonsInstalled"); |
|
161 action = null; |
|
162 } |
|
163 |
|
164 messageString = PluralForm.get(installInfo.installs.length, messageString); |
|
165 messageString = messageString.replace("#1", installInfo.installs[0].name); |
|
166 messageString = messageString.replace("#2", installInfo.installs.length); |
|
167 messageString = messageString.replace("#3", brandShortName); |
|
168 |
|
169 // Remove notificaion on dismissal, since it's possible to cancel the |
|
170 // install through the addons manager UI, making the "restart" prompt |
|
171 // irrelevant. |
|
172 options.removeOnDismissal = true; |
|
173 |
|
174 PopupNotifications.show(browser, notificationID, messageString, anchorID, |
|
175 action, null, options); |
|
176 break; |
|
177 } |
|
178 } |
|
179 }; |
|
180 |
|
181 var LightWeightThemeWebInstaller = { |
|
182 handleEvent: function (event) { |
|
183 switch (event.type) { |
|
184 case "InstallBrowserTheme": |
|
185 case "PreviewBrowserTheme": |
|
186 case "ResetBrowserThemePreview": |
|
187 // ignore requests from background tabs |
|
188 if (event.target.ownerDocument.defaultView.top != content) |
|
189 return; |
|
190 } |
|
191 switch (event.type) { |
|
192 case "InstallBrowserTheme": |
|
193 this._installRequest(event); |
|
194 break; |
|
195 case "PreviewBrowserTheme": |
|
196 this._preview(event); |
|
197 break; |
|
198 case "ResetBrowserThemePreview": |
|
199 this._resetPreview(event); |
|
200 break; |
|
201 case "pagehide": |
|
202 case "TabSelect": |
|
203 this._resetPreview(); |
|
204 break; |
|
205 } |
|
206 }, |
|
207 |
|
208 get _manager () { |
|
209 var temp = {}; |
|
210 Cu.import("resource://gre/modules/LightweightThemeManager.jsm", temp); |
|
211 delete this._manager; |
|
212 return this._manager = temp.LightweightThemeManager; |
|
213 }, |
|
214 |
|
215 _installRequest: function (event) { |
|
216 var node = event.target; |
|
217 var data = this._getThemeFromNode(node); |
|
218 if (!data) |
|
219 return; |
|
220 |
|
221 if (this._isAllowed(node)) { |
|
222 this._install(data); |
|
223 return; |
|
224 } |
|
225 |
|
226 var allowButtonText = |
|
227 gNavigatorBundle.getString("lwthemeInstallRequest.allowButton"); |
|
228 var allowButtonAccesskey = |
|
229 gNavigatorBundle.getString("lwthemeInstallRequest.allowButton.accesskey"); |
|
230 var message = |
|
231 gNavigatorBundle.getFormattedString("lwthemeInstallRequest.message", |
|
232 [node.ownerDocument.location.host]); |
|
233 var buttons = [{ |
|
234 label: allowButtonText, |
|
235 accessKey: allowButtonAccesskey, |
|
236 callback: function () { |
|
237 LightWeightThemeWebInstaller._install(data); |
|
238 } |
|
239 }]; |
|
240 |
|
241 this._removePreviousNotifications(); |
|
242 |
|
243 var notificationBox = gBrowser.getNotificationBox(); |
|
244 var notificationBar = |
|
245 notificationBox.appendNotification(message, "lwtheme-install-request", "", |
|
246 notificationBox.PRIORITY_INFO_MEDIUM, |
|
247 buttons); |
|
248 notificationBar.persistence = 1; |
|
249 }, |
|
250 |
|
251 _install: function (newLWTheme) { |
|
252 var previousLWTheme = this._manager.currentTheme; |
|
253 |
|
254 var listener = { |
|
255 onEnabling: function(aAddon, aRequiresRestart) { |
|
256 if (!aRequiresRestart) |
|
257 return; |
|
258 |
|
259 let messageString = gNavigatorBundle.getFormattedString("lwthemeNeedsRestart.message", |
|
260 [aAddon.name], 1); |
|
261 |
|
262 let action = { |
|
263 label: gNavigatorBundle.getString("lwthemeNeedsRestart.button"), |
|
264 accessKey: gNavigatorBundle.getString("lwthemeNeedsRestart.accesskey"), |
|
265 callback: function () { |
|
266 Application.restart(); |
|
267 } |
|
268 }; |
|
269 |
|
270 let options = { |
|
271 timeout: Date.now() + 30000 |
|
272 }; |
|
273 |
|
274 PopupNotifications.show(gBrowser.selectedBrowser, "addon-theme-change", |
|
275 messageString, "addons-notification-icon", |
|
276 action, null, options); |
|
277 }, |
|
278 |
|
279 onEnabled: function(aAddon) { |
|
280 LightWeightThemeWebInstaller._postInstallNotification(newLWTheme, previousLWTheme); |
|
281 } |
|
282 }; |
|
283 |
|
284 AddonManager.addAddonListener(listener); |
|
285 this._manager.currentTheme = newLWTheme; |
|
286 AddonManager.removeAddonListener(listener); |
|
287 }, |
|
288 |
|
289 _postInstallNotification: function (newTheme, previousTheme) { |
|
290 function text(id) { |
|
291 return gNavigatorBundle.getString("lwthemePostInstallNotification." + id); |
|
292 } |
|
293 |
|
294 var buttons = [{ |
|
295 label: text("undoButton"), |
|
296 accessKey: text("undoButton.accesskey"), |
|
297 callback: function () { |
|
298 LightWeightThemeWebInstaller._manager.forgetUsedTheme(newTheme.id); |
|
299 LightWeightThemeWebInstaller._manager.currentTheme = previousTheme; |
|
300 } |
|
301 }, { |
|
302 label: text("manageButton"), |
|
303 accessKey: text("manageButton.accesskey"), |
|
304 callback: function () { |
|
305 BrowserOpenAddonsMgr("addons://list/theme"); |
|
306 } |
|
307 }]; |
|
308 |
|
309 this._removePreviousNotifications(); |
|
310 |
|
311 var notificationBox = gBrowser.getNotificationBox(); |
|
312 var notificationBar = |
|
313 notificationBox.appendNotification(text("message"), |
|
314 "lwtheme-install-notification", "", |
|
315 notificationBox.PRIORITY_INFO_MEDIUM, |
|
316 buttons); |
|
317 notificationBar.persistence = 1; |
|
318 notificationBar.timeout = Date.now() + 20000; // 20 seconds |
|
319 }, |
|
320 |
|
321 _removePreviousNotifications: function () { |
|
322 var box = gBrowser.getNotificationBox(); |
|
323 |
|
324 ["lwtheme-install-request", |
|
325 "lwtheme-install-notification"].forEach(function (value) { |
|
326 var notification = box.getNotificationWithValue(value); |
|
327 if (notification) |
|
328 box.removeNotification(notification); |
|
329 }); |
|
330 }, |
|
331 |
|
332 _previewWindow: null, |
|
333 _preview: function (event) { |
|
334 if (!this._isAllowed(event.target)) |
|
335 return; |
|
336 |
|
337 var data = this._getThemeFromNode(event.target); |
|
338 if (!data) |
|
339 return; |
|
340 |
|
341 this._resetPreview(); |
|
342 |
|
343 this._previewWindow = event.target.ownerDocument.defaultView; |
|
344 this._previewWindow.addEventListener("pagehide", this, true); |
|
345 gBrowser.tabContainer.addEventListener("TabSelect", this, false); |
|
346 |
|
347 this._manager.previewTheme(data); |
|
348 }, |
|
349 |
|
350 _resetPreview: function (event) { |
|
351 if (!this._previewWindow || |
|
352 event && !this._isAllowed(event.target)) |
|
353 return; |
|
354 |
|
355 this._previewWindow.removeEventListener("pagehide", this, true); |
|
356 this._previewWindow = null; |
|
357 gBrowser.tabContainer.removeEventListener("TabSelect", this, false); |
|
358 |
|
359 this._manager.resetPreview(); |
|
360 }, |
|
361 |
|
362 _isAllowed: function (node) { |
|
363 var pm = Services.perms; |
|
364 |
|
365 var uri = node.ownerDocument.documentURIObject; |
|
366 return pm.testPermission(uri, "install") == pm.ALLOW_ACTION; |
|
367 }, |
|
368 |
|
369 _getThemeFromNode: function (node) { |
|
370 return this._manager.parseTheme(node.getAttribute("data-browsertheme"), |
|
371 node.baseURI); |
|
372 } |
|
373 } |
|
374 |
|
375 /* |
|
376 * Listen for Lightweight Theme styling changes and update the browser's theme accordingly. |
|
377 */ |
|
378 let LightweightThemeListener = { |
|
379 _modifiedStyles: [], |
|
380 |
|
381 init: function () { |
|
382 XPCOMUtils.defineLazyGetter(this, "styleSheet", function() { |
|
383 for (let i = document.styleSheets.length - 1; i >= 0; i--) { |
|
384 let sheet = document.styleSheets[i]; |
|
385 if (sheet.href == "chrome://browser/skin/browser-lightweightTheme.css") |
|
386 return sheet; |
|
387 } |
|
388 }); |
|
389 |
|
390 Services.obs.addObserver(this, "lightweight-theme-styling-update", false); |
|
391 Services.obs.addObserver(this, "lightweight-theme-optimized", false); |
|
392 if (document.documentElement.hasAttribute("lwtheme")) |
|
393 this.updateStyleSheet(document.documentElement.style.backgroundImage); |
|
394 }, |
|
395 |
|
396 uninit: function () { |
|
397 Services.obs.removeObserver(this, "lightweight-theme-styling-update"); |
|
398 Services.obs.removeObserver(this, "lightweight-theme-optimized"); |
|
399 }, |
|
400 |
|
401 /** |
|
402 * Append the headerImage to the background-image property of all rulesets in |
|
403 * browser-lightweightTheme.css. |
|
404 * |
|
405 * @param headerImage - a string containing a CSS image for the lightweight theme header. |
|
406 */ |
|
407 updateStyleSheet: function(headerImage) { |
|
408 if (!this.styleSheet) |
|
409 return; |
|
410 this.substituteRules(this.styleSheet.cssRules, headerImage); |
|
411 }, |
|
412 |
|
413 substituteRules: function(ruleList, headerImage, existingStyleRulesModified = 0) { |
|
414 let styleRulesModified = 0; |
|
415 for (let i = 0; i < ruleList.length; i++) { |
|
416 let rule = ruleList[i]; |
|
417 if (rule instanceof Ci.nsIDOMCSSGroupingRule) { |
|
418 // Add the number of modified sub-rules to the modified count |
|
419 styleRulesModified += this.substituteRules(rule.cssRules, headerImage, existingStyleRulesModified + styleRulesModified); |
|
420 } else if (rule instanceof Ci.nsIDOMCSSStyleRule) { |
|
421 if (!rule.style.backgroundImage) |
|
422 continue; |
|
423 let modifiedIndex = existingStyleRulesModified + styleRulesModified; |
|
424 if (!this._modifiedStyles[modifiedIndex]) |
|
425 this._modifiedStyles[modifiedIndex] = { backgroundImage: rule.style.backgroundImage }; |
|
426 |
|
427 rule.style.backgroundImage = this._modifiedStyles[modifiedIndex].backgroundImage + ", " + headerImage; |
|
428 styleRulesModified++; |
|
429 } else { |
|
430 Cu.reportError("Unsupported rule encountered"); |
|
431 } |
|
432 } |
|
433 return styleRulesModified; |
|
434 }, |
|
435 |
|
436 // nsIObserver |
|
437 observe: function (aSubject, aTopic, aData) { |
|
438 if ((aTopic != "lightweight-theme-styling-update" && aTopic != "lightweight-theme-optimized") || |
|
439 !this.styleSheet) |
|
440 return; |
|
441 |
|
442 if (aTopic == "lightweight-theme-optimized" && aSubject != window) |
|
443 return; |
|
444 |
|
445 let themeData = JSON.parse(aData); |
|
446 if (!themeData) |
|
447 return; |
|
448 this.updateStyleSheet("url(" + themeData.headerURL + ")"); |
|
449 }, |
|
450 }; |