michael@0: # -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- michael@0: # This Source Code Form is subject to the terms of the Mozilla Public michael@0: # License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: # file, You can obtain one at http://mozilla.org/MPL/2.0/. michael@0: michael@0: const gXPInstallObserver = { michael@0: _findChildShell: function (aDocShell, aSoughtShell) michael@0: { michael@0: if (aDocShell == aSoughtShell) michael@0: return aDocShell; michael@0: michael@0: var node = aDocShell.QueryInterface(Components.interfaces.nsIDocShellTreeItem); michael@0: for (var i = 0; i < node.childCount; ++i) { michael@0: var docShell = node.getChildAt(i); michael@0: docShell = this._findChildShell(docShell, aSoughtShell); michael@0: if (docShell == aSoughtShell) michael@0: return docShell; michael@0: } michael@0: return null; michael@0: }, michael@0: michael@0: _getBrowser: function (aDocShell) michael@0: { michael@0: for (let browser of gBrowser.browsers) { michael@0: if (this._findChildShell(browser.docShell, aDocShell)) michael@0: return browser; michael@0: } michael@0: return null; michael@0: }, michael@0: michael@0: observe: function (aSubject, aTopic, aData) michael@0: { michael@0: var brandBundle = document.getElementById("bundle_brand"); michael@0: var installInfo = aSubject.QueryInterface(Components.interfaces.amIWebInstallInfo); michael@0: var win = installInfo.originatingWindow; michael@0: var shell = win.QueryInterface(Components.interfaces.nsIInterfaceRequestor) michael@0: .getInterface(Components.interfaces.nsIWebNavigation) michael@0: .QueryInterface(Components.interfaces.nsIDocShell); michael@0: var browser = this._getBrowser(shell); michael@0: if (!browser) michael@0: return; michael@0: const anchorID = "addons-notification-icon"; michael@0: var messageString, action; michael@0: var brandShortName = brandBundle.getString("brandShortName"); michael@0: michael@0: var notificationID = aTopic; michael@0: // Make notifications persist a minimum of 30 seconds michael@0: var options = { michael@0: timeout: Date.now() + 30000 michael@0: }; michael@0: michael@0: switch (aTopic) { michael@0: case "addon-install-disabled": michael@0: notificationID = "xpinstall-disabled" michael@0: michael@0: if (gPrefService.prefIsLocked("xpinstall.enabled")) { michael@0: messageString = gNavigatorBundle.getString("xpinstallDisabledMessageLocked"); michael@0: buttons = []; michael@0: } michael@0: else { michael@0: messageString = gNavigatorBundle.getString("xpinstallDisabledMessage"); michael@0: michael@0: action = { michael@0: label: gNavigatorBundle.getString("xpinstallDisabledButton"), michael@0: accessKey: gNavigatorBundle.getString("xpinstallDisabledButton.accesskey"), michael@0: callback: function editPrefs() { michael@0: gPrefService.setBoolPref("xpinstall.enabled", true); michael@0: } michael@0: }; michael@0: } michael@0: michael@0: PopupNotifications.show(browser, notificationID, messageString, anchorID, michael@0: action, null, options); michael@0: break; michael@0: case "addon-install-blocked": michael@0: messageString = gNavigatorBundle.getFormattedString("xpinstallPromptWarning", michael@0: [brandShortName, installInfo.originatingURI.host]); michael@0: michael@0: let secHistogram = Components.classes["@mozilla.org/base/telemetry;1"].getService(Ci.nsITelemetry).getHistogramById("SECURITY_UI"); michael@0: action = { michael@0: label: gNavigatorBundle.getString("xpinstallPromptAllowButton"), michael@0: accessKey: gNavigatorBundle.getString("xpinstallPromptAllowButton.accesskey"), michael@0: callback: function() { michael@0: secHistogram.add(Ci.nsISecurityUITelemetry.WARNING_ADDON_ASKING_PREVENTED_CLICK_THROUGH); michael@0: installInfo.install(); michael@0: } michael@0: }; michael@0: michael@0: secHistogram.add(Ci.nsISecurityUITelemetry.WARNING_ADDON_ASKING_PREVENTED); michael@0: PopupNotifications.show(browser, notificationID, messageString, anchorID, michael@0: action, null, options); michael@0: break; michael@0: case "addon-install-started": michael@0: var needsDownload = function needsDownload(aInstall) { michael@0: return aInstall.state != AddonManager.STATE_DOWNLOADED; michael@0: } michael@0: // If all installs have already been downloaded then there is no need to michael@0: // show the download progress michael@0: if (!installInfo.installs.some(needsDownload)) michael@0: return; michael@0: notificationID = "addon-progress"; michael@0: messageString = gNavigatorBundle.getString("addonDownloading"); michael@0: messageString = PluralForm.get(installInfo.installs.length, messageString); michael@0: options.installs = installInfo.installs; michael@0: options.contentWindow = browser.contentWindow; michael@0: options.sourceURI = browser.currentURI; michael@0: options.eventCallback = function(aEvent) { michael@0: if (aEvent != "removed") michael@0: return; michael@0: options.contentWindow = null; michael@0: options.sourceURI = null; michael@0: }; michael@0: PopupNotifications.show(browser, notificationID, messageString, anchorID, michael@0: null, null, options); michael@0: break; michael@0: case "addon-install-failed": michael@0: // TODO This isn't terribly ideal for the multiple failure case michael@0: for (let install of installInfo.installs) { michael@0: let host = (installInfo.originatingURI instanceof Ci.nsIStandardURL) && michael@0: installInfo.originatingURI.host; michael@0: if (!host) michael@0: host = (install.sourceURI instanceof Ci.nsIStandardURL) && michael@0: install.sourceURI.host; michael@0: michael@0: let error = (host || install.error == 0) ? "addonError" : "addonLocalError"; michael@0: if (install.error != 0) michael@0: error += install.error; michael@0: else if (install.addon.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED) michael@0: error += "Blocklisted"; michael@0: else michael@0: error += "Incompatible"; michael@0: michael@0: messageString = gNavigatorBundle.getString(error); michael@0: messageString = messageString.replace("#1", install.name); michael@0: if (host) michael@0: messageString = messageString.replace("#2", host); michael@0: messageString = messageString.replace("#3", brandShortName); michael@0: messageString = messageString.replace("#4", Services.appinfo.version); michael@0: michael@0: PopupNotifications.show(browser, notificationID, messageString, anchorID, michael@0: action, null, options); michael@0: } michael@0: break; michael@0: case "addon-install-complete": michael@0: var needsRestart = installInfo.installs.some(function(i) { michael@0: return i.addon.pendingOperations != AddonManager.PENDING_NONE; michael@0: }); michael@0: michael@0: if (needsRestart) { michael@0: messageString = gNavigatorBundle.getString("addonsInstalledNeedsRestart"); michael@0: action = { michael@0: label: gNavigatorBundle.getString("addonInstallRestartButton"), michael@0: accessKey: gNavigatorBundle.getString("addonInstallRestartButton.accesskey"), michael@0: callback: function() { michael@0: Application.restart(); michael@0: } michael@0: }; michael@0: } michael@0: else { michael@0: messageString = gNavigatorBundle.getString("addonsInstalled"); michael@0: action = null; michael@0: } michael@0: michael@0: messageString = PluralForm.get(installInfo.installs.length, messageString); michael@0: messageString = messageString.replace("#1", installInfo.installs[0].name); michael@0: messageString = messageString.replace("#2", installInfo.installs.length); michael@0: messageString = messageString.replace("#3", brandShortName); michael@0: michael@0: // Remove notificaion on dismissal, since it's possible to cancel the michael@0: // install through the addons manager UI, making the "restart" prompt michael@0: // irrelevant. michael@0: options.removeOnDismissal = true; michael@0: michael@0: PopupNotifications.show(browser, notificationID, messageString, anchorID, michael@0: action, null, options); michael@0: break; michael@0: } michael@0: } michael@0: }; michael@0: michael@0: var LightWeightThemeWebInstaller = { michael@0: handleEvent: function (event) { michael@0: switch (event.type) { michael@0: case "InstallBrowserTheme": michael@0: case "PreviewBrowserTheme": michael@0: case "ResetBrowserThemePreview": michael@0: // ignore requests from background tabs michael@0: if (event.target.ownerDocument.defaultView.top != content) michael@0: return; michael@0: } michael@0: switch (event.type) { michael@0: case "InstallBrowserTheme": michael@0: this._installRequest(event); michael@0: break; michael@0: case "PreviewBrowserTheme": michael@0: this._preview(event); michael@0: break; michael@0: case "ResetBrowserThemePreview": michael@0: this._resetPreview(event); michael@0: break; michael@0: case "pagehide": michael@0: case "TabSelect": michael@0: this._resetPreview(); michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: get _manager () { michael@0: var temp = {}; michael@0: Cu.import("resource://gre/modules/LightweightThemeManager.jsm", temp); michael@0: delete this._manager; michael@0: return this._manager = temp.LightweightThemeManager; michael@0: }, michael@0: michael@0: _installRequest: function (event) { michael@0: var node = event.target; michael@0: var data = this._getThemeFromNode(node); michael@0: if (!data) michael@0: return; michael@0: michael@0: if (this._isAllowed(node)) { michael@0: this._install(data); michael@0: return; michael@0: } michael@0: michael@0: var allowButtonText = michael@0: gNavigatorBundle.getString("lwthemeInstallRequest.allowButton"); michael@0: var allowButtonAccesskey = michael@0: gNavigatorBundle.getString("lwthemeInstallRequest.allowButton.accesskey"); michael@0: var message = michael@0: gNavigatorBundle.getFormattedString("lwthemeInstallRequest.message", michael@0: [node.ownerDocument.location.host]); michael@0: var buttons = [{ michael@0: label: allowButtonText, michael@0: accessKey: allowButtonAccesskey, michael@0: callback: function () { michael@0: LightWeightThemeWebInstaller._install(data); michael@0: } michael@0: }]; michael@0: michael@0: this._removePreviousNotifications(); michael@0: michael@0: var notificationBox = gBrowser.getNotificationBox(); michael@0: var notificationBar = michael@0: notificationBox.appendNotification(message, "lwtheme-install-request", "", michael@0: notificationBox.PRIORITY_INFO_MEDIUM, michael@0: buttons); michael@0: notificationBar.persistence = 1; michael@0: }, michael@0: michael@0: _install: function (newLWTheme) { michael@0: var previousLWTheme = this._manager.currentTheme; michael@0: michael@0: var listener = { michael@0: onEnabling: function(aAddon, aRequiresRestart) { michael@0: if (!aRequiresRestart) michael@0: return; michael@0: michael@0: let messageString = gNavigatorBundle.getFormattedString("lwthemeNeedsRestart.message", michael@0: [aAddon.name], 1); michael@0: michael@0: let action = { michael@0: label: gNavigatorBundle.getString("lwthemeNeedsRestart.button"), michael@0: accessKey: gNavigatorBundle.getString("lwthemeNeedsRestart.accesskey"), michael@0: callback: function () { michael@0: Application.restart(); michael@0: } michael@0: }; michael@0: michael@0: let options = { michael@0: timeout: Date.now() + 30000 michael@0: }; michael@0: michael@0: PopupNotifications.show(gBrowser.selectedBrowser, "addon-theme-change", michael@0: messageString, "addons-notification-icon", michael@0: action, null, options); michael@0: }, michael@0: michael@0: onEnabled: function(aAddon) { michael@0: LightWeightThemeWebInstaller._postInstallNotification(newLWTheme, previousLWTheme); michael@0: } michael@0: }; michael@0: michael@0: AddonManager.addAddonListener(listener); michael@0: this._manager.currentTheme = newLWTheme; michael@0: AddonManager.removeAddonListener(listener); michael@0: }, michael@0: michael@0: _postInstallNotification: function (newTheme, previousTheme) { michael@0: function text(id) { michael@0: return gNavigatorBundle.getString("lwthemePostInstallNotification." + id); michael@0: } michael@0: michael@0: var buttons = [{ michael@0: label: text("undoButton"), michael@0: accessKey: text("undoButton.accesskey"), michael@0: callback: function () { michael@0: LightWeightThemeWebInstaller._manager.forgetUsedTheme(newTheme.id); michael@0: LightWeightThemeWebInstaller._manager.currentTheme = previousTheme; michael@0: } michael@0: }, { michael@0: label: text("manageButton"), michael@0: accessKey: text("manageButton.accesskey"), michael@0: callback: function () { michael@0: BrowserOpenAddonsMgr("addons://list/theme"); michael@0: } michael@0: }]; michael@0: michael@0: this._removePreviousNotifications(); michael@0: michael@0: var notificationBox = gBrowser.getNotificationBox(); michael@0: var notificationBar = michael@0: notificationBox.appendNotification(text("message"), michael@0: "lwtheme-install-notification", "", michael@0: notificationBox.PRIORITY_INFO_MEDIUM, michael@0: buttons); michael@0: notificationBar.persistence = 1; michael@0: notificationBar.timeout = Date.now() + 20000; // 20 seconds michael@0: }, michael@0: michael@0: _removePreviousNotifications: function () { michael@0: var box = gBrowser.getNotificationBox(); michael@0: michael@0: ["lwtheme-install-request", michael@0: "lwtheme-install-notification"].forEach(function (value) { michael@0: var notification = box.getNotificationWithValue(value); michael@0: if (notification) michael@0: box.removeNotification(notification); michael@0: }); michael@0: }, michael@0: michael@0: _previewWindow: null, michael@0: _preview: function (event) { michael@0: if (!this._isAllowed(event.target)) michael@0: return; michael@0: michael@0: var data = this._getThemeFromNode(event.target); michael@0: if (!data) michael@0: return; michael@0: michael@0: this._resetPreview(); michael@0: michael@0: this._previewWindow = event.target.ownerDocument.defaultView; michael@0: this._previewWindow.addEventListener("pagehide", this, true); michael@0: gBrowser.tabContainer.addEventListener("TabSelect", this, false); michael@0: michael@0: this._manager.previewTheme(data); michael@0: }, michael@0: michael@0: _resetPreview: function (event) { michael@0: if (!this._previewWindow || michael@0: event && !this._isAllowed(event.target)) michael@0: return; michael@0: michael@0: this._previewWindow.removeEventListener("pagehide", this, true); michael@0: this._previewWindow = null; michael@0: gBrowser.tabContainer.removeEventListener("TabSelect", this, false); michael@0: michael@0: this._manager.resetPreview(); michael@0: }, michael@0: michael@0: _isAllowed: function (node) { michael@0: var pm = Services.perms; michael@0: michael@0: var uri = node.ownerDocument.documentURIObject; michael@0: return pm.testPermission(uri, "install") == pm.ALLOW_ACTION; michael@0: }, michael@0: michael@0: _getThemeFromNode: function (node) { michael@0: return this._manager.parseTheme(node.getAttribute("data-browsertheme"), michael@0: node.baseURI); michael@0: } michael@0: } michael@0: michael@0: /* michael@0: * Listen for Lightweight Theme styling changes and update the browser's theme accordingly. michael@0: */ michael@0: let LightweightThemeListener = { michael@0: _modifiedStyles: [], michael@0: michael@0: init: function () { michael@0: XPCOMUtils.defineLazyGetter(this, "styleSheet", function() { michael@0: for (let i = document.styleSheets.length - 1; i >= 0; i--) { michael@0: let sheet = document.styleSheets[i]; michael@0: if (sheet.href == "chrome://browser/skin/browser-lightweightTheme.css") michael@0: return sheet; michael@0: } michael@0: }); michael@0: michael@0: Services.obs.addObserver(this, "lightweight-theme-styling-update", false); michael@0: Services.obs.addObserver(this, "lightweight-theme-optimized", false); michael@0: if (document.documentElement.hasAttribute("lwtheme")) michael@0: this.updateStyleSheet(document.documentElement.style.backgroundImage); michael@0: }, michael@0: michael@0: uninit: function () { michael@0: Services.obs.removeObserver(this, "lightweight-theme-styling-update"); michael@0: Services.obs.removeObserver(this, "lightweight-theme-optimized"); michael@0: }, michael@0: michael@0: /** michael@0: * Append the headerImage to the background-image property of all rulesets in michael@0: * browser-lightweightTheme.css. michael@0: * michael@0: * @param headerImage - a string containing a CSS image for the lightweight theme header. michael@0: */ michael@0: updateStyleSheet: function(headerImage) { michael@0: if (!this.styleSheet) michael@0: return; michael@0: this.substituteRules(this.styleSheet.cssRules, headerImage); michael@0: }, michael@0: michael@0: substituteRules: function(ruleList, headerImage, existingStyleRulesModified = 0) { michael@0: let styleRulesModified = 0; michael@0: for (let i = 0; i < ruleList.length; i++) { michael@0: let rule = ruleList[i]; michael@0: if (rule instanceof Ci.nsIDOMCSSGroupingRule) { michael@0: // Add the number of modified sub-rules to the modified count michael@0: styleRulesModified += this.substituteRules(rule.cssRules, headerImage, existingStyleRulesModified + styleRulesModified); michael@0: } else if (rule instanceof Ci.nsIDOMCSSStyleRule) { michael@0: if (!rule.style.backgroundImage) michael@0: continue; michael@0: let modifiedIndex = existingStyleRulesModified + styleRulesModified; michael@0: if (!this._modifiedStyles[modifiedIndex]) michael@0: this._modifiedStyles[modifiedIndex] = { backgroundImage: rule.style.backgroundImage }; michael@0: michael@0: rule.style.backgroundImage = this._modifiedStyles[modifiedIndex].backgroundImage + ", " + headerImage; michael@0: styleRulesModified++; michael@0: } else { michael@0: Cu.reportError("Unsupported rule encountered"); michael@0: } michael@0: } michael@0: return styleRulesModified; michael@0: }, michael@0: michael@0: // nsIObserver michael@0: observe: function (aSubject, aTopic, aData) { michael@0: if ((aTopic != "lightweight-theme-styling-update" && aTopic != "lightweight-theme-optimized") || michael@0: !this.styleSheet) michael@0: return; michael@0: michael@0: if (aTopic == "lightweight-theme-optimized" && aSubject != window) michael@0: return; michael@0: michael@0: let themeData = JSON.parse(aData); michael@0: if (!themeData) michael@0: return; michael@0: this.updateStyleSheet("url(" + themeData.headerURL + ")"); michael@0: }, michael@0: };