From a1be17c1cea81ebb1e8b131a662c698d78f3f7f2 Mon Sep 17 00:00:00 2001 From: wolfbeast Date: Mon, 4 Jun 2018 13:17:38 +0200 Subject: Issue #303 Part 1: Move basilisk files from /browser to /application/basilisk --- .../basilisk/base/content/browser-addons.js | 742 +++++++++++++++++++++ 1 file changed, 742 insertions(+) create mode 100644 application/basilisk/base/content/browser-addons.js (limited to 'application/basilisk/base/content/browser-addons.js') diff --git a/application/basilisk/base/content/browser-addons.js b/application/basilisk/base/content/browser-addons.js new file mode 100644 index 000000000..1d881536a --- /dev/null +++ b/application/basilisk/base/content/browser-addons.js @@ -0,0 +1,742 @@ +/* -*- 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: 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; + }, + + pendingInstalls: new WeakMap(), + + showInstallConfirmation: function(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 persist a minimum of 30 seconds + var options = { + displayURI: installInfo.originatingURI, + timeout: Date.now() + 30000, + }; + + 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(); + } + } + + this.acceptInstallation = null; + + 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 unsigned = document.createElement("label"); + unsigned.setAttribute("value", gNavigatorBundle.getString("addonInstall.unsigned")); + unsigned.setAttribute("class", "addon-install-confirmation-unsigned"); + container.appendChild(unsigned); + } + + addonList.appendChild(container); + } + + this.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); + }; + 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 cancelButton = document.getElementById("addon-install-confirmation-cancel"); + cancelButton.label = gNavigatorBundle.getString("addonInstall.cancelButton.label"); + cancelButton.accessKey = gNavigatorBundle.getString("addonInstall.cancelButton.accesskey"); + + let acceptButton = document.getElementById("addon-install-confirmation-accept"); + acceptButton.label = gNavigatorBundle.getString("addonInstall.acceptButton.label"); + acceptButton.accessKey = gNavigatorBundle.getString("addonInstall.acceptButton.accesskey"); + + if (height) { + let notification = document.getElementById("addon-install-confirmation-notification"); + 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, null, null, + options); + + removeNotificationOnEnd(popup, installInfo.installs); + + Services.telemetry + .getHistogramById("SECURITY_UI") + .add(Ci.nsISecurityUITelemetry.WARNING_CONFIRM_ADDON_INSTALL); + }, + + 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 = { + displayURI: installInfo.originatingURI, + 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("xpinstallPromptMessage", + [brandShortName]); + + 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: function() { + secHistogram.add(Ci.nsISecurityUITelemetry.WARNING_ADDON_ASKING_PREVENTED_CLICK_THROUGH); + installInfo.install(); + } + }; + + secHistogram.add(Ci.nsISecurityUITelemetry.WARNING_ADDON_ASKING_PREVENTED); + let popup = PopupNotifications.show(browser, notificationID, + messageString, anchorID, + action, null, 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 = (aEvent) => { + switch (aEvent) { + case "removed": + options.contentWindow = null; + options.sourceURI = null; + break; + } + }; + let notification = PopupNotifications.show(browser, notificationID, messageString, + anchorID, null, null, options); + notification._startTime = Date.now(); + + let cancelButton = document.getElementById("addon-progress-cancel"); + cancelButton.label = gNavigatorBundle.getString("addonInstall.cancelButton.label"); + cancelButton.accessKey = gNavigatorBundle.getString("addonInstall.cancelButton.accesskey"); + + let acceptButton = document.getElementById("addon-progress-accept"); + if (Preferences.get("xpinstall.customConfirmationUI", false)) { + acceptButton.label = gNavigatorBundle.getString("addonInstall.acceptButton.label"); + acceptButton.accessKey = gNavigatorBundle.getString("addonInstall.acceptButton.accesskey"); + } else { + acceptButton.hidden = true; + } + break; } + case "addon-install-failed": { + // 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; + }); + + if (needsRestart) { + notificationID = "addon-install-restart"; + messageString = gNavigatorBundle.getString("addonsInstalledNeedsRestart"); + action = { + label: gNavigatorBundle.getString("addonInstallRestartButton"), + accessKey: gNavigatorBundle.getString("addonInstallRestartButton.accesskey"), + callback: function() { + BrowserUtils.restartApplication(); + } + }; + } + 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; } + } + }, + _removeProgressNotification(aBrowser) { + let notification = PopupNotifications.getNotification("addon-progress", aBrowser); + if (notification) + notification.remove(); + } +}; + +var LightWeightThemeWebInstaller = { + init: function () { + let mm = window.messageManager; + mm.addMessageListener("LightWeightThemeWebInstaller:Install", this); + mm.addMessageListener("LightWeightThemeWebInstaller:Preview", this); + mm.addMessageListener("LightWeightThemeWebInstaller:ResetPreview", this); + }, + + receiveMessage: function (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.principal, data.baseURI); + break; + } + case "LightWeightThemeWebInstaller:Preview": { + this._preview(data.themeData, data.principal, data.baseURI); + break; + } + case "LightWeightThemeWebInstaller:ResetPreview": { + this._resetPreview(data && data.principal); + break; + } + } + }, + + handleEvent: function (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, principal, baseURI) { + // Don't allow installing off null principals. + if (!principal.URI) { + return; + } + + let data = this._manager.parseTheme(dataString, baseURI); + + if (!data) { + return; + } + + // 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 = this._shouldShowUndoPrompt(principal); + if (this._isAllowed(principal)) { + this._install(data, notify); + return; + } + + let allowButtonText = + gNavigatorBundle.getString("lwthemeInstallRequest.allowButton"); + let allowButtonAccesskey = + gNavigatorBundle.getString("lwthemeInstallRequest.allowButton.accesskey"); + let message = + gNavigatorBundle.getFormattedString("lwthemeInstallRequest.message", + [principal.URI.host]); + let buttons = [{ + label: allowButtonText, + accessKey: allowButtonAccesskey, + callback: function () { + 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: function (newLWTheme, notify) { + let previousLWTheme = this._manager.currentTheme; + + let 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 () { + BrowserUtils.restartApplication(); + } + }; + + let options = { + timeout: Date.now() + 30000 + }; + + PopupNotifications.show(gBrowser.selectedBrowser, "addon-theme-change", + messageString, "addons-notification-icon", + action, null, options); + }, + + onEnabled: function(aAddon) { + if (notify) { + 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); + } + + let 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(); + + 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: function () { + 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, principal, baseURI) { + if (!this._isAllowed(principal)) + return; + + let data = this._manager.parseTheme(dataString, baseURI); + if (!data) + return; + + this._resetPreview(); + gBrowser.tabContainer.addEventListener("TabSelect", this, false); + this._manager.previewTheme(data); + }, + + _resetPreview(principal) { + if (!this._isAllowed(principal)) + return; + gBrowser.tabContainer.removeEventListener("TabSelect", this, false); + this._manager.resetPreview(); + }, + + _isAllowed(principal) { + if (!principal || !principal.URI || !principal.URI.schemeIs("https")) { + return false; + } + + let pm = Services.perms; + return pm.testPermission(principal.URI, "install") == pm.ALLOW_ACTION; + }, + + _shouldShowUndoPrompt(principal) { + if (!principal || !principal.URI) { + return true; + } + + let prePath = principal.URI.prePath; + if (prePath == "https://addons.palemoon.org") { + return false; + } + + return true; + } +}; + +/* + * 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; + } + 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: 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 + ")"); + }, +}; -- cgit v1.2.3