diff options
Diffstat (limited to 'data/extensions/spyblock@gnu.org/lib/ui.js')
-rw-r--r-- | data/extensions/spyblock@gnu.org/lib/ui.js | 1973 |
1 files changed, 1973 insertions, 0 deletions
diff --git a/data/extensions/spyblock@gnu.org/lib/ui.js b/data/extensions/spyblock@gnu.org/lib/ui.js new file mode 100644 index 0000000..668e356 --- /dev/null +++ b/data/extensions/spyblock@gnu.org/lib/ui.js @@ -0,0 +1,1973 @@ +/* + * This file is part of Adblock Plus <http://adblockplus.org/>, + * Copyright (C) 2006-2014 Eyeo GmbH + * + * Adblock Plus is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * Adblock Plus is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>. + */ + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +let {Utils} = require("utils"); +let {Prefs} = require("prefs"); +let {Policy} = require("contentPolicy"); +let {FilterStorage} = require("filterStorage"); +let {FilterNotifier} = require("filterNotifier"); +let {RequestNotifier} = require("requestNotifier"); +let {Filter} = require("filterClasses"); +let {Subscription, SpecialSubscription, DownloadableSubscription} = require("subscriptionClasses"); +let {Synchronizer} = require("synchronizer"); +let {KeySelector} = require("keySelector"); +let {Notification} = require("notification"); +let {initAntiAdblockNotification} = require("antiadblockInit"); + +let CustomizableUI = null; + +/** + * Filter corresponding with "disable on site" menu item (set in fillIconMent()). + * @type Filter + */ +let siteWhitelist = null; +/** + * Filter corresponding with "disable on site" menu item (set in fillIconMenu()). + * @type Filter + */ +let pageWhitelist = null; + +/** + * Window containing the detached list of blockable items. + * @type Window + */ +let detachedBottombar = null; + +/** + * Object initializing add-on options, observes add-on manager notifications + * about add-on options being opened. + * @type nsIObserver + */ +let optionsObserver = +{ + init: function() + { + Services.obs.addObserver(this, "addon-options-displayed", true); + onShutdown.add(function() + { + Services.obs.removeObserver(this, "addon-options-displayed"); + }.bind(this)); + }, + + /** + * Initializes options in add-on manager when they show up. + */ + initOptionsDoc: function(/**Document*/ doc) + { + function hideElement(id, hide) + { + let element = doc.getElementById(id); + if (element) + element.collapsed = hide; + } + function setChecked(id, checked) + { + let element = doc.getElementById(id); + if (element) + element.value = checked; + } + function addCommandHandler(id, handler) + { + let element = doc.getElementById(id); + if (element) + element.addEventListener("command", handler, false); + } + + Utils.splitAllLabels(doc); + + addCommandHandler("adblockplus-filters", UI.openFiltersDialog.bind(UI)); + + let {Sync} = require("sync"); + let syncEngine = Sync.getEngine(); + hideElement("adblockplus-sync", !syncEngine); + + let {defaultToolbarPosition, statusbarPosition} = require("appSupport"); + let hasToolbar = defaultToolbarPosition; + let hasStatusBar = statusbarPosition; + + hideElement("adblockplus-showintoolbar", !hasToolbar); + hideElement("adblockplus-showinstatusbar", !hasStatusBar); + + let checkbox = doc.querySelector("setting[type=bool]"); + if (checkbox) + initCheckboxes(); + + function initCheckboxes() + { + if (!("value" in checkbox)) + { + // XBL bindings didn't apply yet (bug 708397), try later + Utils.runAsync(initCheckboxes); + return; + } + + setChecked("adblockplus-savestats", Prefs.savestats); + addCommandHandler("adblockplus-savestats", function() + { + UI.toggleSaveStats(doc.defaultView); + this.value = Prefs.savestats; + }); + + let hasAcceptableAds = FilterStorage.subscriptions.some((subscription) => subscription instanceof DownloadableSubscription && + subscription.url == Prefs.subscriptions_exceptionsurl); + setChecked("adblockplus-acceptableAds", hasAcceptableAds); + addCommandHandler("adblockplus-acceptableAds", function() + { + this.value = UI.toggleAcceptableAds(); + }); + + setChecked("adblockplus-sync", syncEngine && syncEngine.enabled); + addCommandHandler("adblockplus-sync", function() + { + this.value = UI.toggleSync(); + }); + + setChecked("adblockplus-showintoolbar", UI.isToolbarIconVisible()); + addCommandHandler("adblockplus-showintoolbar", function() + { + UI.toggleToolbarIcon(); + this.value = UI.isToolbarIconVisible(); + }); + + let list = doc.getElementById("adblockplus-subscription-list"); + if (list) + { + // Load subscriptions data + let request = new XMLHttpRequest(); + request.mozBackgroundRequest = true; + request.open("GET", "chrome://adblockplus/content/ui/subscriptions.xml"); + request.addEventListener("load", function() + { + if (onShutdown.done) + return; + + let currentSubscription = FilterStorage.subscriptions.filter((subscription) => subscription instanceof DownloadableSubscription && + subscription.url != Prefs.subscriptions_exceptionsurl); + currentSubscription = (currentSubscription.length ? currentSubscription[0] : null); + + let subscriptions =request.responseXML.getElementsByTagName("subscription"); + for (let i = 0; i < subscriptions.length; i++) + { + let item = subscriptions[i]; + let url = item.getAttribute("url"); + if (!url) + continue; + + list.appendItem(item.getAttribute("title"), url, null); + if (currentSubscription && url == currentSubscription.url) + list.selectedIndex = list.itemCount - 1; + + if (currentSubscription && list.selectedIndex < 0) + { + list.appendItem(currentSubscription.title, currentSubscription.url, null); + list.selectedIndex = list.itemCount - 1; + } + } + + var listener = function() + { + if (list.value) + UI.setSubscription(list.value, list.label); + } + list.addEventListener("command", listener, false); + + // xul:menulist in Fennec is broken and doesn't trigger any events + // on selection. Have to detect selectIndex changes instead. + // See https://bugzilla.mozilla.org/show_bug.cgi?id=891736 + list.watch("selectedIndex", function(prop, oldval, newval) + { + Utils.runAsync(listener); + return newval; + }); + }, false); + request.send(); + } + } + }, + + observe: function(subject, topic, data) + { + let {addonID} = require("info") + if (data != addonID) + return; + + this.initOptionsDoc(subject.QueryInterface(Ci.nsIDOMDocument)); + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]) +}; +optionsObserver.init(); + +/** + * Session restore observer instance, stored to prevent it from being garbage + * collected. + * @type SessionRestoreObserver + */ +let sessionRestoreObserver = null; + +/** + * Observer waiting for the browsing session to be restored on startup. + */ +function SessionRestoreObserver(/**function*/ callback) +{ + sessionRestoreObserver = this; + + this.callback = callback; + Services.obs.addObserver(this, "sessionstore-windows-restored", true); + + // Just in case, don't wait longer than 5 seconds + this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + this.timer.init(this, 5000, Ci.nsITimer.TYPE_ONE_SHOT); +} +SessionRestoreObserver.prototype = +{ + callback: null, + timer: null, + observe: function(subject, topic, data) + { + Services.obs.removeObserver(this, "sessionstore-windows-restored"); + sessionRestoreObserver = null; + + this.timer.cancel(); + this.timer = null; + + if (!onShutdown.done) + this.callback(); + }, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]) +} + +/** + * Timer used to delay notification handling. + * @type nsITimer + */ +let notificationTimer = null; + +let UI = exports.UI = +{ + /** + * Gets called on startup, initializes UI integration. + */ + init: function() + { + // We should call initDone once both overlay and filters are loaded + let overlayLoaded = false; + let filtersLoaded = false; + let sessionRestored = false; + + // Start loading overlay + let request = new XMLHttpRequest(); + request.mozBackgroundRequest = true; + request.open("GET", "chrome://adblockplus/content/ui/overlay.xul"); + request.addEventListener("load", function(event) + { + if (onShutdown.done) + return; + + this.processOverlay(request.responseXML.documentElement); + + // Don't wait for the rest of the startup sequence, add icon already + this.addToolbarButton(); + + overlayLoaded = true; + if (overlayLoaded && filtersLoaded && sessionRestored) + this.initDone(); + }.bind(this), false); + request.send(null); + + // Wait for filters to load + if (FilterStorage._loading) + { + let listener = function(action) + { + if (action != "load") + return; + + FilterNotifier.removeListener(listener); + filtersLoaded = true; + if (overlayLoaded && filtersLoaded && sessionRestored) + this.initDone(); + }.bind(this); + FilterNotifier.addListener(listener); + } + else + filtersLoaded = true; + + // Initialize UI after the session is restored + let window = this.currentWindow; + if (!window && "nsISessionStore" in Ci) + { + // No application windows yet, the application must be starting up. Wait + // for session to be restored before initializing our UI. + new SessionRestoreObserver(function() + { + sessionRestored = true; + if (overlayLoaded && filtersLoaded && sessionRestored) + this.initDone(); + }.bind(this)); + } + else + sessionRestored = true; + }, + + /** + * Provesses overlay document data and initializes overlay property. + */ + processOverlay: function(/**Element*/ root) + { + Utils.splitAllLabels(root); + + let specialElements = {"abp-status-popup": true, "abp-status": true, "abp-toolbarbutton": true, "abp-menuitem": true, "abp-bottombar-container": true}; + + this.overlay = {all: []}; + + // Remove whitespace text nodes + let walker = root.ownerDocument.createTreeWalker( + root, Ci.nsIDOMNodeFilter.SHOW_TEXT, + (node) => !/\S/.test(node.nodeValue), false + ); + let whitespaceNodes = []; + while (walker.nextNode()) + whitespaceNodes.push(walker.currentNode); + + for (let i = 0; i < whitespaceNodes.length; i++) + whitespaceNodes[i].parentNode.removeChild(whitespaceNodes[i]); + + // Put overlay elements into appropriate fields + while (root.firstElementChild) + { + let child = root.firstElementChild; + if (child.getAttribute("id") in specialElements) + this.overlay[child.getAttribute("id")] = child; + else + this.overlay.all.push(child); + root.removeChild(child); + } + + // Read overlay attributes + this.overlay.attributes = {}; + for (let i = 0; i < root.attributes.length; i++) + this.overlay.attributes[root.attributes[i].name] = root.attributes[i].value; + + // Copy context menu into the toolbar icon and Tools menu item + function fixId(element, newId) + { + if (element.hasAttribute("id")) + element.setAttribute("id", element.getAttribute("id").replace("abp-status", newId)); + + for (let i = 0, len = element.children.length; i < len; i++) + fixId(element.children[i], newId); + + return element; + } + + if ("abp-status-popup" in this.overlay) + { + let menuSource = this.overlay["abp-status-popup"]; + delete this.overlay["abp-status-popup"]; + + if (this.overlay.all.length) + this.overlay.all[0].appendChild(menuSource); + if ("abp-toolbarbutton" in this.overlay) + this.overlay["abp-toolbarbutton"].appendChild(fixId(menuSource.cloneNode(true), "abp-toolbar")); + if ("abp-menuitem" in this.overlay) + this.overlay["abp-menuitem"].appendChild(fixId(menuSource.cloneNode(true), "abp-menuitem")); + } + }, + + /** + * Gets called once the initialization is finished and Adblock Plus elements + * can be added to the UI. + */ + initDone: function() + { + // The icon might be added already, make sure its state is correct + this.updateState(); + + // Listen for pref and filters changes + Prefs.addListener(function(name) + { + if (name == "enabled" || name == "defaulttoolbaraction" || name == "defaultstatusbaraction") + this.updateState(); + else if (name == "showinstatusbar") + { + for (let window in this.applicationWindows) + this.updateStatusbarIcon(window); + } + }.bind(this)); + FilterNotifier.addListener(function(action) + { + if (/^(filter|subscription)\.(added|removed|disabled|updated)$/.test(action) || action == "load") + this.updateState(); + }.bind(this)); + + notificationTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + notificationTimer.initWithCallback(this.showNextNotification.bind(this), + 3 * 60 * 1000, Ci.nsITimer.TYPE_ONE_SHOT); + onShutdown.add(() => notificationTimer.cancel()); + + // Add "anti-adblock messages" notification + initAntiAdblockNotification(); + + let documentCreationObserver = { + observe: function(subject, topic, data) + { + if (!(subject instanceof Ci.nsIDOMWindow)) + return; + + this.showNextNotification(subject.location.href); + }.bind(UI) + }; + Services.obs.addObserver(documentCreationObserver, "content-document-global-created", false); + onShutdown.add(function() + { + Services.obs.removeObserver(documentCreationObserver, "content-document-global-created", false); + }); + + // Execute first-run actions if a window is open already, otherwise it + // will happen in applyToWindow() when a window is opened. + this.firstRunActions(this.currentWindow); + }, + + addToolbarButton: function() + { + let {WindowObserver} = require("windowObserver"); + new WindowObserver(this); + + let {defaultToolbarPosition} = require("appSupport"); + if ("abp-toolbarbutton" in this.overlay && defaultToolbarPosition) + { + try + { + ({CustomizableUI}) = Cu.import("resource:///modules/CustomizableUI.jsm", null); + } + catch (e) + { + // No built-in CustomizableUI API, use our own implementation. + ({CustomizableUI}) = require("customizableUI"); + } + + CustomizableUI.createWidget({ + id: "abp-toolbarbutton", + type: "custom", + positionAttribute: "abp-iconposition", // For emulation only + defaultArea: defaultToolbarPosition.parent, + defaultBefore: defaultToolbarPosition.before, // For emulation only + defaultAfter: defaultToolbarPosition.after, // For emulation only + removable: true, + onBuild: function(document) + { + let node = document.importNode(this.overlay["abp-toolbarbutton"], true); + node.addEventListener("click", this.onIconClick, false); + node.addEventListener("command", this.onIconClick, false); + this.updateIconState(document.defaultView, node); + return node; + }.bind(this), + onAdded: function(node) + { + // For emulation only, this callback isn't part of the official + // CustomizableUI API. + this.updateIconState(node.ownerDocument.defaultView, node); + }.bind(this), + }); + onShutdown.add(CustomizableUI.destroyWidget.bind(CustomizableUI, "abp-toolbarbutton")); + } + }, + + firstRunActions: function(window) + { + if (this.firstRunDone || !window || FilterStorage._loading) + return; + + this.firstRunDone = true; + + let {addonVersion} = require("info"); + let prevVersion = Prefs.currentVersion; + if (prevVersion != addonVersion) + { + Prefs.currentVersion = addonVersion; + this.addSubscription(window, prevVersion); + } + }, + + /** + * Will be set to true after the check whether first-run actions should run + * has been performed. + * @type Boolean + */ + firstRunDone: false, + + /** + * Initializes Adblock Plus UI in a window. + */ + applyToWindow: function(/**Window*/ window, /**Boolean*/ noDelay) + { + let {delayInitialization, isKnownWindow, getBrowser, addBrowserLocationListener, addBrowserClickListener} = require("appSupport"); + if (window.document.documentElement.id == "CustomizeToolbarWindow" || isKnownWindow(window)) + { + // Add style processing instruction + let style = window.document.createProcessingInstruction("xml-stylesheet", 'class="adblockplus-node" href="chrome://adblockplus/skin/overlay.css" type="text/css"'); + window.document.insertBefore(style, window.document.firstChild); + } + + if (!isKnownWindow(window)) + return; + + // Thunderbird windows will not be initialized at this point, execute + // delayed + if (!noDelay && delayInitialization) + { + Utils.runAsync(this.applyToWindow.bind(this, window, true)); + return; + } + + // Add general items to the document + for (let i = 0; i < this.overlay.all.length; i++) + window.document.documentElement.appendChild(this.overlay.all[i].cloneNode(true)); + + // Add status bar icon + this.updateStatusbarIcon(window); + + // Add tools menu item + if ("abp-menuitem" in this.overlay) + { + let {toolsMenu} = require("appSupport"); + let [parent, before] = this.resolveInsertionPoint(window, toolsMenu); + if (parent) + parent.insertBefore(this.overlay["abp-menuitem"].cloneNode(true), before); + } + + // Attach event handlers + for (let i = 0; i < eventHandlers.length; i++) + { + let [id, event, handler] = eventHandlers[i]; + let element = window.document.getElementById(id); + if (element) + element.addEventListener(event, handler.bind(null, window), false); + } + window.addEventListener("popupshowing", this.onPopupShowing, false); + window.addEventListener("keypress", this.onKeyPress, false); + + addBrowserLocationListener(window, function() + { + this.updateIconState(window, window.document.getElementById("abp-status")); + this.updateIconState(window, window.document.getElementById("abp-toolbarbutton")); + }.bind(this)); + addBrowserClickListener(window, this.onBrowserClick.bind(this, window)); + + window.document.getElementById("abp-notification-close").addEventListener("command", function(event) + { + window.document.getElementById("abp-notification").hidePopup(); + }, false); + + // First-run actions? + this.firstRunActions(window); + + // Some people actually switch off browser.frames.enabled and are surprised + // that things stop working... + window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell) + .allowSubframes = true; + }, + + /** + * Removes Adblock Plus UI from a window. + */ + removeFromWindow: function(/**Window*/ window) + { + let {isKnownWindow, removeBrowserLocationListeners, removeBrowserClickListeners} = require("appSupport"); + if (window.document.documentElement.id == "CustomizeToolbarWindow" || isKnownWindow(window)) + { + // Remove style processing instruction + for (let child = window.document.firstChild; child; child = child.nextSibling) + if (child.nodeType == child.PROCESSING_INSTRUCTION_NODE && child.data.indexOf("adblockplus-node") >= 0) + child.parentNode.removeChild(child); + } + + if (!isKnownWindow(window)) + return; + + for (let id in this.overlay) + { + if (id == "all") + { + let list = this.overlay[id]; + for (let i = 0; i < list.length; i++) + { + let clone = window.document.getElementById(list[i].getAttribute("id")); + if (clone) + clone.parentNode.removeChild(clone); + } + } + else + { + let clone = window.document.getElementById(id); + if (clone) + clone.parentNode.removeChild(clone); + } + } + + window.removeEventListener("popupshowing", this.onPopupShowing, false); + window.removeEventListener("keypress", this.onKeyPress, false); + removeBrowserLocationListeners(window); + removeBrowserClickListeners(window); + }, + + /** + * The overlay information to be used when adding elements to the UI. + * @type Object + */ + overlay: null, + + /** + * Iterator for application windows that Adblock Plus should apply to. + * @type Iterator + */ + get applicationWindows() + { + let {isKnownWindow} = require("appSupport"); + + let enumerator = Services.wm.getZOrderDOMWindowEnumerator(null, true); + if (!enumerator.hasMoreElements()) + { + // On Linux the list returned will be empty, see bug 156333. Fall back to random order. + enumerator = Services.wm.getEnumerator(null); + } + while (enumerator.hasMoreElements()) + { + let window = enumerator.getNext().QueryInterface(Ci.nsIDOMWindow); + if (isKnownWindow(window)) + yield window; + } + }, + + /** + * Returns the top-most application window or null if none exists. + * @type Window + */ + get currentWindow() + { + for (let window of this.applicationWindows) + return window; + return null; + }, + + /** + * Opens a URL in the browser window. If browser window isn't passed as parameter, + * this function attempts to find a browser window. If an event is passed in + * it should be passed in to the browser if possible (will e.g. open a tab in + * background depending on modifiers keys). + */ + loadInBrowser: function(/**String*/ url, /**Window*/ currentWindow, /**Event*/ event) + { + if (!currentWindow) + currentWindow = this.currentWindow; + + let {addTab} = require("appSupport"); + if (currentWindow && addTab) + addTab(currentWindow, url, event); + else + { + let protocolService = Cc["@mozilla.org/uriloader/external-protocol-service;1"].getService(Ci.nsIExternalProtocolService); + protocolService.loadURI(Services.io.newURI(url, null, null), null); + } + }, + + /** + * Opens a pre-defined documentation link in the browser window. This will + * send the UI language to adblockplus.org so that the correct language + * version of the page can be selected. + */ + loadDocLink: function(/**String*/ linkID, /**Window*/ window) + { + let link = Utils.getDocLink(linkID); + this.loadInBrowser(link, window); + }, + + + /** + * Brings up the filter composer dialog to block an item. + */ + blockItem: function(/**Window*/ window, /**Node*/ node, /**RequestEntry*/ item) + { + if (!item) + return; + + window.openDialog("chrome://adblockplus/content/ui/composer.xul", "_blank", "chrome,centerscreen,resizable,dialog=no,dependent", [node], item); + }, + + /** + * Opens filter preferences dialog or focuses an already open dialog. + * @param {Filter} [filter] filter to be selected + */ + openFiltersDialog: function(filter) + { + let existing = Services.wm.getMostRecentWindow("abp:filters"); + if (existing) + { + try + { + existing.focus(); + } catch (e) {} + if (filter) + existing.SubscriptionActions.selectFilter(filter); + } + else + { + Services.ww.openWindow(null, "chrome://adblockplus/content/ui/filters.xul", "_blank", "chrome,centerscreen,resizable,dialog=no", {wrappedJSObject: filter}); + } + }, + + /** + * Opens report wizard for the current page. + */ + openReportDialog: function(/**Window*/ window) + { + let wnd = Services.wm.getMostRecentWindow("abp:sendReport"); + if (wnd) + wnd.focus(); + else + { + let uri = this.getCurrentLocation(window); + if (uri) + { + let {getBrowser} = require("appSupport"); + window.openDialog("chrome://adblockplus/content/ui/sendReport.xul", "_blank", "chrome,centerscreen,resizable=no", getBrowser(window).contentWindow, uri); + } + } + }, + + /** + * Opens our contribution page. + */ + openContributePage: function(/**Window*/ window) + { + this.loadDocLink("contribute", window); + }, + + /** + * Executed on first run, adds a filter subscription and notifies that user + * about that. + */ + addSubscription: function(/**Window*/ window, /**String*/ prevVersion) + { + // Add "acceptable ads" subscription for new users and user updating from old ABP versions. + // Don't add it for users of privacy subscriptions (use a hardcoded list for now). + let addAcceptable = (Services.vc.compare(prevVersion, "2.0") < 0); + let privacySubscriptions = { + "https://easylist-downloads.adblockplus.org/easyprivacy+easylist.txt": true, + "https://easylist-downloads.adblockplus.org/easyprivacy.txt": true, + "https://secure.fanboy.co.nz/fanboy-tracking.txt": true, + "https://fanboy-adblock-list.googlecode.com/hg/fanboy-adblocklist-stats.txt": true, + "https://bitbucket.org/fanboy/fanboyadblock/raw/tip/fanboy-adblocklist-stats.txt": true, + "https://hg01.codeplex.com/fanboyadblock/raw-file/tip/fanboy-adblocklist-stats.txt": true, + "https://adversity.googlecode.com/hg/Adversity-Tracking.txt": true + }; + if (FilterStorage.subscriptions.some((subscription) => subscription.url == Prefs.subscriptions_exceptionsurl || subscription.url in privacySubscriptions)) + addAcceptable = false; + + // Don't add subscription if the user has a subscription already + let addSubscription = true; + //if (FilterStorage.subscriptions.some((subscription) => subscription instanceof DownloadableSubscription && subscription.url != Prefs.subscriptions_exceptionsurl)) + addSubscription = false; + + // If this isn't the first run, only add subscription if the user has no custom filters + if (addSubscription && Services.vc.compare(prevVersion, "0.0") > 0) + { + if (FilterStorage.subscriptions.some((subscription) => subscription.url != Prefs.subscriptions_exceptionsurl && subscription.filters.length)) + addSubscription = false; + } + + // Add "acceptable ads" subscription + if (false) + { + let subscription = Subscription.fromURL(Prefs.subscriptions_exceptionsurl); + if (subscription) + { + subscription.title = "Allow non-intrusive advertising"; + FilterStorage.addSubscription(subscription); + if (subscription instanceof DownloadableSubscription && !subscription.lastDownload) + Synchronizer.execute(subscription); + } + else + addAcceptable = false; + } + + /* Add "anti-adblock messages" subscription for new users and users updating from old ABP versions + if (Services.vc.compare(prevVersion, "2.5") < 0) + { + let subscription = Subscription.fromURL(Prefs.subscriptions_antiadblockurl); + if (subscription && !(subscription.url in FilterStorage.knownSubscriptions)) + { + subscription.disabled = true; + FilterStorage.addSubscription(subscription); + if (subscription instanceof DownloadableSubscription && !subscription.lastDownload) + Synchronizer.execute(subscription); + } + }*/ + + // Extra subsriptions + let subscription = Subscription.fromURL("http://gnuzilla.gnu.org/filters/blacklist.txt"); + subscription.disabled = false; + FilterStorage.addSubscription(subscription); + Synchronizer.execute(subscription); + + let subscription = Subscription.fromURL("http://gnuzilla.gnu.org/filters/third-party.txt"); + subscription.disabled = false; + FilterStorage.addSubscription(subscription); + Synchronizer.execute(subscription); + + let subscription = Subscription.fromURL("http://gnuzilla.gnu.org/filters/javascript.txt"); + subscription.disabled = true; + FilterStorage.addSubscription(subscription); + Synchronizer.execute(subscription); + + if (!addSubscription && !addAcceptable) + return; + + function notifyUser() + {return; + let {addTab} = require("appSupport"); + if (addTab) + { + addTab(window, "chrome://adblockplus/content/ui/firstRun.html"); + } + else + { + let dialogSource = '\ + <?xml-stylesheet href="chrome://global/skin/" type="text/css"?>\ + <dialog xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" onload="document.title=content.document.title" buttons="accept" width="500" height="600">\ + <iframe type="content-primary" flex="1" src="chrome://adblockplus/content/ui/firstRun.html"/>\ + </dialog>'; + Services.ww.openWindow(window, + "data:application/vnd.mozilla.xul+xml," + encodeURIComponent(dialogSource), + "_blank", "chrome,centerscreen,resizable,dialog=no", null); + } + } + + if (addSubscription) + { + // Load subscriptions data + let request = new XMLHttpRequest(); + request.mozBackgroundRequest = true; + request.open("GET", "chrome://adblockplus/content/ui/subscriptions.xml"); + request.addEventListener("load", function() + { + if (onShutdown.done) + return; + + let node = Utils.chooseFilterSubscription(request.responseXML.getElementsByTagName("subscription")); + let subscription = (node ? Subscription.fromURL(node.getAttribute("url")) : null); + if (subscription) + { + FilterStorage.addSubscription(subscription); + subscription.disabled = false; + subscription.title = node.getAttribute("title"); + subscription.homepage = node.getAttribute("homepage"); + if (subscription instanceof DownloadableSubscription && !subscription.lastDownload) + Synchronizer.execute(subscription); + + notifyUser(); + } + }, false); + request.send(); + } + else + notifyUser(); + }, + + /** + * Handles clicks inside the browser's content area, will intercept clicks on + * abp: links. This can be called either with an event object or with the link + * target (if it is the former then link target will be retrieved from event + * target). + */ + onBrowserClick: function (/**Window*/ window, /**Event*/ event, /**String*/ linkTarget) + { + if (event) + { + // Ignore right-clicks + if (event.button == 2) + return; + + // Search the link associated with the click + let link = event.target; + while (link && !(link instanceof Ci.nsIDOMHTMLAnchorElement)) + link = link.parentNode; + + if (!link || link.protocol != "abp:") + return; + + // This is our link - make sure the browser doesn't handle it + event.preventDefault(); + event.stopPropagation(); + + linkTarget = link.href; + } + + let match = /^abp:\/*subscribe\/*\?(.*)/i.exec(linkTarget); + if (!match) + return; + + // Decode URL parameters + let title = null; + let url = null; + let mainSubscriptionTitle = null; + let mainSubscriptionURL = null; + for (let param of match[1].split('&')) + { + let parts = param.split("=", 2); + if (parts.length != 2 || !/\S/.test(parts[1])) + continue; + switch (parts[0]) + { + case "title": + title = decodeURIComponent(parts[1]); + break; + case "location": + url = decodeURIComponent(parts[1]); + break; + case "requiresTitle": + mainSubscriptionTitle = decodeURIComponent(parts[1]); + break; + case "requiresLocation": + mainSubscriptionURL = decodeURIComponent(parts[1]); + break; + } + } + if (!url) + return; + + // Default title to the URL + if (!title) + title = url; + + // Main subscription needs both title and URL + if (mainSubscriptionTitle && !mainSubscriptionURL) + mainSubscriptionTitle = null; + if (mainSubscriptionURL && !mainSubscriptionTitle) + mainSubscriptionURL = null; + + // Trim spaces in title and URL + title = title.replace(/^\s+/, "").replace(/\s+$/, ""); + url = url.replace(/^\s+/, "").replace(/\s+$/, ""); + if (mainSubscriptionURL) + { + mainSubscriptionTitle = mainSubscriptionTitle.replace(/^\s+/, "").replace(/\s+$/, ""); + mainSubscriptionURL = mainSubscriptionURL.replace(/^\s+/, "").replace(/\s+$/, ""); + } + + // Verify that the URL is valid + url = Utils.makeURI(url); + if (!url || (url.scheme != "http" && url.scheme != "https" && url.scheme != "ftp")) + return; + url = url.spec; + + if (mainSubscriptionURL) + { + mainSubscriptionURL = Utils.makeURI(mainSubscriptionURL); + if (!mainSubscriptionURL || (mainSubscriptionURL.scheme != "http" && mainSubscriptionURL.scheme != "https" && mainSubscriptionURL.scheme != "ftp")) + mainSubscriptionURL = mainSubscriptionTitle = null; + else + mainSubscriptionURL = mainSubscriptionURL.spec; + } + + this.openSubscriptionDialog(window, url, title, mainSubscriptionURL, mainSubscriptionTitle); + }, + + /** + * Opens a dialog letting the user confirm/adjust a filter subscription to + * be added. + */ + openSubscriptionDialog: function(/**Window*/ window, /**String*/ url, /**String*/ title, /**String*/ mainURL, /**String*/ mainTitle) + { + let subscription = {url: url, title: title, disabled: false, external: false, + mainSubscriptionTitle: mainTitle, mainSubscriptionURL: mainURL}; + window.openDialog("chrome://adblockplus/content/ui/subscriptionSelection.xul", "_blank", + "chrome,centerscreen,resizable,dialog=no", subscription, null); + }, + + /** + * Retrieves the current location of the browser. + */ + getCurrentLocation: function(/**Window*/ window) /**nsIURI*/ + { + let {getCurrentLocation} = require("appSupport"); + let result = getCurrentLocation(window); + return (result ? Utils.unwrapURL(result) : null); + }, + + /** + * Looks up an element with given ID in the window. If a list of IDs is given + * will try all of them until an element exists. + */ + findElement: function(/**Window*/ window, /**String|String[]*/ id) /**Element*/ + { + if (id instanceof Array) + { + for (let candidate of id) + { + let result = window.document.getElementById(candidate); + if (result) + return result; + } + return null; + } + else + return window.document.getElementById(id); + }, + + /** + * Resolves an insertion point as specified in appSupport module. Returns + * two elements: the parent element and the element to insert before. + */ + resolveInsertionPoint: function(/**Window*/ window, /**Object*/ insertionPoint) /**Element[]*/ + { + let parent = null; + let before = null; + if (insertionPoint) + { + if ("parent" in insertionPoint) + parent = this.findElement(window, insertionPoint.parent); + + if (parent && "before" in insertionPoint) + before = this.findElement(window, insertionPoint.before); + + if (parent && !before && "after" in insertionPoint) + { + let after = this.findElement(window, insertionPoint.after); + if (after) + before = after.nextElementSibling; + } + + if (before && before.parentNode != parent) + before = null; + } + + return [parent, before]; + }, + + /** + * Toggles visibility state of the toolbar icon. + */ + toggleToolbarIcon: function() + { + if (!CustomizableUI) + return; + if (this.isToolbarIconVisible()) + CustomizableUI.removeWidgetFromArea("abp-toolbarbutton"); + else + { + let {defaultToolbarPosition} = require("appSupport"); + CustomizableUI.addWidgetToArea("abp-toolbarbutton", defaultToolbarPosition.parent); + } + }, + + /** + * Updates Adblock Plus icon state for all windows. + */ + updateState: function() + { + for (let window in this.applicationWindows) + { + this.updateIconState(window, window.document.getElementById("abp-status")); + this.updateIconState(window, window.document.getElementById("abp-toolbarbutton")); + } + }, + + /** + * Updates Adblock Plus icon state for a single application window. + */ + updateIconState: function(/**Window*/ window, /**Element*/ icon) + { + if (!icon) + return; + + let state = (Prefs.enabled ? "active" : "disabled"); + if (state == "active") + { + let location = this.getCurrentLocation(window); + if (location && Policy.isWhitelisted(location.spec)) + state = "whitelisted"; + } + + let popupId = "abp-status-popup"; + if (icon.localName == "statusbarpanel") + { + if (Prefs.defaultstatusbaraction == 0) + { + icon.setAttribute("popup", popupId); + icon.removeAttribute("context"); + } + else + { + icon.removeAttribute("popup"); + icon.setAttribute("context", popupId); + } + } + else + { + if (Prefs.defaulttoolbaraction == 0) + { + icon.setAttribute("type", "menu"); + icon.removeAttribute("context"); + } + else + { + icon.setAttribute("type", "menu-button"); + icon.setAttribute("context", popupId); + } + } + + icon.setAttribute("abpstate", state); + }, + + /** + * Shows or hides status bar icons in all windows, according to pref. + */ + updateStatusbarIcon: function(/**Window*/ window) + { + if (!("abp-status" in this.overlay)) + return; + + let {statusbarPosition} = require("appSupport"); + if (!statusbarPosition) + return; + + let icon = window.document.getElementById("abp-status"); + if (Prefs.showinstatusbar && !icon) + { + let [parent, before] = this.resolveInsertionPoint(window, statusbarPosition); + if (!parent) + return; + + parent.insertBefore(this.overlay["abp-status"].cloneNode(true), before); + + icon = window.document.getElementById("abp-status"); + this.updateIconState(window, icon); + icon.addEventListener("click", this.onIconClick, false); + } + else if (!Prefs.showinstatusbar && icon) + icon.parentNode.removeChild(icon); + }, + + /** + * Toggles the value of a boolean preference. + */ + togglePref: function(/**String*/ pref) + { + Prefs[pref] = !Prefs[pref]; + }, + + /** + * If the given filter is already in user's list, removes it from the list. Otherwise adds it. + */ + toggleFilter: function(/**Filter*/ filter) + { + if (filter.subscriptions.length) + { + if (filter.disabled || filter.subscriptions.some((subscription) => !(subscription instanceof SpecialSubscription))) + filter.disabled = !filter.disabled; + else + FilterStorage.removeFilter(filter); + } + else + FilterStorage.addFilter(filter); + }, + + + /** + * Toggles "Count filter hits" option. + */ + toggleSaveStats: function(window) + { + if (Prefs.savestats) + { + if (!Utils.confirm(window, Utils.getString("clearStats_warning"))) + return; + + FilterStorage.resetHitCounts(); + Prefs.savestats = false; + } + else + Prefs.savestats = true; + }, + + /** + * Sets the current filter subscription in a single-subscription scenario, + * all other subscriptions will be removed. + */ + setSubscription: function(url, title) + { + let subscription = Subscription.fromURL(url); + let currentSubscriptions = FilterStorage.subscriptions.filter( + ((subscription) => subscription instanceof DownloadableSubscription && subscription.url != Prefs.subscriptions_exceptionsurl) + ); + if (!subscription || currentSubscriptions.indexOf(subscription) >= 0) + return; + + for (let i = 0; i < currentSubscriptions.length; i++) + FilterStorage.removeSubscription(currentSubscriptions[i]); + + subscription.title = title; + FilterStorage.addSubscription(subscription); + if (subscription instanceof DownloadableSubscription && !subscription.lastDownload) + Synchronizer.execute(subscription); + }, + + /** + * Adds or removes "non-intrisive ads" filter list. + * @return {Boolean} true if the filter list has been added + **/ + toggleAcceptableAds: function() + { + let subscription = Subscription.fromURL(Prefs.subscriptions_exceptionsurl); + if (!subscription) + return false; + + subscription.disabled = false; + subscription.title = "Allow non-intrusive advertising"; + if (subscription.url in FilterStorage.knownSubscriptions) + FilterStorage.removeSubscription(subscription); + else + { + FilterStorage.addSubscription(subscription); + if (subscription instanceof DownloadableSubscription && !subscription.lastDownload) + Synchronizer.execute(subscription); + } + + return (subscription.url in FilterStorage.knownSubscriptions); + }, + + /** + * Toggles the pref for the Adblock Plus sync engine. + * @return {Boolean} new state of the sync engine + */ + toggleSync: function() + { + let {Sync} = require("sync"); + let syncEngine = Sync.getEngine(); + if (syncEngine) + { + syncEngine.enabled = !syncEngine.enabled; + return syncEngine.enabled; + } + else + return false; + }, + + /** + * Tests whether blockable items list is currently open. + */ + isBottombarOpen: function(/**Window*/ window) /**Boolean*/ + { + if (detachedBottombar && !detachedBottombar.closed) + return true; + + return !!window.document.getElementById("abp-bottombar"); + }, + + /** + * Called when some pop-up in the application window shows up, initializes + * pop-ups related to Adblock Plus. + */ + onPopupShowing: function(/**Event*/ event) + { + if (event.defaultPrevented) + return; + + let popup = event.originalTarget; + + let {contentContextMenu} = require("appSupport"); + if ((typeof contentContextMenu == "string" && popup.id == contentContextMenu) || + (contentContextMenu instanceof Array && contentContextMenu.indexOf(popup.id) >= 0)) + { + this.fillContentContextMenu(popup); + } + else if (popup.id == "abp-tooltip") + this.fillIconTooltip(event, popup.ownerDocument.defaultView); + else + { + let match = /^(abp-(?:toolbar|status|menuitem)-)popup$/.exec(popup.id); + if (match) + this.fillIconMenu(event, popup.ownerDocument.defaultView, match[1]); + } + }, + + /** + * Handles click on toolbar and status bar icons. + */ + onIconClick: function(/**Event*/ event) + { + if (event.eventPhase != event.AT_TARGET) + return; + + let isToolbar = (event.target.localName != "statusbarpanel"); + let action = 0; + if ((isToolbar && event.type == "command") || (!isToolbar && event.button == 0)) + action = (isToolbar ? Prefs.defaulttoolbaraction : Prefs.defaultstatusbaraction); + else if (event.button == 1) + action = 3; + + let window = event.target.ownerDocument.defaultView; + if (action == 1) + this.toggleBottombar(window); + else if (action == 2) + this.openFiltersDialog(); + else if (action == 3) + { + // If there is a whitelisting rule for current page - remove it (reenable). + // Otherwise flip "enabled" pref. + if (!this.removeWhitelist(window)) + this.togglePref("enabled"); + } + }, + + /** + * Removes/disables the exception rule applying for the current page. + */ + removeWhitelist: function(/**Window*/ window) + { + let location = this.getCurrentLocation(window); + let filter = null; + if (location) + filter = Policy.isWhitelisted(location.spec); + if (filter && filter.subscriptions.length && !filter.disabled) + { + UI.toggleFilter(filter); + return true; + } + return false; + }, + + /** + * Updates state of the icon tooltip. + */ + fillIconTooltip: function(/**Event*/ event, /**Window*/ window) + { + let E = (id) => window.document.getElementById(id); + + let node = window.document.tooltipNode; + if (!node || !node.hasAttribute("tooltip")) + { + event.preventDefault(); + return; + } + + // Prevent tooltip from overlapping menu + for (let id of ["abp-toolbar-popup", "abp-status-popup"]) + { + let element = E(id); + if (element && element.state == "open") + { + event.preventDefault(); + return; + } + } + + let type = (node.id == "abp-toolbarbutton" ? "toolbar" : "statusbar"); + let action = parseInt(Prefs["default" + type + "action"]); + if (isNaN(action)) + action = -1; + + let actionDescr = E("abp-tooltip-action"); + actionDescr.hidden = (action < 0 || action > 3); + if (!actionDescr.hidden) + actionDescr.setAttribute("value", Utils.getString("action" + action + "_tooltip")); + + let statusDescr = E("abp-tooltip-status"); + let state = node.getAttribute("abpstate"); + let statusStr = Utils.getString(state + "_tooltip"); + if (state == "active") + { + let [activeSubscriptions, activeFilters] = FilterStorage.subscriptions.reduce(function([subscriptions, filters], current) + { + if (current instanceof SpecialSubscription) + return [subscriptions, filters + current.filters.filter((filter) => !filter.disabled).length]; + else if (!current.disabled && !(Prefs.subscriptions_exceptionscheckbox && current.url == Prefs.subscriptions_exceptionsurl)) + return [subscriptions + 1, filters]; + else + return [subscriptions, filters] + }, [0, 0]); + + statusStr = statusStr.replace(/\?1\?/, activeSubscriptions).replace(/\?2\?/, activeFilters); + } + statusDescr.setAttribute("value", statusStr); + + let activeFilters = []; + E("abp-tooltip-blocked-label").hidden = (state != "active"); + E("abp-tooltip-blocked").hidden = (state != "active"); + if (state == "active") + { + let {getBrowser} = require("appSupport"); + let stats = RequestNotifier.getWindowStatistics(getBrowser(window).contentWindow); + + let blockedStr = Utils.getString("blocked_count_tooltip"); + blockedStr = blockedStr.replace(/\?1\?/, stats ? stats.blocked : 0).replace(/\?2\?/, stats ? stats.items : 0); + + if (stats && stats.whitelisted + stats.hidden) + { + blockedStr += " " + Utils.getString("blocked_count_addendum"); + blockedStr = blockedStr.replace(/\?1\?/, stats.whitelisted).replace(/\?2\?/, stats.hidden); + } + + E("abp-tooltip-blocked").setAttribute("value", blockedStr); + + if (stats) + { + let filterSort = function(a, b) + { + return stats.filters[b] - stats.filters[a]; + }; + for (let filter in stats.filters) + activeFilters.push(filter); + activeFilters = activeFilters.sort(filterSort); + } + + if (activeFilters.length > 0) + { + let filtersContainer = E("abp-tooltip-filters"); + while (filtersContainer.firstChild) + filtersContainer.removeChild(filtersContainer.firstChild); + + for (let i = 0; i < activeFilters.length && i < 3; i++) + { + let descr = filtersContainer.ownerDocument.createElement("description"); + descr.setAttribute("value", activeFilters[i] + " (" + stats.filters[activeFilters[i]] + ")"); + filtersContainer.appendChild(descr); + } + } + } + + E("abp-tooltip-filters-label").hidden = (activeFilters.length == 0); + E("abp-tooltip-filters").hidden = (activeFilters.length == 0); + E("abp-tooltip-more-filters").hidden = (activeFilters.length <= 3); + }, + + /** + * Updates state of the icon context menu. + */ + fillIconMenu: function(/**Event*/ event, /**Window*/ window, /**String*/ prefix) + { + function hideElement(id, hide) + { + let element = window.document.getElementById(id); + if (element) + element.hidden = hide; + } + function setChecked(id, checked) + { + let element = window.document.getElementById(id); + if (element) + element.setAttribute("checked", checked); + } + function setDisabled(id, disabled) + { + let element = window.document.getElementById(id); + if (element) + element.setAttribute("disabled", disabled); + } + function setDefault(id, isDefault) + { + let element = window.document.getElementById(id); + if (element) + element.setAttribute("default", isDefault); + } + function generateLabel(id, param) + { + let element = window.document.getElementById(id); + if (element) + element.setAttribute("label", element.getAttribute("labeltempl").replace(/\?1\?/, param)); + } + + let bottombarOpen = this.isBottombarOpen(window); + hideElement(prefix + "openbottombar", bottombarOpen); + hideElement(prefix + "closebottombar", !bottombarOpen); + + hideElement(prefix + "whitelistsite", true); + hideElement(prefix + "whitelistpage", true); + + let location = this.getCurrentLocation(window); + if (location && Policy.isBlockableScheme(location)) + { + let host = null; + try + { + host = location.host.replace(/^www\./, ""); + } catch (e) {} + + if (host) + { + let ending = "|"; + location = location.clone(); + if (location instanceof Ci.nsIURL) + location.ref = ""; + if (location instanceof Ci.nsIURL && location.query) + { + location.query = ""; + ending = "?"; + } + + siteWhitelist = Filter.fromText("@@||" + host + "^$document"); + setChecked(prefix + "whitelistsite", siteWhitelist.subscriptions.length && !siteWhitelist.disabled); + generateLabel(prefix + "whitelistsite", host); + hideElement(prefix + "whitelistsite", false); + + pageWhitelist = Filter.fromText("@@|" + location.spec + ending + "$document"); + setChecked(prefix + "whitelistpage", pageWhitelist.subscriptions.length && !pageWhitelist.disabled); + hideElement(prefix + "whitelistpage", false); + } + else + { + siteWhitelist = Filter.fromText("@@|" + location.spec + "|"); + setChecked(prefix + "whitelistsite", siteWhitelist.subscriptions.length && !siteWhitelist.disabled); + generateLabel(prefix + "whitelistsite", location.spec.replace(/^mailto:/, "")); + hideElement(prefix + "whitelistsite", false); + } + } + + setDisabled("abp-command-sendReport", !location || !Policy.isBlockableScheme(location) || location.scheme == "mailto"); + + setChecked(prefix + "disabled", !Prefs.enabled); + setChecked(prefix + "frameobjects", Prefs.frameobjects); + setChecked(prefix + "slowcollapse", !Prefs.fastcollapse); + setChecked(prefix + "savestats", Prefs.savestats); + + let {defaultToolbarPosition, statusbarPosition} = require("appSupport"); + let hasToolbar = defaultToolbarPosition; + let hasStatusBar = statusbarPosition; + hideElement(prefix + "showintoolbar", !hasToolbar || prefix == "abp-toolbar-"); + hideElement(prefix + "showinstatusbar", !hasStatusBar); + hideElement(prefix + "iconSettingsSeparator", (prefix == "abp-toolbar-" || !hasToolbar) && !hasStatusBar); + + setChecked(prefix + "showintoolbar", this.isToolbarIconVisible()); + setChecked(prefix + "showinstatusbar", Prefs.showinstatusbar); + + let {Sync} = require("sync"); + let syncEngine = Sync.getEngine(); + hideElement(prefix + "sync", !syncEngine); + setChecked(prefix + "sync", syncEngine && syncEngine.enabled); + + let defAction = (!window.document.popupNode || window.document.popupNode.id == "abp-toolbarbutton" ? + Prefs.defaulttoolbaraction : + Prefs.defaultstatusbaraction); + setDefault(prefix + "openbottombar", defAction == 1); + setDefault(prefix + "closebottombar", defAction == 1); + setDefault(prefix + "filters", defAction == 2); + setDefault(prefix + "disabled", defAction == 3); + + let popup = window.document.getElementById(prefix + "popup"); + let items = (popup ? popup.querySelectorAll('menuitem[key]') : []); + for (let i = 0; i < items.length; i++) + { + let item = items[i]; + let match = /^abp-key-/.exec(item.getAttribute("key")); + if (!match) + continue; + + let name = match.input.substr(match.index + match[0].length); + if (!this.hotkeys) + this.configureKeys(window); + if (name in this.hotkeys) + { + let text = KeySelector.getTextForKey(this.hotkeys[name]); + if (text) + item.setAttribute("acceltext", text); + else + item.removeAttribute("acceltext"); + } + } + + hideElement(prefix + "contributebutton", Prefs.hideContributeButton); + }, + + /** + * Adds Adblock Plus menu items to the content area context menu when it shows + * up. + */ + fillContentContextMenu: function(/**Element*/ popup) + { + let target = popup.triggerNode; + if (target instanceof Ci.nsIDOMHTMLMapElement || target instanceof Ci.nsIDOMHTMLAreaElement) + { + // HTML image maps will usually receive events when the mouse pointer is + // over a different element, get the real event target. + let rect = target.getClientRects()[0]; + target = target.ownerDocument.elementFromPoint(Math.max(rect.left, 0), Math.max(rect.top, 0)); + } + + if (!target) + return; + + let window = popup.ownerDocument.defaultView; + let menuItems = []; + let addMenuItem = function([node, nodeData]) + { + let type = nodeData.typeDescr.toLowerCase(); + if (type == "background") + { + type = "image"; + node = null; + } + + let label = this.overlay.attributes[type + "contextlabel"]; + if (!label) + return; + + let item = popup.ownerDocument.createElement("menuitem"); + item.setAttribute("label", label); + item.setAttribute("class", "abp-contextmenuitem"); + item.addEventListener("command", this.blockItem.bind(this, window, node, nodeData), false); + popup.appendChild(item); + + menuItems.push(item); + }.bind(this); + + // Look up data that we have for the node + let data = RequestNotifier.getDataForNode(target); + let hadImage = false; + if (data && !data[1].filter) + { + addMenuItem(data); + hadImage = (data[1].typeDescr == "IMAGE"); + } + + // Look for frame data + let wnd = Utils.getWindow(target); + if (wnd.frameElement) + { + let data = RequestNotifier.getDataForNode(wnd.frameElement, true); + if (data && !data[1].filter) + addMenuItem(data); + } + + // Look for a background image + if (!hadImage) + { + let extractImageURL = function(computedStyle, property) + { + let value = computedStyle.getPropertyCSSValue(property); + // CSSValueList + if ("length" in value && value.length >= 1) + value = value[0]; + // CSSValuePrimitiveType + if ("primitiveType" in value && value.primitiveType == value.CSS_URI) + return Utils.unwrapURL(value.getStringValue()).spec; + + return null; + }; + + let node = target; + while (node) + { + if (node.nodeType == Ci.nsIDOMNode.ELEMENT_NODE) + { + let style = wnd.getComputedStyle(node, ""); + let bgImage = extractImageURL(style, "background-image") || extractImageURL(style, "list-style-image"); + if (bgImage) + { + let data = RequestNotifier.getDataForNode(wnd.document, true, Policy.type.IMAGE, bgImage); + if (data && !data[1].filter) + { + addMenuItem(data); + break; + } + } + } + + node = node.parentNode; + } + } + + // Add "Remove exception" menu item if necessary + let location = this.getCurrentLocation(window); + let filter = (location ? Policy.isWhitelisted(location.spec) : null); + if (filter && filter.subscriptions.length && !filter.disabled) + { + let label = this.overlay.attributes.whitelistcontextlabel; + if (!label) + return; + + let item = popup.ownerDocument.createElement("menuitem"); + item.setAttribute("label", label); + item.setAttribute("class", "abp-contextmenuitem"); + item.addEventListener("command", this.toggleFilter.bind(this, filter), false); + popup.appendChild(item); + + menuItems.push(item); + } + + // Make sure to clean up everything once the context menu is closed + if (menuItems.length) + { + let cleanUp = function(event) + { + if (event.eventPhase != event.AT_TARGET) + return; + + popup.removeEventListener("popuphidden", cleanUp, false); + for (let i = 0; i < menuItems.length; i++) + if (menuItems[i].parentNode) + menuItems[i].parentNode.removeChild(menuItems[i]); + }.bind(this); + popup.addEventListener("popuphidden", cleanUp, false); + } + }, + + /** + * Called when the user presses a key in the application window, reacts to our + * shortcut keys. + */ + onKeyPress: function(/**Event*/ event) + { + if (!this.hotkeys) + this.configureKeys(event.currentTarget); + + for (let key in this.hotkeys) + { + if (KeySelector.matchesKey(event, this.hotkeys[key])) + { + event.preventDefault(); + let command = event.currentTarget.document.getElementById("abp-command-" + key); + if (command) + command.doCommand(); + } + } + }, + + /** + * Checks whether the toolbar icon is currently displayed. + */ + isToolbarIconVisible: function() /**Boolean*/ + { + if (!CustomizableUI) + return false; + let placement = CustomizableUI.getPlacementOfWidget("abp-toolbarbutton"); + return !!placement; + }, + + /** + * Stores the selected hotkeys, initialized when the user presses a key. + */ + hotkeys: null, + + /** + * Chooses shortcut keys that are available in the window according to + * preferences. + */ + configureKeys: function(/**Window*/ window) + { + let selector = new KeySelector(window); + + this.hotkeys = {}; + for (let name in Prefs) + { + let match = /_key$/.exec(name); + if (match && typeof Prefs[name] == "string") + { + let keyName = match.input.substr(0, match.index); + this.hotkeys[keyName] = selector.selectKey(Prefs[name]); + } + } + }, + + /** + * Toggles open/closed state of the blockable items list. + */ + toggleBottombar: function(/**Window*/ window) + { + if (detachedBottombar && !detachedBottombar.closed) + { + detachedBottombar.close(); + detachedBottombar = null; + } + else + { + let {addBottomBar, removeBottomBar, getBrowser} = require("appSupport"); + let mustDetach = !addBottomBar || !removeBottomBar || !("abp-bottombar-container" in this.overlay); + let detach = mustDetach || Prefs.detachsidebar; + if (!detach && window.document.getElementById("abp-bottombar")) + { + removeBottomBar(window); + + let browser = (getBrowser ? getBrowser(window) : null); + if (browser) + browser.contentWindow.focus(); + } + else if (!detach) + { + addBottomBar(window, this.overlay["abp-bottombar-container"]); + let element = window.document.getElementById("abp-bottombar"); + if (element) + { + element.setAttribute("width", Prefs.blockableItemsSize.width); + element.setAttribute("height", Prefs.blockableItemsSize.height); + + let splitter = window.document.getElementById("abp-bottombar-splitter"); + if (splitter) + { + splitter.addEventListener("command", function() + { + Prefs.blockableItemsSize = {width: element.width, height: element.height}; + }, false); + } + } + } + else + detachedBottombar = window.openDialog("chrome://adblockplus/content/ui/sidebarDetached.xul", "_blank", "chrome,resizable,dependent,dialog=no", mustDetach); + } + }, + + /** + * Hide contribute button and persist this choice. + */ + hideContributeButton: function(/**Window*/ window) + { + Prefs.hideContributeButton = true; + + for (let id of ["abp-status-contributebutton", "abp-toolbar-contributebutton", "abp-menuitem-contributebutton"]) + { + let button = window.document.getElementById(id); + if (button) + button.hidden = true; + } + }, + + showNextNotification: function(url) + { + let window = this.currentWindow; + if (!window) + return; + + let button = window.document.getElementById("abp-toolbarbutton") + || window.document.getElementById("abp-status"); + if (!button) + return; + + let notification = Notification.getNextToShow(url); + if (!notification) + return; + + this._showNotification(window, button, notification); + }, + + _showNotification: function(window, button, notification) + { + let panel = window.document.getElementById("abp-notification"); + if (panel.state !== "closed") + return; + + function insertMessage(element, text, links) + { + let match = /^(.*?)<(a|strong)>(.*?)<\/\2>(.*)$/.exec(text); + if (!match) + { + element.appendChild(window.document.createTextNode(text)); + return; + } + + let [_, before, tagName, value, after] = match; + + insertMessage(element, before, links); + + let newElement = window.document.createElementNS("http://www.w3.org/1999/xhtml", tagName); + if (tagName === "a" && links && links.length) + newElement.setAttribute("href", links.shift()); + insertMessage(newElement, value, links); + element.appendChild(newElement); + + insertMessage(element, after, links); + } + + let texts = Notification.getLocalizedTexts(notification); + let titleElement = window.document.getElementById("abp-notification-title"); + titleElement.textContent = texts.title; + let messageElement = window.document.getElementById("abp-notification-message"); + messageElement.innerHTML = ""; + let docLinks = []; + for (let link of notification.links) + docLinks.push(Utils.getDocLink(link)); + insertMessage(messageElement, texts.message, docLinks); + + messageElement.addEventListener("click", function(event) + { + let link = event.target; + while (link && link !== messageElement && link.localName !== "a") + link = link.parentNode; + if (!link || link.localName !== "a") + return; + event.preventDefault(); + event.stopPropagation(); + this.loadInBrowser(link.href, window); + }.bind(this)); + + if (notification.type === "question") + { + function buttonHandler(approved, event) + { + event.preventDefault(); + event.stopPropagation(); + panel.hidePopup(); + Notification.triggerQuestionListeners(notification.id, approved) + Notification.markAsShown(notification.id); + } + window.document.getElementById("abp-notification-yes").onclick = buttonHandler.bind(null, true); + window.document.getElementById("abp-notification-no").onclick = buttonHandler.bind(null, false); + } + + panel.setAttribute("class", "abp-" + notification.type); + panel.setAttribute("noautohide", notification.type === "question"); + panel.openPopup(button, "bottomcenter topcenter", 0, 0, false, false, null); + } +}; +UI.onPopupShowing = UI.onPopupShowing.bind(UI); +UI.onKeyPress = UI.onKeyPress.bind(UI); +UI.onIconClick = UI.onIconClick.bind(UI); +UI.init(); + +/** + * List of event handers to be registered for each window. For each event + * handler the element ID, event and the actual event handler are listed. + * @type Array + */ +let eventHandlers = [ + ["abp-command-sendReport", "command", UI.openReportDialog.bind(UI)], + ["abp-command-filters", "command", UI.openFiltersDialog.bind(UI)], + ["abp-command-sidebar", "command", UI.toggleBottombar.bind(UI)], + ["abp-command-togglesitewhitelist", "command", function() { UI.toggleFilter(siteWhitelist); }], + ["abp-command-togglepagewhitelist", "command", function() { UI.toggleFilter(pageWhitelist); }], + ["abp-command-toggleobjtabs", "command", UI.togglePref.bind(UI, "frameobjects")], + ["abp-command-togglecollapse", "command", UI.togglePref.bind(UI, "fastcollapse")], + ["abp-command-togglesavestats", "command", UI.toggleSaveStats.bind(UI)], + ["abp-command-togglesync", "command", UI.toggleSync.bind(UI)], + ["abp-command-toggleshowintoolbar", "command", UI.toggleToolbarIcon.bind(UI)], + ["abp-command-toggleshowinstatusbar", "command", UI.togglePref.bind(UI, "showinstatusbar")], + ["abp-command-enable", "command", UI.togglePref.bind(UI, "enabled")], + ["abp-command-contribute", "command", UI.openContributePage.bind(UI)], + ["abp-command-contribute-hide", "command", UI.hideContributeButton.bind(UI)] +]; + +onShutdown.add(function() +{ + for (let window in UI.applicationWindows) + if (UI.isBottombarOpen(window)) + UI.toggleBottombar(window); +}); |