# -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. // Removes a doorhanger notification if all of the installs it was notifying // about have ended in some way. function removeNotificationOnEnd(notification, installs) { let count = installs.length; function maybeRemove(install) { install.removeListener(this); if (--count == 0) { // Check that the notification is still showing let current = PopupNotifications.getNotification(notification.id, notification.browser); if (current === notification) notification.remove(); } } for (let install of installs) { install.addListener({ onDownloadCancelled: maybeRemove, onDownloadFailed: maybeRemove, onInstallFailed: maybeRemove, onInstallEnded: maybeRemove }); } } const gXPInstallObserver = { _findChildShell: function (aDocShell, aSoughtShell) { if (aDocShell == aSoughtShell) { return aDocShell; } var node = aDocShell.QueryInterface(Components.interfaces.nsIDocShellTreeItem); for (var i = 0; i < node.childCount; ++i) { var docShell = node.getChildAt(i); docShell = this._findChildShell(docShell, aSoughtShell); if (docShell == aSoughtShell) { return docShell; } } return null; }, _getBrowser: function (aDocShell) { for (let browser of gBrowser.browsers) { if (this._findChildShell(browser.docShell, aDocShell)) { return browser; } } return null; }, observe: function (aSubject, aTopic, aData) { var brandBundle = document.getElementById("bundle_brand"); var installInfo = aSubject.QueryInterface(Components.interfaces.amIWebInstallInfo); var browser = installInfo.browser; // Make sure the browser is still alive. if (!browser || gBrowser.browsers.indexOf(browser) == -1) { return; } const anchorID = "addons-notification-icon"; var messageString, action; var brandShortName = brandBundle.getString("brandShortName"); var notificationID = aTopic; // Make notifications persist a minimum of 30 seconds var options = { timeout: Date.now() + 30000 }; switch (aTopic) { case "addon-install-disabled": { notificationID = "xpinstall-disabled" if (gPrefService.prefIsLocked("xpinstall.enabled")) { messageString = gNavigatorBundle.getString("xpinstallDisabledMessageLocked"); buttons = []; } else { messageString = gNavigatorBundle.getString("xpinstallDisabledMessage"); action = { label: gNavigatorBundle.getString("xpinstallDisabledButton"), accessKey: gNavigatorBundle.getString("xpinstallDisabledButton.accesskey"), callback: function editPrefs() { gPrefService.setBoolPref("xpinstall.enabled", true); } }; } PopupNotifications.show(browser, notificationID, messageString, anchorID, action, null, options); break; } case "addon-install-origin-blocked": { messageString = gNavigatorBundle.getFormattedString("xpinstallPromptWarningOrigin", [brandShortName]); let popup = PopupNotifications.show(browser, notificationID, messageString, anchorID, null, null, options); removeNotificationOnEnd(popup, installInfo.installs); break; } case "addon-install-blocked": { let originatingHost; try { originatingHost = installInfo.originatingURI.host; } catch(ex) { // Need to deal with missing originatingURI and with about:/data: URIs more gracefully, // see bug 1063418 - but for now, bail: return; } messageString = gNavigatorBundle.getFormattedString("xpinstallPromptWarning", [brandShortName, originatingHost]); action = { label: gNavigatorBundle.getString("xpinstallPromptAllowButton"), accessKey: gNavigatorBundle.getString("xpinstallPromptAllowButton.accesskey"), callback: function() { installInfo.install(); } }; let popup = PopupNotifications.show(browser, notificationID, messageString, anchorID, action, null, options); removeNotificationOnEnd(popup, installInfo.installs); break; } case "addon-install-started": { var needsDownload = function needsDownload(aInstall) { return aInstall.state != AddonManager.STATE_DOWNLOADED; } // If all installs have already been downloaded then there is no need to // show the download progress if (!installInfo.installs.some(needsDownload)) { return; } notificationID = "addon-progress"; messageString = gNavigatorBundle.getString("addonDownloading"); messageString = PluralForm.get(installInfo.installs.length, messageString); options.installs = installInfo.installs; options.contentWindow = browser.contentWindow; options.sourceURI = browser.currentURI; options.eventCallback = function(aEvent) { if (aEvent != "removed") { return; } options.contentWindow = null; options.sourceURI = null; }; PopupNotifications.show(browser, notificationID, messageString, anchorID, null, null, options); break; } case "addon-install-failed": { // TODO This isn't terribly ideal for the multiple failure case for (let install of installInfo.installs) { let host = (installInfo.originatingURI instanceof Ci.nsIStandardURL) && installInfo.originatingURI.host; if (!host) { host = (install.sourceURI instanceof Ci.nsIStandardURL) && install.sourceURI.host; } let error = (host || install.error == 0) ? "addonError" : "addonLocalError"; if (install.error != 0) { error += install.error; } else if (install.addon.jetsdk) { error += "JetSDK"; } else if (install.addon.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED) { error += "Blocklisted"; } else { error += "Incompatible"; } messageString = gNavigatorBundle.getString(error); messageString = messageString.replace("#1", install.name); if (host) { messageString = messageString.replace("#2", host); } messageString = messageString.replace("#3", brandShortName); messageString = messageString.replace("#4", Services.appinfo.version); PopupNotifications.show(browser, notificationID, messageString, anchorID, action, null, options); } break; } case "addon-install-complete": { var needsRestart = installInfo.installs.some(function(i) { return i.addon.pendingOperations != AddonManager.PENDING_NONE; }); if (needsRestart) { messageString = gNavigatorBundle.getString("addonsInstalledNeedsRestart"); action = { label: gNavigatorBundle.getString("addonInstallRestartButton"), accessKey: gNavigatorBundle.getString("addonInstallRestartButton.accesskey"), callback: function() { Application.restart(); } }; } else { messageString = gNavigatorBundle.getString("addonsInstalled"); action = null; } messageString = PluralForm.get(installInfo.installs.length, messageString); messageString = messageString.replace("#1", installInfo.installs[0].name); messageString = messageString.replace("#2", installInfo.installs.length); messageString = messageString.replace("#3", brandShortName); // Remove notificaion on dismissal, since it's possible to cancel the // install through the addons manager UI, making the "restart" prompt // irrelevant. options.removeOnDismissal = true; PopupNotifications.show(browser, notificationID, messageString, anchorID, action, null, options); break; } } } }; /* * When addons are installed/uninstalled, check and see if the number of items * on the add-on bar changed: * - If an add-on was installed, incrementing the count, show the bar. * - If an add-on was uninstalled, and no more items are left, hide the bar. */ var AddonsMgrListener = { get addonBar() document.getElementById("addon-bar"), get statusBar() document.getElementById("status-bar"), getAddonBarItemCount: function() { // Take into account the contents of the status bar shim for the count. var itemCount = this.statusBar.childNodes.length; var defaultOrNoninteractive = this.addonBar.getAttribute("defaultset") .split(",") .concat(["separator", "spacer", "spring"]); for (let item of this.addonBar.currentSet.split(",")) { if (defaultOrNoninteractive.indexOf(item) == -1) { itemCount++; } } return itemCount; }, onInstalling: function(aAddon) { this.lastAddonBarCount = this.getAddonBarItemCount(); }, onInstalled: function(aAddon) { if (this.getAddonBarItemCount() > this.lastAddonBarCount) { setToolbarVisibility(this.addonBar, true); } }, onUninstalling: function(aAddon) { this.lastAddonBarCount = this.getAddonBarItemCount(); }, onUninstalled: function(aAddon) { if (this.getAddonBarItemCount() == 0) { setToolbarVisibility(this.addonBar, false); } }, onEnabling: function(aAddon) { return this.onInstalling(); }, onEnabled: function(aAddon) { return this.onInstalled(); }, onDisabling: function(aAddon) { return this.onUninstalling(); }, onDisabled: function(aAddon) { return this.onUninstalled(); } }; #ifdef MOZ_PERSONAS var LightWeightThemeWebInstaller = { handleEvent: function (event) { switch (event.type) { case "InstallBrowserTheme": case "PreviewBrowserTheme": case "ResetBrowserThemePreview": // ignore requests from background tabs if (event.target.ownerDocument.defaultView.top != content) { return; } } switch (event.type) { case "InstallBrowserTheme": this._installRequest(event); break; case "PreviewBrowserTheme": this._preview(event); break; case "ResetBrowserThemePreview": this._resetPreview(event); break; case "pagehide": case "TabSelect": this._resetPreview(); break; } }, get _manager () { var temp = {}; Cu.import("resource://gre/modules/LightweightThemeManager.jsm", temp); delete this._manager; return this._manager = temp.LightweightThemeManager; }, _installRequest: function (event) { var node = event.target; var data = this._getThemeFromNode(node); if (!data) { return; } if (this._isAllowed(node)) { this._install(data); return; } var allowButtonText = gNavigatorBundle.getString("lwthemeInstallRequest.allowButton"); var allowButtonAccesskey = gNavigatorBundle.getString("lwthemeInstallRequest.allowButton.accesskey"); var message = gNavigatorBundle.getFormattedString("lwthemeInstallRequest.message", [node.ownerDocument.location.host]); var buttons = [{ label: allowButtonText, accessKey: allowButtonAccesskey, callback: function () { LightWeightThemeWebInstaller._install(data); } }]; this._removePreviousNotifications(); var notificationBox = gBrowser.getNotificationBox(); var notificationBar = notificationBox.appendNotification(message, "lwtheme-install-request", "", notificationBox.PRIORITY_INFO_MEDIUM, buttons); notificationBar.persistence = 1; }, _install: function (newLWTheme) { var previousLWTheme = this._manager.currentTheme; var listener = { onEnabling: function(aAddon, aRequiresRestart) { if (!aRequiresRestart) { return; } let messageString = gNavigatorBundle.getFormattedString("lwthemeNeedsRestart.message", [aAddon.name], 1); let action = { label: gNavigatorBundle.getString("lwthemeNeedsRestart.button"), accessKey: gNavigatorBundle.getString("lwthemeNeedsRestart.accesskey"), callback: function () { Application.restart(); } }; let options = { timeout: Date.now() + 30000 }; PopupNotifications.show(gBrowser.selectedBrowser, "addon-theme-change", messageString, "addons-notification-icon", action, null, options); }, onEnabled: function(aAddon) { LightWeightThemeWebInstaller._postInstallNotification(newLWTheme, previousLWTheme); } }; AddonManager.addAddonListener(listener); this._manager.currentTheme = newLWTheme; AddonManager.removeAddonListener(listener); }, _postInstallNotification: function (newTheme, previousTheme) { function text(id) { return gNavigatorBundle.getString("lwthemePostInstallNotification." + id); } var buttons = [{ label: text("undoButton"), accessKey: text("undoButton.accesskey"), callback: function () { LightWeightThemeWebInstaller._manager.forgetUsedTheme(newTheme.id); LightWeightThemeWebInstaller._manager.currentTheme = previousTheme; } }, { label: text("manageButton"), accessKey: text("manageButton.accesskey"), callback: function () { BrowserOpenAddonsMgr("addons://list/theme"); } }]; this._removePreviousNotifications(); var notificationBox = gBrowser.getNotificationBox(); var notificationBar = notificationBox.appendNotification(text("message"), "lwtheme-install-notification", "", notificationBox.PRIORITY_INFO_MEDIUM, buttons); notificationBar.persistence = 1; notificationBar.timeout = Date.now() + 20000; // 20 seconds }, _removePreviousNotifications: function () { var box = gBrowser.getNotificationBox(); ["lwtheme-install-request", "lwtheme-install-notification"].forEach(function (value) { var notification = box.getNotificationWithValue(value); if (notification) box.removeNotification(notification); }); }, _previewWindow: null, _preview: function (event) { if (!this._isAllowed(event.target)) { return; } var data = this._getThemeFromNode(event.target); if (!data) { return; } this._resetPreview(); this._previewWindow = event.target.ownerDocument.defaultView; this._previewWindow.addEventListener("pagehide", this, true); gBrowser.tabContainer.addEventListener("TabSelect", this, false); this._manager.previewTheme(data); }, _resetPreview: function (event) { if (!this._previewWindow || (event && !this._isAllowed(event.target))) { return; } this._previewWindow.removeEventListener("pagehide", this, true); this._previewWindow = null; gBrowser.tabContainer.removeEventListener("TabSelect", this, false); this._manager.resetPreview(); }, _isAllowed: function (node) { var pm = Services.perms; var uri = node.ownerDocument.documentURIObject; return pm.testPermission(uri, "install") == pm.ALLOW_ACTION; }, _getThemeFromNode: function (node) { return this._manager.parseTheme(node.getAttribute("data-browsertheme"), node.baseURI); } } /* * Listen for Lightweight Theme styling changes and update the browser's theme accordingly. */ var LightweightThemeListener = { _modifiedStyles: [], init: function () { XPCOMUtils.defineLazyGetter(this, "styleSheet", function() { for (let i = document.styleSheets.length - 1; i >= 0; i--) { let sheet = document.styleSheets[i]; if (sheet.href == "chrome://browser/skin/browser-lightweightTheme.css") return sheet; } }); Services.obs.addObserver(this, "lightweight-theme-styling-update", false); Services.obs.addObserver(this, "lightweight-theme-optimized", false); if (document.documentElement.hasAttribute("lwtheme")) { this.updateStyleSheet(document.documentElement.style.backgroundImage); } }, uninit: function () { Services.obs.removeObserver(this, "lightweight-theme-styling-update"); Services.obs.removeObserver(this, "lightweight-theme-optimized"); }, /** * Append the headerImage to the background-image property of all rulesets in * browser-lightweightTheme.css. * * @param headerImage - a string containing a CSS image for the lightweight theme header. */ updateStyleSheet: function(headerImage) { if (!this.styleSheet) { return; } this.substituteRules(this.styleSheet.cssRules, headerImage); }, substituteRules: function(ruleList, headerImage, existingStyleRulesModified = 0) { let styleRulesModified = 0; for (let i = 0; i < ruleList.length; i++) { let rule = ruleList[i]; if (rule instanceof Ci.nsIDOMCSSGroupingRule) { // Add the number of modified sub-rules to the modified count styleRulesModified += this.substituteRules(rule.cssRules, headerImage, existingStyleRulesModified + styleRulesModified); } else if (rule instanceof Ci.nsIDOMCSSStyleRule) { if (!rule.style.backgroundImage) { continue; } let modifiedIndex = existingStyleRulesModified + styleRulesModified; if (!this._modifiedStyles[modifiedIndex]) { this._modifiedStyles[modifiedIndex] = { backgroundImage: rule.style.backgroundImage }; } rule.style.backgroundImage = this._modifiedStyles[modifiedIndex].backgroundImage + ", " + headerImage; styleRulesModified++; } else { Cu.reportError("Unsupported rule encountered"); } } return styleRulesModified; }, // nsIObserver observe: function (aSubject, aTopic, aData) { if ((aTopic != "lightweight-theme-styling-update" && aTopic != "lightweight-theme-optimized") || !this.styleSheet) { return; } if (aTopic == "lightweight-theme-optimized" && aSubject != window) { return; } let themeData = JSON.parse(aData); if (!themeData) { return; } this.updateStyleSheet("url(" + themeData.headerURL + ")"); }, }; #endif