/* * This file is part of Adblock Plus , * 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 . */ 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 = '\ \ \