/* -*- indent-tabs-mode: nil; js-indent-level: 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(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(aDocShell) { for (let browser of gBrowser.browsers) { if (this._findChildShell(browser.docShell, aDocShell)) return browser; } return null; }, pendingInstalls: new WeakMap(), showInstallConfirmation(browser, installInfo, height = undefined) { // If the confirmation notification is already open cache the installInfo // and the new confirmation will be shown later if (PopupNotifications.getNotification("addon-install-confirmation", browser)) { let pending = this.pendingInstalls.get(browser); if (pending) { pending.push(installInfo); } else { this.pendingInstalls.set(browser, [installInfo]); } return; } let showNextConfirmation = () => { // Make sure the browser is still alive. if (gBrowser.browsers.indexOf(browser) == -1) return; let pending = this.pendingInstalls.get(browser); if (pending && pending.length) this.showInstallConfirmation(browser, pending.shift()); } // If all installs have already been cancelled in some way then just show // the next confirmation if (installInfo.installs.every(i => i.state != AddonManager.STATE_DOWNLOADED)) { showNextConfirmation(); return; } const anchorID = "addons-notification-icon"; // Make notifications persistent var options = { displayURI: installInfo.originatingURI, persistent: true, }; let acceptInstallation = () => { for (let install of installInfo.installs) install.install(); installInfo = null; Services.telemetry .getHistogramById("SECURITY_UI") .add(Ci.nsISecurityUITelemetry.WARNING_CONFIRM_ADDON_INSTALL_CLICK_THROUGH); }; let cancelInstallation = () => { if (installInfo) { for (let install of installInfo.installs) { // The notification may have been closed because the add-ons got // cancelled elsewhere, only try to cancel those that are still // pending install. if (install.state != AddonManager.STATE_CANCELLED) install.cancel(); } } showNextConfirmation(); }; let unsigned = installInfo.installs.filter(i => i.addon.signedState <= AddonManager.SIGNEDSTATE_MISSING); let someUnsigned = unsigned.length > 0 && unsigned.length < installInfo.installs.length; options.eventCallback = (aEvent) => { switch (aEvent) { case "removed": cancelInstallation(); break; case "shown": let addonList = document.getElementById("addon-install-confirmation-content"); while (addonList.firstChild) addonList.firstChild.remove(); for (let install of installInfo.installs) { let container = document.createElement("hbox"); let name = document.createElement("label"); name.setAttribute("value", install.addon.name); name.setAttribute("class", "addon-install-confirmation-name"); container.appendChild(name); if (someUnsigned && install.addon.signedState <= AddonManager.SIGNEDSTATE_MISSING) { let unsignedLabel = document.createElement("label"); unsignedLabel.setAttribute("value", gNavigatorBundle.getString("addonInstall.unsigned")); unsignedLabel.setAttribute("class", "addon-install-confirmation-unsigned"); container.appendChild(unsignedLabel); } addonList.appendChild(container); } break; } }; options.learnMoreURL = Services.urlFormatter.formatURLPref("app.support.baseURL"); let messageString; let notification = document.getElementById("addon-install-confirmation-notification"); if (unsigned.length == installInfo.installs.length) { // None of the add-ons are verified messageString = gNavigatorBundle.getString("addonConfirmInstallUnsigned.message"); notification.setAttribute("warning", "true"); options.learnMoreURL += "unsigned-addons"; } else if (unsigned.length == 0) { // All add-ons are verified or don't need to be verified messageString = gNavigatorBundle.getString("addonConfirmInstall.message"); notification.removeAttribute("warning"); options.learnMoreURL += "find-and-install-add-ons"; } else { // Some of the add-ons are unverified, the list of names will indicate // which messageString = gNavigatorBundle.getString("addonConfirmInstallSomeUnsigned.message"); notification.setAttribute("warning", "true"); options.learnMoreURL += "unsigned-addons"; } let brandBundle = document.getElementById("bundle_brand"); let brandShortName = brandBundle.getString("brandShortName"); messageString = PluralForm.get(installInfo.installs.length, messageString); messageString = messageString.replace("#1", brandShortName); messageString = messageString.replace("#2", installInfo.installs.length); let action = { label: gNavigatorBundle.getString("addonInstall.acceptButton.label"), accessKey: gNavigatorBundle.getString("addonInstall.acceptButton.accesskey"), callback: acceptInstallation, }; let secondaryAction = { label: gNavigatorBundle.getString("addonInstall.cancelButton.label"), accessKey: gNavigatorBundle.getString("addonInstall.cancelButton.accesskey"), callback: () => {}, }; if (height) { notification.style.minHeight = height + "px"; } let tab = gBrowser.getTabForBrowser(browser); if (tab) { gBrowser.selectedTab = tab; } let popup = PopupNotifications.show(browser, "addon-install-confirmation", messageString, anchorID, action, [secondaryAction], options); removeNotificationOnEnd(popup, installInfo.installs); Services.telemetry .getHistogramById("SECURITY_UI") .add(Ci.nsISecurityUITelemetry.WARNING_CONFIRM_ADDON_INSTALL); }, observe(aSubject, aTopic, aData) { var brandBundle = document.getElementById("bundle_brand"); var installInfo = aSubject.wrappedJSObject; 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 persistent var options = { displayURI: installInfo.originatingURI, persistent: true, hideClose: true, timeout: Date.now() + 30000, }; switch (aTopic) { case "addon-install-disabled": { notificationID = "xpinstall-disabled"; let secondaryActions = null; 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); } }; secondaryActions = [{ label: gNavigatorBundle.getString("addonInstall.cancelButton.label"), accessKey: gNavigatorBundle.getString("addonInstall.cancelButton.accesskey"), callback: () => {}, }]; } PopupNotifications.show(browser, notificationID, messageString, anchorID, action, secondaryActions, options); break; } case "addon-install-origin-blocked": { messageString = gNavigatorBundle.getFormattedString("xpinstallPromptMessage", [brandShortName]); options.removeOnDismissal = true; options.persistent = false; let secHistogram = Components.classes["@mozilla.org/base/telemetry;1"].getService(Ci.nsITelemetry).getHistogramById("SECURITY_UI"); secHistogram.add(Ci.nsISecurityUITelemetry.WARNING_ADDON_ASKING_PREVENTED); let popup = PopupNotifications.show(browser, notificationID, messageString, anchorID, null, null, options); removeNotificationOnEnd(popup, installInfo.installs); break; } case "addon-install-blocked": { messageString = gNavigatorBundle.getFormattedString("xpinstallPromptMessage", [brandShortName]); let secHistogram = Components.classes["@mozilla.org/base/telemetry;1"].getService(Ci.nsITelemetry).getHistogramById("SECURITY_UI"); action = { label: gNavigatorBundle.getString("xpinstallPromptAllowButton"), accessKey: gNavigatorBundle.getString("xpinstallPromptAllowButton.accesskey"), callback() { secHistogram.add(Ci.nsISecurityUITelemetry.WARNING_ADDON_ASKING_PREVENTED_CLICK_THROUGH); installInfo.install(); } }; let secondaryAction = { label: gNavigatorBundle.getString("xpinstallPromptMessage.dontAllow"), accessKey: gNavigatorBundle.getString("xpinstallPromptMessage.dontAllow.accesskey"), callback: () => {}, }; secHistogram.add(Ci.nsISecurityUITelemetry.WARNING_ADDON_ASKING_PREVENTED); let popup = PopupNotifications.show(browser, notificationID, messageString, anchorID, action, [secondaryAction], options); removeNotificationOnEnd(popup, installInfo.installs); break; } case "addon-install-started": { let 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("addonDownloadingAndVerifying"); messageString = PluralForm.get(installInfo.installs.length, messageString); messageString = messageString.replace("#1", installInfo.installs.length); options.installs = installInfo.installs; options.contentWindow = browser.contentWindow; options.sourceURI = browser.currentURI; options.eventCallback = function(aEvent) { switch (aEvent) { case "shown": let notificationElement = [...this.owner.panel.childNodes] .find(n => n.notification == this); if (notificationElement) { if (Preferences.get("xpinstall.customConfirmationUI", false)) { notificationElement.setAttribute("mainactiondisabled", "true"); } else { notificationElement.button.hidden = true; } } break; case "removed": options.contentWindow = null; options.sourceURI = null; break; } }; action = { label: gNavigatorBundle.getString("addonInstall.acceptButton.label"), accessKey: gNavigatorBundle.getString("addonInstall.acceptButton.accesskey"), callback: () => {}, }; let secondaryAction = { label: gNavigatorBundle.getString("addonInstall.cancelButton.label"), accessKey: gNavigatorBundle.getString("addonInstall.cancelButton.accesskey"), callback: () => { for (let install of installInfo.installs) { if (install.state != AddonManager.STATE_CANCELLED) { install.cancel(); } } }, }; let notification = PopupNotifications.show(browser, notificationID, messageString, anchorID, action, [secondaryAction], options); notification._startTime = Date.now(); break; } case "addon-install-failed": { options.removeOnDismissal = true; options.persistent = false; // TODO This isn't terribly ideal for the multiple failure case for (let install of installInfo.installs) { let host; try { host = options.displayURI.host; } catch (e) { // displayURI might be missing or 'host' might throw for non-nsStandardURL nsIURIs. } if (!host) host = (install.sourceURI instanceof Ci.nsIStandardURL) && install.sourceURI.host; let error = (host || install.error == 0) ? "addonInstallError" : "addonLocalInstallError"; let args; if (install.error < 0) { error += install.error; args = [brandShortName, install.name]; } else if (install.addon.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED) { error += "Blocklisted"; args = [install.name]; } else { error += "Incompatible"; args = [brandShortName, Services.appinfo.version, install.name]; } // Add Learn More link when refusing to install an unsigned add-on if (install.error == AddonManager.ERROR_SIGNEDSTATE_REQUIRED) { options.learnMoreURL = Services.urlFormatter.formatURLPref("app.support.baseURL") + "unsigned-addons"; } messageString = gNavigatorBundle.getFormattedString(error, args); PopupNotifications.show(browser, notificationID, messageString, anchorID, action, null, options); // Can't have multiple notifications with the same ID, so stop here. break; } this._removeProgressNotification(browser); break; } case "addon-install-confirmation": { let showNotification = () => { let height = undefined; if (PopupNotifications.isPanelOpen) { let rect = document.getElementById("addon-progress-notification").getBoundingClientRect(); height = rect.height; } this._removeProgressNotification(browser); this.showInstallConfirmation(browser, installInfo, height); }; let progressNotification = PopupNotifications.getNotification("addon-progress", browser); if (progressNotification) { let downloadDuration = Date.now() - progressNotification._startTime; let securityDelay = Services.prefs.getIntPref("security.dialog_enable_delay") - downloadDuration; if (securityDelay > 0) { setTimeout(() => { // The download may have been cancelled during the security delay if (PopupNotifications.getNotification("addon-progress", browser)) showNotification(); }, securityDelay); break; } } showNotification(); break; } case "addon-install-complete": { let needsRestart = installInfo.installs.some(function(i) { return i.addon.pendingOperations != AddonManager.PENDING_NONE; }); let secondaryActions = null; if (needsRestart) { notificationID = "addon-install-restart"; messageString = gNavigatorBundle.getString("addonsInstalledNeedsRestart"); action = { label: gNavigatorBundle.getString("addonInstallRestartButton"), accessKey: gNavigatorBundle.getString("addonInstallRestartButton.accesskey"), callback() { BrowserUtils.restartApplication(); } }; secondaryActions = [{ label: gNavigatorBundle.getString("addonInstallRestartIgnoreButton"), accessKey: gNavigatorBundle.getString("addonInstallRestartIgnoreButton.accesskey"), callback: () => {}, }]; } 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 notification on dismissal, since it's possible to cancel the // install through the addons manager UI, making the "restart" prompt // irrelevant. options.removeOnDismissal = true; options.persistent = false; PopupNotifications.show(browser, notificationID, messageString, anchorID, action, secondaryActions, options); break; } } }, _removeProgressNotification(aBrowser) { let notification = PopupNotifications.getNotification("addon-progress", aBrowser); if (notification) notification.remove(); } }; const gExtensionsNotifications = { initialized: false, init() { this.updateAlerts(); this.boundUpdate = this.updateAlerts.bind(this); ExtensionsUI.on("change", this.boundUpdate); this.initialized = true; }, uninit() { // uninit() can race ahead of init() in some cases, if that happens, // we have no handler to remove. if (!this.initialized) { return; } ExtensionsUI.off("change", this.boundUpdate); }, updateAlerts() { let sideloaded = ExtensionsUI.sideloaded; let updates = ExtensionsUI.updates; if (sideloaded.size + updates.size == 0) { gMenuButtonBadgeManager.removeBadge(gMenuButtonBadgeManager.BADGEID_ADDONS); } else { gMenuButtonBadgeManager.addBadge(gMenuButtonBadgeManager.BADGEID_ADDONS, "addon-alert"); } let container = document.getElementById("PanelUI-footer-addons"); while (container.firstChild) { container.firstChild.remove(); } const DEFAULT_EXTENSION_ICON = "chrome://mozapps/skin/extensions/extensionGeneric.svg"; let items = 0; for (let update of updates) { if (++items > 4) { break; } let button = document.createElement("toolbarbutton"); let text = gNavigatorBundle.getFormattedString("webextPerms.updateMenuItem", [update.addon.name]); button.setAttribute("label", text); let icon = update.addon.iconURL || DEFAULT_EXTENSION_ICON; button.setAttribute("image", icon); button.addEventListener("click", evt => { ExtensionsUI.showUpdate(gBrowser, update); }); container.appendChild(button); } let appName; for (let addon of sideloaded) { if (++items > 4) { break; } if (!appName) { let brandBundle = document.getElementById("bundle_brand"); appName = brandBundle.getString("brandShortName"); } let button = document.createElement("toolbarbutton"); let text = gNavigatorBundle.getFormattedString("webextPerms.sideloadMenuItem", [addon.name, appName]); button.setAttribute("label", text); let icon = addon.iconURL || DEFAULT_EXTENSION_ICON; button.setAttribute("image", icon); button.addEventListener("click", evt => { ExtensionsUI.showSideloaded(gBrowser, addon); }); container.appendChild(button); } }, }; var LightWeightThemeWebInstaller = { init() { let mm = window.messageManager; mm.addMessageListener("LightWeightThemeWebInstaller:Install", this); mm.addMessageListener("LightWeightThemeWebInstaller:Preview", this); mm.addMessageListener("LightWeightThemeWebInstaller:ResetPreview", this); }, receiveMessage(message) { // ignore requests from background tabs if (message.target != gBrowser.selectedBrowser) { return; } let data = message.data; switch (message.name) { case "LightWeightThemeWebInstaller:Install": { this._installRequest(data.themeData, data.baseURI); break; } case "LightWeightThemeWebInstaller:Preview": { this._preview(data.themeData, data.baseURI); break; } case "LightWeightThemeWebInstaller:ResetPreview": { this._resetPreview(data && data.baseURI); break; } } }, handleEvent(event) { switch (event.type) { case "TabSelect": { this._resetPreview(); break; } } }, get _manager() { let temp = {}; Cu.import("resource://gre/modules/LightweightThemeManager.jsm", temp); delete this._manager; return this._manager = temp.LightweightThemeManager; }, _installRequest(dataString, baseURI) { let data = this._manager.parseTheme(dataString, baseURI); if (!data) { return; } let uri = makeURI(baseURI); // A notification bar with the option to undo is normally shown after a // theme is installed. But the discovery pane served from the url(s) // below has its own toggle switch for quick undos, so don't show the // notification in that case. let notify = uri.prePath != "https://discovery.addons.mozilla.org"; if (notify) { try { if (Services.prefs.getBoolPref("extensions.webapi.testing") && (uri.prePath == "https://discovery.addons.allizom.org" || uri.prePath == "https://discovery.addons-dev.allizom.org")) { notify = false; } } catch (e) { // getBoolPref() throws if the testing pref isn't set. ignore it. } } if (this._isAllowed(baseURI)) { this._install(data, notify); return; } let allowButtonText = gNavigatorBundle.getString("lwthemeInstallRequest.allowButton"); let allowButtonAccesskey = gNavigatorBundle.getString("lwthemeInstallRequest.allowButton.accesskey"); let message = gNavigatorBundle.getFormattedString("lwthemeInstallRequest.message", [uri.host]); let buttons = [{ label: allowButtonText, accessKey: allowButtonAccesskey, callback() { LightWeightThemeWebInstaller._install(data, notify); } }]; this._removePreviousNotifications(); let notificationBox = gBrowser.getNotificationBox(); let notificationBar = notificationBox.appendNotification(message, "lwtheme-install-request", "", notificationBox.PRIORITY_INFO_MEDIUM, buttons); notificationBar.persistence = 1; }, _install(newLWTheme, notify) { let previousLWTheme = this._manager.currentTheme; let listener = { onEnabling(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() { BrowserUtils.restartApplication(); } }; let options = { persistent: true }; PopupNotifications.show(gBrowser.selectedBrowser, "addon-theme-change", messageString, "addons-notification-icon", action, null, options); }, onEnabled(aAddon) { if (notify) { LightWeightThemeWebInstaller._postInstallNotification(newLWTheme, previousLWTheme); } } }; AddonManager.addAddonListener(listener); this._manager.currentTheme = newLWTheme; AddonManager.removeAddonListener(listener); }, _postInstallNotification(newTheme, previousTheme) { function text(id) { return gNavigatorBundle.getString("lwthemePostInstallNotification." + id); } let buttons = [{ label: text("undoButton"), accessKey: text("undoButton.accesskey"), callback() { LightWeightThemeWebInstaller._manager.forgetUsedTheme(newTheme.id); LightWeightThemeWebInstaller._manager.currentTheme = previousTheme; } }, { label: text("manageButton"), accessKey: text("manageButton.accesskey"), callback() { BrowserOpenAddonsMgr("addons://list/theme"); } }]; this._removePreviousNotifications(); let notificationBox = gBrowser.getNotificationBox(); let notificationBar = notificationBox.appendNotification(text("message"), "lwtheme-install-notification", "", notificationBox.PRIORITY_INFO_MEDIUM, buttons); notificationBar.persistence = 1; notificationBar.timeout = Date.now() + 20000; // 20 seconds }, _removePreviousNotifications() { let box = gBrowser.getNotificationBox(); ["lwtheme-install-request", "lwtheme-install-notification"].forEach(function(value) { let notification = box.getNotificationWithValue(value); if (notification) box.removeNotification(notification); }); }, _preview(dataString, baseURI) { if (!this._isAllowed(baseURI)) return; let data = this._manager.parseTheme(dataString, baseURI); if (!data) return; this._resetPreview(); gBrowser.tabContainer.addEventListener("TabSelect", this); this._manager.previewTheme(data); }, _resetPreview(baseURI) { if (baseURI && !this._isAllowed(baseURI)) return; gBrowser.tabContainer.removeEventListener("TabSelect", this); this._manager.resetPreview(); }, _isAllowed(srcURIString) { let uri; try { uri = makeURI(srcURIString); } catch (e) { // makeURI fails if srcURIString is a nonsense URI return false; } if (!uri.schemeIs("https")) { return false; } let pm = Services.perms; return pm.testPermission(uri, "install") == pm.ALLOW_ACTION; } }; /* * Listen for Lightweight Theme styling changes and update the browser's theme accordingly. */ var LightweightThemeListener = { _modifiedStyles: [], init() { 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; } return undefined; }); 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() { 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(headerImage) { if (!this.styleSheet) return; this.substituteRules(this.styleSheet.cssRules, headerImage); }, substituteRules(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(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 + ")"); }, };