diff options
Diffstat (limited to 'application/basilisk/base/content/browser-addons.js')
-rw-r--r-- | application/basilisk/base/content/browser-addons.js | 869 |
1 files changed, 869 insertions, 0 deletions
diff --git a/application/basilisk/base/content/browser-addons.js b/application/basilisk/base/content/browser-addons.js new file mode 100644 index 000000000..378437b2b --- /dev/null +++ b/application/basilisk/base/content/browser-addons.js @@ -0,0 +1,869 @@ +/* -*- 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 + ")"); + }, +}; |