diff options
Diffstat (limited to 'data/extensions/spyblock@gnu.org/lib')
27 files changed, 12382 insertions, 0 deletions
diff --git a/data/extensions/spyblock@gnu.org/lib/Public.jsm b/data/extensions/spyblock@gnu.org/lib/Public.jsm new file mode 100644 index 0000000..0f96bcb --- /dev/null +++ b/data/extensions/spyblock@gnu.org/lib/Public.jsm @@ -0,0 +1,202 @@ +/* + * 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/>. + */ + +/** + * @fileOverview Public Adblock Plus API. + */ + +var EXPORTED_SYMBOLS = ["AdblockPlus"]; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +Cu.import("resource://gre/modules/Services.jsm"); + +function require(module) +{ + let result = {}; + result.wrappedJSObject = result; + Services.obs.notifyObservers(result, "adblockplus-require", module); + return result.exports; +} + +let {FilterStorage} = require("filterStorage"); +let {Filter} = require("filterClasses"); +let {Subscription, SpecialSubscription, RegularSubscription, DownloadableSubscription, ExternalSubscription} = require("subscriptionClasses"); + +const externalPrefix = "~external~"; + +/** + * Class implementing public Adblock Plus API + * @class + */ +var AdblockPlus = +{ + /** + * Returns current subscription count + * @type Integer + */ + get subscriptionCount() + { + return FilterStorage.subscriptions.length; + }, + + /** + * Gets a subscription by its URL + */ + getSubscription: function(/**String*/ id) /**IAdblockPlusSubscription*/ + { + if (id in FilterStorage.knownSubscriptions) + return createSubscriptionWrapper(FilterStorage.knownSubscriptions[id]); + + return null; + }, + + /** + * Gets a subscription by its position in the list + */ + getSubscriptionAt: function(/**Integer*/ index) /**IAdblockPlusSubscription*/ + { + if (index < 0 || index >= FilterStorage.subscriptions.length) + return null; + + return createSubscriptionWrapper(FilterStorage.subscriptions[index]); + }, + + /** + * Updates an external subscription and creates it if necessary + */ + updateExternalSubscription: function(/**String*/ id, /**String*/ title, /**Array of Filter*/ filters) /**String*/ + { + if (id.substr(0, externalPrefix.length) != externalPrefix) + id = externalPrefix + id; + let subscription = Subscription.knownSubscriptions[id]; + if (typeof subscription == "undefined") + subscription = new ExternalSubscription(id, title); + + subscription.lastDownload = parseInt(new Date().getTime() / 1000); + + let newFilters = []; + for (let filter of filters) + { + filter = Filter.fromText(Filter.normalize(filter)); + if (filter) + newFilters.push(filter); + } + + if (id in FilterStorage.knownSubscriptions) + FilterStorage.updateSubscriptionFilters(subscription, newFilters); + else + { + subscription.filters = newFilters; + FilterStorage.addSubscription(subscription); + } + + return id; + }, + + /** + * Removes an external subscription by its identifier + */ + removeExternalSubscription: function(/**String*/ id) /**Boolean*/ + { + if (id.substr(0, externalPrefix.length) != externalPrefix) + id = externalPrefix + id; + if (!(id in FilterStorage.knownSubscriptions)) + return false; + + FilterStorage.removeSubscription(FilterStorage.knownSubscriptions[id]); + return true; + }, + + /** + * Adds user-defined filters to the list + */ + addPatterns: function(/**Array of String*/ filters) + { + for (let filter of filters) + { + filter = Filter.fromText(Filter.normalize(filter)); + if (filter) + { + filter.disabled = false; + FilterStorage.addFilter(filter); + } + } + }, + + /** + * Removes user-defined filters from the list + */ + removePatterns: function(/**Array of String*/ filters) + { + for (let filter of filters) + { + filter = Filter.fromText(Filter.normalize(filter)); + if (filter) + FilterStorage.removeFilter(filter); + } + }, + + /** + * Returns installed Adblock Plus version + */ + getInstalledVersion: function() /**String*/ + { + return require("info").addonVersion; + }, + + /** + * Returns source code revision this Adblock Plus build was created from (if available) + */ + getInstalledBuild: function() /**String*/ + { + return ""; + }, +}; + +/** + * Wraps a subscription into IAdblockPlusSubscription structure. + */ +function createSubscriptionWrapper(/**Subscription*/ subscription) /**IAdblockPlusSubscription*/ +{ + if (!subscription) + return null; + + return { + url: subscription.url, + special: subscription instanceof SpecialSubscription, + title: subscription.title, + autoDownload: true, + disabled: subscription.disabled, + external: subscription instanceof ExternalSubscription, + lastDownload: subscription instanceof RegularSubscription ? subscription.lastDownload : 0, + downloadStatus: subscription instanceof DownloadableSubscription ? subscription.downloadStatus : "synchronize_ok", + lastModified: subscription instanceof DownloadableSubscription ? subscription.lastModified : null, + expires: subscription instanceof DownloadableSubscription ? subscription.expires : 0, + getPatterns: function() + { + let result = subscription.filters.map(function(filter) + { + return filter.text; + }); + return result; + } + }; +} diff --git a/data/extensions/spyblock@gnu.org/lib/antiadblockInit.js b/data/extensions/spyblock@gnu.org/lib/antiadblockInit.js new file mode 100644 index 0000000..d8b29ca --- /dev/null +++ b/data/extensions/spyblock@gnu.org/lib/antiadblockInit.js @@ -0,0 +1,78 @@ +/* + * 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/Services.jsm"); + +let {Utils} = require("utils"); +let {Prefs} = require("prefs"); +let {ActiveFilter} = require("filterClasses"); +let {FilterStorage} = require("filterStorage"); +let {FilterNotifier} = require("filterNotifier"); +let {Subscription} = require("subscriptionClasses"); +let {Notification} = require("notification"); + +exports.initAntiAdblockNotification = function initAntiAdblockNotification() +{ + let notification = { + id: "antiadblock", + type: "question", + title: Utils.getString("notification_antiadblock_title"), + message: Utils.getString("notification_antiadblock_message"), + urlFilters: [] + }; + + function notificationListener(approved) + { + let subscription = Subscription.fromURL(Prefs.subscriptions_antiadblockurl); + if (subscription.url in FilterStorage.knownSubscriptions) + subscription.disabled = !approved; + } + + function addAntiAdblockNotification(subscription) + { + let urlFilters = []; + for (let filter of subscription.filters) + if (filter instanceof ActiveFilter) + for (let domain in filter.domains) + if (domain && urlFilters.indexOf(domain) == -1) + urlFilters.push(domain); + notification.urlFilters = urlFilters; + Notification.addNotification(notification); + Notification.addQuestionListener(notification.id, notificationListener); + } + + function removeAntiAdblockNotification() + { + Notification.removeNotification(notification); + Notification.removeQuestionListener(notification.id, notificationListener); + } + + let subscription = Subscription.fromURL(Prefs.subscriptions_antiadblockurl); + if (subscription.lastDownload && subscription.disabled) + addAntiAdblockNotification(subscription); + + FilterNotifier.addListener(function(action, value, newItem, oldItem) + { + if (!/^subscription\.(updated|removed|disabled)$/.test(action) || value.url != Prefs.subscriptions_antiadblockurl) + return; + + if (action == "subscription.updated") + addAntiAdblockNotification(value); + else if (action == "subscription.removed" || (action == "subscription.disabled" && !value.disabled)) + removeAntiAdblockNotification(); + }); +} diff --git a/data/extensions/spyblock@gnu.org/lib/appSupport.js b/data/extensions/spyblock@gnu.org/lib/appSupport.js new file mode 100644 index 0000000..67c6248 --- /dev/null +++ b/data/extensions/spyblock@gnu.org/lib/appSupport.js @@ -0,0 +1,948 @@ +/* + * 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/>. + */ + +/** + * @fileOverview Various application-specific functions. + */ + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/AddonManager.jsm"); + +/** + * Checks whether an application window is known and should get Adblock Plus + * user interface elements. + * @result Boolean + */ +exports.isKnownWindow = (/**Window*/ window) => false; + +/** + * HACK: In some applications the window finishes initialization during load + * event processing which makes an additional delay necessary. This flag + * indicates that. + * @type Boolean + */ +exports.delayInitialization = false; + +/** + * Retrieves the browser element for an application window. + * @type function(window) + */ +exports.getBrowser = null; + +/** + * Adds a new browser tab in the given application window. + * @type function(window, url, event) + */ +exports.addTab = null; + +/** + * Retrieves the current browser location for an application window. + */ +exports.getCurrentLocation = function getCurrentLocation(/**Window*/ window) /**nsIURI|String*/ +{ + let browser = (exports.getBrowser ? exports.getBrowser(window) : null); + return (browser ? browser.currentURI : null); +} + + +/** + * The ID (or a list of possible IDs) of the content area context menu. + * @type String|String[] + */ +exports.contentContextMenu = null; + +/** + * Determines the default placement of the toolbar icon via object properties + * parent, before and after. + * @type Object + */ +exports.defaultToolbarPosition = null; + +/** + * The properties parent, before, after determine the placement of the status + * bar icon. + * @type Object + */ +exports.statusbarPosition = null; + +/** + * The properties parent, before, after determine the placement of the Tools + * submenu. + * @type Object + */ +exports.toolsMenu = null; + +/** + * Maps windows to their bottom bar info. + */ +let bottomBars = new WeakMap(); + +/** + * Adds a bottom bar to the application window. + * @type function(window, element) + */ +exports.addBottomBar = null; + +function _addBottomBar(window, parent, element) +{ + if (bottomBars.has(window) || !parent) + return null; + + let bar = {elements: []}; + for (let child = element.firstElementChild; child; child = child.nextElementSibling) + { + let clone = child.cloneNode(true); + parent.appendChild(clone); + bar.elements.push(clone); + } + + bottomBars.set(window, bar); + return bar; +}; + +/** + * Removes the bottom bar from the application window. + * @type function(window) + */ +exports.removeBottomBar = null; + +function _removeBottomBar(window) +{ + if (!bottomBars.has(window)) + return null; + + let bar = bottomBars.get(window); + for (let i = 0; i < bar.elements.length; i++) + if (bar.elements[i].parentNode) + bar.elements[i].parentNode.removeChild(bar.elements[i]); + + bottomBars.delete(window); + return bar; +}; + +/** + * Maps windows to a list of progress listeners. + */ +let progressListeners = new WeakMap(); + +/** + * Makes sure that a function is called whenever the displayed browser location changes. + */ +exports.addBrowserLocationListener = function addBrowserLocationListener(/**Window*/ window, /**Function*/ callback, /**Boolean*/ ignoreSameDoc) +{ + let browser = (exports.getBrowser ? exports.getBrowser(window) : null); + if (browser) + { + let dummy = function() {}; + let progressListener = + { + callback: callback, + onLocationChange: function(progress, request, uri, flags) + { + if (!ignoreSameDoc || !flags || !(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT)) + this.callback(); + }, + onProgressChange: dummy, + onSecurityChange: dummy, + onStateChange: dummy, + onStatusChange: dummy, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener, Ci.nsISupportsWeakReference]) + }; + browser.addProgressListener(progressListener); + + if (progressListeners.has(window)) + progressListeners.get(window).push(progressListener); + else + progressListeners.set(window, [progressListener]); + } +}; + +/** + * Removes a location listener registered for a window. + */ +exports.removeBrowserLocationListener = function removeBrowserLocationListener(/**Window*/ window, /**Function*/ callback) +{ + if (!progressListeners.has(window)) + return; + + let browser = (exports.getBrowser ? exports.getBrowser(window) : null); + let listeners = progressListeners.get(window); + for (let i = 0; i < listeners.length; i++) + { + if (listeners[i].callback == callback) + { + if (browser) + browser.removeProgressListener(listeners[i]); + listeners.splice(i--, 1); + } + } +}; + +/** + * Removes all location listeners registered for a window, to be called on + * cleanup. + */ +exports.removeBrowserLocationListeners = function removeBrowserLocationListeners(/**Window*/ window) +{ + if (!progressListeners.has(window)) + return; + + let browser = (exports.getBrowser ? exports.getBrowser(window) : null); + if (browser) + { + let listeners = progressListeners.get(window); + for (let i = 0; i < listeners.length; i++) + browser.removeProgressListener(listeners[i]); + } + progressListeners.delete(window); +}; + +/** + * Maps windows to a list of click listeners. + */ +let clickListeners = new WeakMap(); + +/** + * Makes sure that a function is called whenever the user clicks inside the + * browser's content area. + */ +exports.addBrowserClickListener = function addBrowserClickListener(/**Window*/ window, /**Function*/ callback) +{ + let browser = (exports.getBrowser ? exports.getBrowser(window) : null); + if (browser) + { + browser.addEventListener("click", callback, true); + + if (clickListeners.has(window)) + clickListeners.get(window).push(callback); + else + clickListeners.set(window, [callback]); + } +}; + +/** + * Removes all click listeners registered for a window, to be called on + * cleanup. + */ +exports.removeBrowserClickListeners = function removeBrowserClickListeners(/**Window*/ window) +{ + if (!clickListeners.has(window)) + return; + + let browser = (exports.getBrowser ? exports.getBrowser(window) : null); + if (browser) + { + let listeners = clickListeners.get(window); + for (let i = 0; i < listeners.length; i++) + browser.removeEventListener("click", listeners[i], true); + } + clickListeners.delete(window); +}; + +let {application} = require("info"); +switch (application) +{ + case "firefox": + { + exports.isKnownWindow = function ff_isKnownWindow(window) + { + return (window.document.documentElement.getAttribute("windowtype") == "navigator:browser"); + }; + + exports.getBrowser = (window) => window.gBrowser; + + exports.addTab = function ff_addTab(window, url, event) + { + if (event) + window.openNewTabWith(url, exports.getBrowser(window).contentDocument, null, event, false); + else + window.gBrowser.loadOneTab(url, {inBackground: false}); + }; + + exports.contentContextMenu = "contentAreaContextMenu"; + + exports.defaultToolbarPosition = { + parent: "nav-bar" + }; + + exports.toolsMenu = { + parent: "menu_ToolsPopup" + }; + + exports.addBottomBar = function fx_addBottomBar(window, element) + { + let bar = _addBottomBar(window, window.document.getElementById("appcontent"), element); + if (bar) + { + let display = window.document.getElementById("statusbar-display"); + bar.changedFixed = display && !display.hasAttribute("fixed"); + if (bar.changedFixed) + display.setAttribute("fixed", "true"); + } + }; + + exports.removeBottomBar = function fx_removeBottomBar(window) + { + let bar = _removeBottomBar(window); + if (bar && bar.changedFixed) + window.document.getElementById("statusbar-display").removeAttribute("fixed"); + }; + + break; + } + + case "seamonkey": + { + exports.isKnownWindow = function sm_isKnownWindow(window) + { + let type = window.document.documentElement.getAttribute("windowtype"); + return (type == "navigator:browser" || type == "mail:3pane" || type == "mail:messageWindow"); + }; + + exports.addTab = function sm_addTab(window, url, event) + { + if (event || !("gBrowser" in window)) + window.openNewTabWith(url, ("gBrowser" in window ? window.gBrowser.contentDocument : null), null, event, false); + else + window.gBrowser.loadOneTab(url, {inBackground: false}); + }; + + exports.getBrowser = function sm_getBrowser(window) + { + if ("gBrowser" in window) + return window.gBrowser; + else if ("getMessageBrowser" in window) + return window.getMessageBrowser(); + else + return null; + }; + + exports.getCurrentLocation = function sm_getCurrentLocation(window) + { + if ("currentHeaderData" in window && "content-base" in window.currentHeaderData) + { + // This is a blog entry + return window.currentHeaderData["content-base"].headerValue; + } + else if ("currentHeaderData" in window && "from" in window.currentHeaderData) + { + // This is a mail/newsgroup entry + try + { + let headerParser = Cc["@mozilla.org/messenger/headerparser;1"].getService(Ci.nsIMsgHeaderParser); + let emailAddress = headerParser.extractHeaderAddressMailboxes(window.currentHeaderData.from.headerValue); + return "mailto:" + emailAddress.replace(/^[\s"]+/, "").replace(/[\s"]+$/, "").replace(/\s/g, "%20"); + } + catch(e) + { + return null; + } + } + else + { + let browser = exports.getBrowser(window); + return (browser ? browser.currentURI : null); + } + }; + + exports.contentContextMenu = ["contentAreaContextMenu", "mailContext"]; + + exports.defaultToolbarPosition = { + parent: ["PersonalToolbar", "msgToolbar"], + before: ["bookmarks-button", "button-junk"] + }; + + exports.statusbarPosition = { + parent: "status-bar" + }; + + exports.toolsMenu = { + parent: "taskPopup", + after: "downloadmgr" + }; + + exports.addBottomBar = function sm_addBottomBar(window, element) + { + _addBottomBar(window, window.document.getElementById("appcontent") || window.document.getElementById("messagepanebox"), element); + }; + + exports.removeBottomBar = _removeBottomBar; + + break; + } + + case "thunderbird": + { + exports.isKnownWindow = function tb_isKnownWindow(window) + { + let type = window.document.documentElement.getAttribute("windowtype"); + return (type == "mail:3pane" || type == "mail:messageWindow"); + }; + + exports.delayInitialization = true; + + exports.getBrowser = (window) => window.getBrowser(); + + exports.addTab = function tb_addTab(window, url, event) + { + let tabmail = window.document.getElementById("tabmail"); + if (!tabmail) + { + let wnd = Services.wm.getMostRecentWindow("mail:3pane"); + if (window) + tabmail = wnd.document.getElementById("tabmail"); + } + + if (tabmail) + tabmail.openTab("contentTab", {contentPage: url}); + else + { + window.openDialog("chrome://messenger/content/", "_blank", + "chrome,dialog=no,all", null, + { + tabType: "contentTab", + tabParams: {contentPage: url} + }); + } + }; + + exports.contentContextMenu = ["mailContext", "pageContextMenu"]; + + exports.defaultToolbarPosition = { + parent: "header-view-toolbar", + before: "hdrReplyButton", + addClass: "msgHeaderView-button" + }; + + exports.statusbarPosition = { + parent: "status-bar" + }; + + exports.toolsMenu = { + parent: "taskPopup", + after: "javaScriptConsole" + }; + + exports.getCurrentLocation = function getCurrentLocation(window) + { + let browser = exports.getBrowser(window); + if (!browser) + return null; + + if (browser.id == "messagepane" && "currentHeaderData" in window && "content-base" in window.currentHeaderData) + { + // This is a blog entry + return window.currentHeaderData["content-base"].headerValue; + } + else if (browser.id == "messagepane" && "currentHeaderData" in window && "from" in window.currentHeaderData) + { + // This is a mail/newsgroup entry + try + { + let headerParser = Cc["@mozilla.org/messenger/headerparser;1"].getService(Ci.nsIMsgHeaderParser); + let emailAddress = headerParser.extractHeaderAddressMailboxes(window.currentHeaderData.from.headerValue); + return "mailto:" + emailAddress.replace(/^[\s"]+/, "").replace(/[\s"]+$/, "").replace(/\s/g, "%20"); + } + catch(e) + { + return null; + } + } + else + return browser.currentURI; + } + + exports.addBottomBar = function tb_addBottomBar(window, element) + { + let browser = exports.getBrowser(window); + if (!browser) + return; + + let parent = window.document.getElementById("messagepanebox"); + if (!parent || !(parent.compareDocumentPosition(browser) & Ci.nsIDOMNode.DOCUMENT_POSITION_CONTAINED_BY)) + parent = browser.parentNode; + + _addBottomBar(window, parent, element); + }; + + exports.removeBottomBar = _removeBottomBar; + + let BrowserChangeListener = function(window, callback) + { + this.window = window; + this.callback = callback; + this.onSelect = this.onSelect.bind(this); + this.attach(); + }; + BrowserChangeListener.prototype = { + window: null, + callback: null, + currentBrowser: null, + + setBrowser: function(browser) + { + if (browser != this.currentBrowser) + { + let oldBrowser = this.currentBrowser; + this.currentBrowser = browser; + this.callback(oldBrowser, browser); + } + }, + + onSelect: function() + { + this.setBrowser(exports.getBrowser(this.window)); + }, + + attach: function() + { + this.onSelect(); + + let tabmail = this.window.document.getElementById("tabmail"); + if (tabmail) + tabmail.tabContainer.addEventListener("select", this.onSelect, false); + }, + detach: function() + { + let tabmail = this.window.document.getElementById("tabmail"); + if (tabmail) + tabmail.tabContainer.removeEventListener("select", this.onSelect, false); + + this.setBrowser(null); + } + }; + + exports.addBrowserLocationListener = function(/**Window*/ window, /**Function*/ callback, /**Boolean*/ ignoreSameDoc) + { + if (progressListeners.has(window)) + { + progressListeners.get(window).locationCallbacks.push(callback); + return; + } + + let callbacks = [callback]; + let dummy = function() {}; + let progressListener = + { + onLocationChange: function(progress, request, uri, flags) + { + if (!ignoreSameDoc || !flags || !(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT)) + for (let i = 0; i < callbacks.length; i++) + callbacks[i](); + }, + onProgressChange: dummy, + onSecurityChange: dummy, + onStateChange: dummy, + onStatusChange: dummy, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener, Ci.nsISupportsWeakReference]) + }; + let messageListener = + { + onStartHeaders: dummy, + onEndHeaders: function() + { + let browser = exports.getBrowser(window); + if (browser.id == "messagepane") + for (let i = 0; i < callbacks.length; i++) + callbacks[i](); + }, + onEndAttachments: dummy, + onBeforeShowHeaderPane: dummy + }; + + let listener = new BrowserChangeListener(window, function(oldBrowser, newBrowser) + { + if (oldBrowser) + oldBrowser.removeProgressListener(progressListener); + if (newBrowser) + newBrowser.addProgressListener(progressListener); + progressListener.onLocationChange(); + }); + listener.locationCallbacks = callbacks; + + if ("gMessageListeners" in window) + window.gMessageListeners.push(messageListener); + listener.messageListener = messageListener; + + progressListeners.set(window, listener); + }; + + exports.removeBrowserLocationListener = function(/**Window*/ window, /**Function*/ callback) + { + if (!progressListeners.has(window)) + return; + + let callbacks = progressListeners.get(window).locationCallbacks; + for (let i = 0; i < callbacks.length; i++) + if (callbacks[i] == callback) + callbacks.splice(i--, 1); + }; + + exports.removeBrowserLocationListeners = function(/**Window*/ window) + { + if (!progressListeners.has(window)) + return; + + let listener = progressListeners.get(window); + + let messageListener = listener.messageListener; + let index = ("gMessageListeners" in window ? window.gMessageListeners.indexOf(messageListener) : -1); + if (index >= 0) + window.gMessageListeners.splice(index, 1); + + listener.detach(); + progressListeners.delete(window); + }; + + exports.addBrowserClickListener = function addBrowserClickListener(/**Window*/ window, /**Function*/ callback) + { + if (clickListeners.has(window)) + { + clickListeners.get(window).callbacks.push(callback); + return; + } + + let callbacks = [callback]; + let listener = new BrowserChangeListener(window, function(oldBrowser, newBrowser) + { + if (oldBrowser) + for (let i = 0; i < callbacks.length; i++) + oldBrowser.removeEventListener("click", callbacks[i], true); + if (newBrowser) + for (let i = 0; i < callbacks.length; i++) + newBrowser.addEventListener("click", callbacks[i], true); + }); + listener.callbacks = callbacks; + + clickListeners.set(window, listener); + }; + + exports.removeBrowserClickListeners = function removeBrowserClickListeners(/**Window*/ window) + { + if (!clickListeners.has(window)) + return; + + let listener = clickListeners.get(window); + listener.detach(); + + clickListeners.delete(window); + }; + + // Make sure to close/reopen list of blockable items when the user changes tabs + let {WindowObserver} = require("windowObserver"); + new WindowObserver({ + listeners: new WeakMap(), + applyToWindow: function(window) + { + if (!exports.isKnownWindow(window) || this.listeners.has(window)) + return; + + let {Utils} = require("utils"); + Utils.runAsync(function() + { + let listener = new BrowserChangeListener(window, function(oldBrowser, newBrowser) + { + if (bottomBars.has(window)) + { + let {UI} = require("ui") + UI.toggleBottombar(window); + UI.toggleBottombar(window); + } + }); + this.listeners.set(window, listener); + }.bind(this)); + }, + removeFromWindow: function(window) + { + if (!this.listeners.has(window)) + return; + + let listener = this.listeners.get(window); + listener.detach(); + this.listeners.delete(window); + } + }); + + break; + } + + case "fennec2": + { + exports.isKnownWindow = (window) => window.document.documentElement.id == "main-window"; + + exports.getBrowser = (window) => window.BrowserApp.selectedBrowser; + + exports.addTab = (window, url, event) => window.BrowserApp.addTab(url, {selected: true}); + + let BrowserChangeListener = function(window, callback) + { + this.window = window; + this.callback = callback; + this.onSelect = this.onSelect.bind(this); + this.attach(); + }; + BrowserChangeListener.prototype = { + window: null, + callback: null, + currentBrowser: null, + + setBrowser: function(browser) + { + if (browser != this.currentBrowser) + { + let oldBrowser = this.currentBrowser; + this.currentBrowser = browser; + this.callback(oldBrowser, browser); + } + }, + + onSelect: function() + { + let {Utils} = require("utils"); + Utils.runAsync(function() + { + this.setBrowser(exports.getBrowser(this.window)); + }.bind(this)); + }, + + attach: function() + { + this.onSelect(); + + this.window.BrowserApp.deck.addEventListener("TabSelect", this.onSelect, false); + }, + detach: function() + { + this.window.BrowserApp.deck.removeEventListener("TabSelect", this.onSelect, false); + + this.setBrowser(null); + } + }; + + exports.addBrowserLocationListener = function ffn_addBrowserLocationListener(/**Window*/ window, /**Function*/ callback, /**Boolean*/ ignoreSameDoc) + { + if (progressListeners.has(window)) + { + progressListeners.get(window).locationCallbacks.push(callback); + return; + } + + let callbacks = [callback]; + let dummy = function() {}; + let progressListener = + { + onLocationChange: function(progress, request, uri, flags) + { + if (!ignoreSameDoc || !flags || !(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT)) + for (let i = 0; i < callbacks.length; i++) + callbacks[i](); + }, + onProgressChange: dummy, + onSecurityChange: dummy, + onStateChange: dummy, + onStatusChange: dummy, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener, Ci.nsISupportsWeakReference]) + }; + + let listener = new BrowserChangeListener(window, function(oldBrowser, newBrowser) + { + if (oldBrowser && typeof oldBrowser.removeProgressListener == "function") + oldBrowser.removeProgressListener(progressListener); + if (newBrowser && typeof newBrowser.removeProgressListener == "function") + newBrowser.addProgressListener(progressListener); + progressListener.onLocationChange(); + }); + listener.locationCallbacks = callbacks; + + progressListeners.set(window, listener); + }; + + exports.removeBrowserLocationListener = function ffn_removeBrowserLocationListener(/**Window*/ window, /**Function*/ callback) + { + if (!progressListeners.has(window)) + return; + + let callbacks = progressListeners.get(window).locationCallbacks; + for (let i = 0; i < callbacks.length; i++) + if (callbacks[i] == callback) + callbacks.splice(i--, 1); + }; + + exports.removeBrowserLocationListeners = function ffn_removeBrowserLocationListeners(/**Window*/ window) + { + if (!progressListeners.has(window)) + return; + + let listener = progressListeners.get(window); + listener.detach(); + progressListeners.delete(window); + }; + + exports.addBrowserClickListener = function ffn_addBrowserClickListener(/**Window*/ window, /**Function*/ callback) + { + if (clickListeners.has(window)) + { + clickListeners.get(window).callbacks.push(callback); + return; + } + + let callbacks = [callback]; + let listener = new BrowserChangeListener(window, function(oldBrowser, newBrowser) + { + if (oldBrowser) + for (let i = 0; i < callbacks.length; i++) + oldBrowser.removeEventListener("click", callbacks[i], true); + if (newBrowser) + for (let i = 0; i < callbacks.length; i++) + newBrowser.addEventListener("click", callbacks[i], true); + }); + listener.callbacks = callbacks; + + clickListeners.set(window, listener); + }; + + exports.removeBrowserClickListeners = function ffn_removeBrowserClickListeners(/**Window*/ window) + { + if (!clickListeners.has(window)) + return; + + let listener = clickListeners.get(window); + listener.detach(); + + clickListeners.delete(window); + }; + + let {Filter} = require("filterClasses"); + let {Prefs} = require("prefs"); + let {Policy} = require("contentPolicy"); + let {UI} = require("ui"); + let {Utils} = require("utils"); + + let toggleWhitelist = function(window) + { + if (!Prefs.enabled) + { + Prefs.enabled = true; + return; + } + + let location = exports.getCurrentLocation(window); + let host = null; + if (location instanceof Ci.nsIURL && Policy.isBlockableScheme(location)) + { + try + { + host = location.host.replace(/^www\./, ""); + } catch (e) {} + } + + if (!host) + return; + + if (Policy.isWhitelisted(location.spec)) + UI.removeWhitelist(window); + else + UI.toggleFilter(Filter.fromText("@@||" + host + "^$document")); + }; + + let menuItem = null; + onShutdown.add(function() + { + let window = null; + for (window in UI.applicationWindows) + break; + + if (window && menuItem) + window.NativeWindow.menu.remove(menuItem); + }); + + UI.updateIconState = function fmn_updateIconState(window, icon) + { + if (menuItem !== null) + { + window.NativeWindow.menu.remove(menuItem); + menuItem = null; + } + + let action; + let host = null; + if (Prefs.enabled) + { + let location = exports.getCurrentLocation(window); + if (location instanceof Ci.nsIURL && Policy.isBlockableScheme(location)) + { + try + { + host = location.host.replace(/^www\./, ""); + } catch (e) {} + } + if (!host) + return; + + if (host && Policy.isWhitelisted(location.spec)) + action = "enable_site"; + else if (host) + action = "disable_site"; + } + else + action = "enable"; + + let actionText = Utils.getString("mobile_menu_" + action); + if (host) + actionText = actionText.replace(/\?1\?/g, host); + + let iconUrl = require("info").addonRoot + "icon64.png"; + menuItem = window.NativeWindow.menu.add(actionText, iconUrl, toggleWhitelist.bind(null, window)); + }; + + UI.openSubscriptionDialog = function(window, url, title, mainURL, mainTitle) + { + let dialogTitle = this.overlay.attributes.subscriptionDialogTitle; + let dialogMessage = this.overlay.attributes.subscriptionDialogMessage.replace(/\?1\?/, title).replace(/\?2\?/, url); + if (Utils.confirm(window, dialogMessage, dialogTitle)) + this.setSubscription(url, title); + }; + + UI.openFiltersDialog = function() + { + let window = UI.currentWindow; + if (!window) + return + + let browser = exports.addTab(window, "about:addons").browser; + browser.addEventListener("load", function openAddonPrefs(event) + { + browser.removeEventListener("load", openAddonPrefs, true); + Utils.runAsync(function() + { + // The page won't be ready until the add-on manager data is loaded so we call this method + // to know when the data will be ready. + AddonManager.getAddonsByTypes(["extension", "theme", "locale"], function() + { + let event = new Event("Event"); + event.initEvent("popstate", true, false); + event.state = {id: require("info").addonID}; + browser._contentWindow.dispatchEvent(event); + }); + }); + }, true); + }; + + break; + } +} diff --git a/data/extensions/spyblock@gnu.org/lib/contentPolicy.js b/data/extensions/spyblock@gnu.org/lib/contentPolicy.js new file mode 100644 index 0000000..084ebc5 --- /dev/null +++ b/data/extensions/spyblock@gnu.org/lib/contentPolicy.js @@ -0,0 +1,779 @@ +/* + * 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/>. + */ + +/** + * @fileOverview Content policy implementation, responsible for blocking things. + */ + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +let {TimeLine} = require("timeline"); +let {Utils} = require("utils"); +let {Prefs} = require("prefs"); +let {FilterStorage} = require("filterStorage"); +let {BlockingFilter, WhitelistFilter} = require("filterClasses"); +let {defaultMatcher} = require("matcher"); +let {objectMouseEventHander} = require("objectTabs"); +let {RequestNotifier} = require("requestNotifier"); +let {ElemHide} = require("elemHide"); + +/** + * List of explicitly supported content types + * @type Array of String + */ +let contentTypes = ["OTHER", "SCRIPT", "IMAGE", "STYLESHEET", "OBJECT", "SUBDOCUMENT", "DOCUMENT", "XMLHTTPREQUEST", "OBJECT_SUBREQUEST", "FONT", "MEDIA"]; + +/** + * List of content types that aren't associated with a visual document area + * @type Array of String + */ +let nonVisualTypes = ["SCRIPT", "STYLESHEET", "XMLHTTPREQUEST", "OBJECT_SUBREQUEST", "FONT"]; + +/** + * Randomly generated class name, to be applied to collapsed nodes. + */ +let collapsedClass = ""; + +/** + * Public policy checking functions and auxiliary objects + * @class + */ +let Policy = exports.Policy = +{ + /** + * Map of content type identifiers by their name. + * @type Object + */ + type: {}, + + /** + * Map of content type names by their identifiers (reverse of type map). + * @type Object + */ + typeDescr: {}, + + /** + * Map of localized content type names by their identifiers. + * @type Object + */ + localizedDescr: {}, + + /** + * Lists the non-visual content types. + * @type Object + */ + nonVisual: {}, + + /** + * Map containing all schemes that should be ignored by content policy. + * @type Object + */ + whitelistSchemes: {}, + + /** + * Called on module startup, initializes various exported properties. + */ + init: function() + { + TimeLine.enter("Entered content policy initialization"); + + // type constant by type description and type description by type constant + let iface = Ci.nsIContentPolicy; + for (let typeName of contentTypes) + { + if ("TYPE_" + typeName in iface) + { + let id = iface["TYPE_" + typeName]; + this.type[typeName] = id; + this.typeDescr[id] = typeName; + this.localizedDescr[id] = Utils.getString("type_label_" + typeName.toLowerCase()); + } + } + + this.type.ELEMHIDE = 0xFFFD; + this.typeDescr[0xFFFD] = "ELEMHIDE"; + this.localizedDescr[0xFFFD] = Utils.getString("type_label_elemhide"); + + this.type.POPUP = 0xFFFE; + this.typeDescr[0xFFFE] = "POPUP"; + this.localizedDescr[0xFFFE] = Utils.getString("type_label_popup"); + + for (let type of nonVisualTypes) + this.nonVisual[this.type[type]] = true; + + // whitelisted URL schemes + for (let scheme of Prefs.whitelistschemes.toLowerCase().split(" ")) + this.whitelistSchemes[scheme] = true; + + TimeLine.log("done initializing types"); + + // Generate class identifier used to collapse node and register corresponding + // stylesheet. + TimeLine.log("registering global stylesheet"); + + let offset = "a".charCodeAt(0); + for (let i = 0; i < 20; i++) + collapsedClass += String.fromCharCode(offset + Math.random() * 26); + + let collapseStyle = Services.io.newURI("data:text/css," + + encodeURIComponent("." + collapsedClass + + "{-moz-binding: url(chrome://global/content/bindings/general.xml#foobarbazdummy) !important;}"), null, null); + Utils.styleService.loadAndRegisterSheet(collapseStyle, Ci.nsIStyleSheetService.USER_SHEET); + onShutdown.add(function() + { + Utils.styleService.unregisterSheet(collapseStyle, Ci.nsIStyleSheetService.USER_SHEET); + }) + TimeLine.log("done registering stylesheet"); + + TimeLine.leave("Done initializing content policy"); + }, + + /** + * Checks whether a node should be blocked, hides it if necessary + * @param wnd {nsIDOMWindow} + * @param node {nsIDOMElement} + * @param contentType {String} + * @param location {nsIURI} + * @param collapse {Boolean} true to force hiding of the node + * @return {Boolean} false if the node should be blocked + */ + processNode: function(wnd, node, contentType, location, collapse) + { + let topWnd = wnd.top; + if (!topWnd || !topWnd.location || !topWnd.location.href) + return true; + + let privatenode=false; + Components.utils.import("resource://gre/modules/PrivateBrowsingUtils.jsm"); + if (PrivateBrowsingUtils.isWindowPrivate(wnd)) + privatenode=true; + + let originWindow = Utils.getOriginWindow(wnd); + let wndLocation = originWindow.location.href; + let docDomain = getHostname(wndLocation); + let match = null; + if (!match && Prefs.enabled) + { + let testWnd = wnd; + let parentWndLocation = getWindowLocation(testWnd); + while (true) + { + let testWndLocation = parentWndLocation; + parentWndLocation = (testWnd == testWnd.parent ? testWndLocation : getWindowLocation(testWnd.parent)); + match = Policy.isWhitelisted(testWndLocation, parentWndLocation); + + if (!(match instanceof WhitelistFilter)) + { + let keydata = (testWnd.document && testWnd.document.documentElement ? testWnd.document.documentElement.getAttribute("data-adblockkey") : null); + if (keydata && keydata.indexOf("_") >= 0) + { + let [key, signature] = keydata.split("_", 2); + let keyMatch = defaultMatcher.matchesByKey(testWndLocation, key.replace(/=/g, ""), docDomain); + if (keyMatch && Utils.crypto) + { + // Website specifies a key that we know but is the signature valid? + let uri = Services.io.newURI(testWndLocation, null, null); + let params = [ + uri.path.replace(/#.*/, ""), // REQUEST_URI + uri.asciiHost, // HTTP_HOST + Utils.httpProtocol.userAgent // HTTP_USER_AGENT + ]; + if (Utils.verifySignature(key, signature, params.join("\0"))) + match = keyMatch; + } + } + } + + if (match instanceof WhitelistFilter) + { + FilterStorage.increaseHitCount(match, wnd); + RequestNotifier.addNodeData(testWnd.document, topWnd, Policy.type.DOCUMENT, getHostname(parentWndLocation), false, testWndLocation, match); + return true; + } + + if (testWnd.parent == testWnd) + break; + else + testWnd = testWnd.parent; + } + } + + // Data loaded by plugins should be attached to the document + if (contentType == Policy.type.OBJECT_SUBREQUEST && node instanceof Ci.nsIDOMElement) + node = node.ownerDocument; + + // Fix type for objects misrepresented as frames or images + if (contentType != Policy.type.OBJECT && (node instanceof Ci.nsIDOMHTMLObjectElement || node instanceof Ci.nsIDOMHTMLEmbedElement)) + contentType = Policy.type.OBJECT; + + let locationText = location.spec; + if (!match && contentType == Policy.type.ELEMHIDE) + { + let testWnd = wnd; + let parentWndLocation = getWindowLocation(testWnd); + while (true) + { + let testWndLocation = parentWndLocation; + parentWndLocation = (testWnd == testWnd.parent ? testWndLocation : getWindowLocation(testWnd.parent)); + let parentDocDomain = getHostname(parentWndLocation); + match = defaultMatcher.matchesAny(testWndLocation, "ELEMHIDE", parentDocDomain, false); + if (match instanceof WhitelistFilter) + { + FilterStorage.increaseHitCount(match, wnd); + RequestNotifier.addNodeData(testWnd.document, topWnd, contentType, parentDocDomain, false, testWndLocation, match); + return true; + } + + if (testWnd.parent == testWnd) + break; + else + testWnd = testWnd.parent; + } + + match = location; + locationText = match.text.replace(/^.*?#/, '#'); + location = locationText; + + if (!match.isActiveOnDomain(docDomain)) + return true; + + let exception = ElemHide.getException(match, docDomain); + if (exception) + { + FilterStorage.increaseHitCount(exception, wnd); + RequestNotifier.addNodeData(node, topWnd, contentType, docDomain, thirdParty, locationText, exception); + return true; + } + } + + let thirdParty = (contentType == Policy.type.ELEMHIDE ? false : isThirdParty(location, docDomain)); + + if (!match && Prefs.enabled) + { + match = defaultMatcher.matchesAny(locationText, Policy.typeDescr[contentType] || "", docDomain, thirdParty, privatenode); + if (match instanceof BlockingFilter && node.ownerDocument && !(contentType in Policy.nonVisual)) + { + let prefCollapse = (match.collapse != null ? match.collapse : !Prefs.fastcollapse); + if (collapse || prefCollapse) + schedulePostProcess(node); + } + + // Track mouse events for objects + if (!match && contentType == Policy.type.OBJECT && node.nodeType == Ci.nsIDOMNode.ELEMENT_NODE) + { + node.addEventListener("mouseover", objectMouseEventHander, true); + node.addEventListener("mouseout", objectMouseEventHander, true); + } + } + + // Store node data + RequestNotifier.addNodeData(node, topWnd, contentType, docDomain, thirdParty, locationText, match); + if (match) + FilterStorage.increaseHitCount(match, wnd); + + return !match || match instanceof WhitelistFilter; + }, + + /** + * Checks whether the location's scheme is blockable. + * @param location {nsIURI} + * @return {Boolean} + */ + isBlockableScheme: function(location) + { + return !(location.scheme in Policy.whitelistSchemes); + }, + + /** + * Checks whether a page is whitelisted. + * @param {String} url + * @param {String} [parentUrl] location of the parent page + * @return {Filter} filter that matched the URL or null if not whitelisted + */ + isWhitelisted: function(url, parentUrl) + { + if (!url) + return null; + + // Do not apply exception rules to schemes on our whitelistschemes list. + let match = /^([\w\-]+):/.exec(url); + if (match && match[1] in Policy.whitelistSchemes) + return null; + + if (!parentUrl) + parentUrl = url; + + // Ignore fragment identifier + let index = url.indexOf("#"); + if (index >= 0) + url = url.substring(0, index); + + let result = defaultMatcher.matchesAny(url, "DOCUMENT", getHostname(parentUrl), false); + return (result instanceof WhitelistFilter ? result : null); + }, + + /** + * Checks whether the page loaded in a window is whitelisted. + * @param wnd {nsIDOMWindow} + * @return {Filter} matching exception rule or null if not whitelisted + */ + isWindowWhitelisted: function(wnd) + { + return Policy.isWhitelisted(getWindowLocation(wnd)); + }, + + + /** + * Asynchronously re-checks filters for given nodes. + */ + refilterNodes: function(/**Node[]*/ nodes, /**RequestEntry*/ entry) + { + // Ignore nodes that have been blocked already + if (entry.filter && !(entry.filter instanceof WhitelistFilter)) + return; + + for (let node of nodes) + Utils.runAsync(refilterNode, this, node, entry); + } +}; +Policy.init(); + +/** + * Actual nsIContentPolicy and nsIChannelEventSink implementation + * @class + */ +let PolicyImplementation = +{ + classDescription: "Adblock Plus content policy", + classID: Components.ID("cfeaabe6-1dd1-11b2-a0c6-cb5c268894c9"), + contractID: "@adblockplus.org/abp/policy;1", + xpcom_categories: ["content-policy", "net-channel-event-sinks"], + + /** + * Registers the content policy on startup. + */ + init: function() + { + let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar); + try + { + registrar.registerFactory(this.classID, this.classDescription, this.contractID, this); + } + catch (e if e.result == Cr.NS_ERROR_FACTORY_EXISTS) + { + // See bug 924340 - it might be too early to init now, the old version + // we are replacing didn't finish removing itself yet. + Utils.runAsync(this.init.bind(this)); + return; + } + + let catMan = Utils.categoryManager; + for (let category of this.xpcom_categories) + catMan.addCategoryEntry(category, this.contractID, this.contractID, false, true); + + // http-on-opening-request is new in Gecko 18, http-on-modify-request can + // be used in earlier releases. + let httpTopic = "http-on-opening-request"; + if (Services.vc.compare(Utils.platformVersion, "18.0") < 0) + httpTopic = "http-on-modify-request"; + + Services.obs.addObserver(this, httpTopic, true); + Services.obs.addObserver(this, "content-document-global-created", true); + Services.obs.addObserver(this, "xpcom-category-entry-removed", true); + Services.obs.addObserver(this, "xpcom-category-cleared", true); + + onShutdown.add(function() + { + // Our category observers should be removed before changing category + // memberships, just in case. + Services.obs.removeObserver(this, httpTopic); + Services.obs.removeObserver(this, "content-document-global-created"); + Services.obs.removeObserver(this, "xpcom-category-entry-removed"); + Services.obs.removeObserver(this, "xpcom-category-cleared"); + + for (let category of this.xpcom_categories) + catMan.deleteCategoryEntry(category, this.contractID, false); + + // This needs to run asynchronously, see bug 753687 + Utils.runAsync(function() + { + registrar.unregisterFactory(this.classID, this); + }.bind(this)); + + this.previousRequest = null; + }.bind(this)); + }, + + // + // nsISupports interface implementation + // + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentPolicy, Ci.nsIObserver, + Ci.nsIChannelEventSink, Ci.nsIFactory, Ci.nsISupportsWeakReference]), + + // + // nsIContentPolicy interface implementation + // + + shouldLoad: function(contentType, contentLocation, requestOrigin, node, mimeTypeGuess, extra) + { + // Ignore requests without context and top-level documents + if (!node || contentType == Policy.type.DOCUMENT) + return Ci.nsIContentPolicy.ACCEPT; + + // Ignore standalone objects + if (contentType == Policy.type.OBJECT && node.ownerDocument && !/^text\/|[+\/]xml$/.test(node.ownerDocument.contentType)) + return Ci.nsIContentPolicy.ACCEPT; + + let wnd = Utils.getWindow(node); + if (!wnd) + return Ci.nsIContentPolicy.ACCEPT; + + // Ignore whitelisted schemes + let location = Utils.unwrapURL(contentLocation); + if (!Policy.isBlockableScheme(location)) + return Ci.nsIContentPolicy.ACCEPT; + + // Interpret unknown types as "other" + if (!(contentType in Policy.typeDescr)) + contentType = Policy.type.OTHER; + + let result = Policy.processNode(wnd, node, contentType, location, false); + if (result) + { + // We didn't block this request so we will probably see it again in + // http-on-opening-request. Keep it so that we can associate it with the + // channel there - will be needed in case of redirect. + this.previousRequest = [location, contentType]; + } + return (result ? Ci.nsIContentPolicy.ACCEPT : Ci.nsIContentPolicy.REJECT_REQUEST); + }, + + shouldProcess: function(contentType, contentLocation, requestOrigin, insecNode, mimeType, extra) + { + return Ci.nsIContentPolicy.ACCEPT; + }, + + // + // nsIObserver interface implementation + // + observe: function(subject, topic, data, additional) + { + switch (topic) + { + case "content-document-global-created": + { + if (!(subject instanceof Ci.nsIDOMWindow) || !subject.opener) + return; + + let uri = additional || Utils.makeURI(subject.location.href); + if (!Policy.processNode(subject.opener, subject.opener.document, Policy.type.POPUP, uri, false)) + { + subject.stop(); + Utils.runAsync(subject.close, subject); + } + else if (uri.spec == "about:blank") + { + // An about:blank pop-up most likely means that a load will be + // initiated synchronously. Set a flag for our "http-on-opening-request" + // handler. + this.expectingPopupLoad = true; + Utils.runAsync(function() + { + this.expectingPopupLoad = false; + }); + } + break; + } + case "http-on-opening-request": + case "http-on-modify-request": + { + if (!(subject instanceof Ci.nsIHttpChannel)) + return; + + if (this.previousRequest && subject.URI == this.previousRequest[0] && + subject instanceof Ci.nsIWritablePropertyBag) + { + // We just handled a content policy call for this request - associate + // the data with the channel so that we can find it in case of a redirect. + subject.setProperty("abpRequestType", this.previousRequest[1]); + this.previousRequest = null; + } + + if (this.expectingPopupLoad) + { + let wnd = Utils.getRequestWindow(subject); + if (wnd && wnd.opener && wnd.location.href == "about:blank") + { + this.observe(wnd, "content-document-global-created", null, subject.URI); + if (subject instanceof Ci.nsIWritablePropertyBag) + subject.setProperty("abpRequestType", Policy.type.POPUP); + } + } + + break; + } + case "xpcom-category-entry-removed": + case "xpcom-category-cleared": + { + let category = data; + if (this.xpcom_categories.indexOf(category) < 0) + return; + + if (topic == "xpcom-category-entry-removed" && + subject instanceof Ci.nsISupportsCString && + subject.data != this.contractID) + { + return; + } + + // Our category entry was removed, make sure to add it back + let catMan = Utils.categoryManager; + catMan.addCategoryEntry(category, this.contractID, this.contractID, false, true); + break; + } + } + }, + + // + // nsIChannelEventSink interface implementation + // + + asyncOnChannelRedirect: function(oldChannel, newChannel, flags, callback) + { + let result = Cr.NS_OK; + try + { + // Try to retrieve previously stored request data from the channel + let contentType; + if (oldChannel instanceof Ci.nsIWritablePropertyBag) + { + try + { + contentType = oldChannel.getProperty("abpRequestType"); + } + catch(e) + { + // No data attached, ignore this redirect + return; + } + } + + let newLocation = null; + try + { + newLocation = newChannel.URI; + } catch(e2) {} + if (!newLocation) + return; + + let wnd = Utils.getRequestWindow(newChannel); + if (!wnd) + return; + + if (contentType == Policy.type.SUBDOCUMENT && wnd.parent == wnd.top && wnd.opener) + { + // This is a window opened in a new tab miscategorized as frame load, + // see bug 467514. Get the frame as context to be at least consistent. + wnd = wnd.opener; + } + + if (contentType == Policy.type.POPUP && wnd.opener) + { + // Popups are initiated by their opener, not their own window. + wnd = wnd.opener; + } + + if (!Policy.processNode(wnd, wnd.document, contentType, newLocation, false)) + result = Cr.NS_BINDING_ABORTED; + } + catch (e) + { + // We shouldn't throw exceptions here - this will prevent the redirect. + Cu.reportError(e); + } + finally + { + callback.onRedirectVerifyCallback(result); + } + }, + + // + // nsIFactory interface implementation + // + + createInstance: function(outer, iid) + { + if (outer) + throw Cr.NS_ERROR_NO_AGGREGATION; + return this.QueryInterface(iid); + } +}; +PolicyImplementation.init(); + +/** + * Nodes scheduled for post-processing (might be null). + * @type Array of Node + */ +let scheduledNodes = null; + +/** + * Schedules a node for post-processing. + */ +function schedulePostProcess(/**Element*/ node) +{ + if (scheduledNodes) + scheduledNodes.push(node); + else + { + scheduledNodes = [node]; + Utils.runAsync(postProcessNodes); + } +} + +/** + * Processes nodes scheduled for post-processing (typically hides them). + */ +function postProcessNodes() +{ + let nodes = scheduledNodes; + scheduledNodes = null; + + for (let node of nodes) + { + // adjust frameset's cols/rows for frames + let parentNode = node.parentNode; + if (parentNode && parentNode instanceof Ci.nsIDOMHTMLFrameSetElement) + { + let hasCols = (parentNode.cols && parentNode.cols.indexOf(",") > 0); + let hasRows = (parentNode.rows && parentNode.rows.indexOf(",") > 0); + if ((hasCols || hasRows) && !(hasCols && hasRows)) + { + let index = -1; + for (let frame = node; frame; frame = frame.previousSibling) + if (frame instanceof Ci.nsIDOMHTMLFrameElement || frame instanceof Ci.nsIDOMHTMLFrameSetElement) + index++; + + let property = (hasCols ? "cols" : "rows"); + let weights = parentNode[property].split(","); + weights[index] = "0"; + parentNode[property] = weights.join(","); + } + } + else + node.classList.add(collapsedClass); + } +} + +/** + * Extracts the hostname from a URL (might return null). + */ +function getHostname(/**String*/ url) /**String*/ +{ + try + { + return Utils.unwrapURL(url).host; + } + catch(e) + { + return null; + } +} + +/** + * Retrieves the location of a window. + * @param wnd {nsIDOMWindow} + * @return {String} window location or null on failure + */ +function getWindowLocation(wnd) +{ + if ("name" in wnd && wnd.name == "messagepane") + { + // Thunderbird branch + try + { + let mailWnd = wnd.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShellTreeItem) + .rootTreeItem + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow); + + // Typically we get a wrapped mail window here, need to unwrap + try + { + mailWnd = mailWnd.wrappedJSObject; + } catch(e) {} + + if ("currentHeaderData" in mailWnd && "content-base" in mailWnd.currentHeaderData) + { + return mailWnd.currentHeaderData["content-base"].headerValue; + } + else if ("currentHeaderData" in mailWnd && "from" in mailWnd.currentHeaderData) + { + let emailAddress = Utils.headerParser.extractHeaderAddressMailboxes(mailWnd.currentHeaderData.from.headerValue); + if (emailAddress) + return 'mailto:' + emailAddress.replace(/^[\s"]+/, "").replace(/[\s"]+$/, "").replace(/\s/g, '%20'); + } + } catch(e) {} + } + + // Firefox branch + return wnd.location.href; +} + +/** + * Checks whether the location's origin is different from document's origin. + */ +function isThirdParty(/**nsIURI*/location, /**String*/ docDomain) /**Boolean*/ +{ + if (!location || !docDomain) + return true; + + try + { + return Utils.effectiveTLD.getBaseDomain(location) != Utils.effectiveTLD.getBaseDomainFromHost(docDomain); + } + catch (e) + { + // EffectiveTLDService throws on IP addresses, just compare the host name + let host = ""; + try + { + host = location.host; + } catch (e) {} + return host != docDomain; + } +} + +/** + * Re-checks filters on an element. + */ +function refilterNode(/**Node*/ node, /**RequestEntry*/ entry) +{ + let wnd = Utils.getWindow(node); + if (!wnd || wnd.closed) + return; + + if (entry.type == Policy.type.OBJECT) + { + node.removeEventListener("mouseover", objectMouseEventHander, true); + node.removeEventListener("mouseout", objectMouseEventHander, true); + } + Policy.processNode(wnd, node, entry.type, Utils.makeURI(entry.location), true); +} diff --git a/data/extensions/spyblock@gnu.org/lib/customizableUI.js b/data/extensions/spyblock@gnu.org/lib/customizableUI.js new file mode 100644 index 0000000..7db7425 --- /dev/null +++ b/data/extensions/spyblock@gnu.org/lib/customizableUI.js @@ -0,0 +1,320 @@ +/* + * 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/>. + */ + +/** + * @fileOverview This emulates a subset of the CustomizableUI API from Firefox 28. + */ + +let {XPCOMUtils} = Cu.import("resource://gre/modules/XPCOMUtils.jsm", null); + +let {Utils} = require("utils"); + +// UI module has to be referenced lazily to avoid circular references +XPCOMUtils.defineLazyGetter(this, "UI", () => require("ui").UI); + +let widgets = Map(); + +function getToolbox(/**Window*/ window, /**Widget*/ widget) /**Element*/ +{ + if (!("defaultArea" in widget) || !widget.defaultArea) + return null; + + let toolbar = UI.findElement(window, widget.defaultArea); + if (!toolbar) + return null; + + let toolbox = toolbar.toolbox; + if (toolbox && ("palette" in toolbox) && toolbox.palette) + return toolbox; + else + return null; +} + +function getToolbar(/**Element*/ element) /**Element*/ +{ + for (let parent = element.parentNode; parent; parent = parent.parentNode) + if (parent.localName == "toolbar") + return parent; + return null; +} + +function getPaletteItem(/**Element*/ toolbox, /**String*/ id) /**Element*/ +{ + for (let child of toolbox.palette.children) + if (child.id == id) + return child; + + return null; +} + +function restoreWidget(/**Element*/ toolbox, /**Widget*/ widget) +{ + // Create node + let node = widget.onBuild(toolbox.ownerDocument); + + // Insert into the palette first + toolbox.palette.insertBefore(node, toolbox.palette.firstChild); + + // Now find out where we should put it + let position = toolbox.getAttribute(widget.positionAttribute); + if (!/^\S*,\S*,\S*$/.test(position)) + position = null; + + if (position == null) + { + // No explicitly saved position but maybe we can find it in a currentset + // attribute somewhere. + let toolbars = toolbox.externalToolbars.slice(); + for (let child of toolbox.children) + if (child.localName == "toolbar") + toolbars.push(child); + for (let toolbar of toolbars) + { + let currentSet = toolbar.getAttribute("currentset"); + if (currentSet) + { + let items = currentSet.split(","); + let index = items.indexOf(widget.id); + if (index >= 0) + { + let before = (index + 1 < items.length ? items[index + 1] : ""); + position = "visible," + toolbar.id + "," + before; + toolbox.setAttribute(widget.positionAttribute, position); + toolbox.ownerDocument.persist(toolbox.id, widget.positionAttribute); + break; + } + } + } + } + + showWidget(toolbox, widget, position); +} + +function showWidget(/**Element*/ toolbox, /**Widget*/ widget, /**String*/ position) +{ + let visible = "visible", parent = null, before = null; + if (position) + { + [visible, parent, before] = position.split(",", 3); + parent = toolbox.ownerDocument.getElementById(parent); + if (before == "") + before = null; + else + before = toolbox.ownerDocument.getElementById(before); + if (before && before.parentNode != parent) + before = null; + } + + if (visible == "visible" && !parent) + { + let insertionPoint = { + parent: widget.defaultArea + }; + if (typeof widget.defaultBefore != "undefined") + insertionPoint.before = widget.defaultBefore; + if (typeof widget.defaultAfter != "undefined") + insertionPoint.after = widget.defaultAfter; + + [parent, before] = UI.resolveInsertionPoint(toolbox.ownerDocument.defaultView, insertionPoint); + } + + if (parent && parent.localName != "toolbar") + parent = null; + + if (visible != "visible") + { + // Move to palette if the item is currently visible + let node = toolbox.ownerDocument.getElementById(widget.id); + if (node) + toolbox.palette.appendChild(node); + } + else if (parent) + { + // Add the item to the toolbar + let items = parent.currentSet.split(","); + let index = (before ? items.indexOf(before.id) : -1); + if (index < 0) + before = null; + parent.insertItem(widget.id, before, null, false); + } + + saveState(toolbox, widget); +} + +function removeWidget(/**Window*/ window, /**Widget*/ widget) +{ + let element = window.document.getElementById(widget.id); + if (element) + element.parentNode.removeChild(element); + + let toolbox = getToolbox(window, widget); + if (toolbox) + { + let paletteItem = getPaletteItem(toolbox, widget.id); + if (paletteItem) + paletteItem.parentNode.removeChild(paletteItem); + } +} + +function onToolbarCustomization(/**Event*/ event) +{ + let toolbox = event.currentTarget; + for (let [id, widget] of widgets) + saveState(toolbox, widget); +} + +function saveState(/**Element*/ toolbox, /**Widget*/ widget) +{ + let node = toolbox.ownerDocument.getElementById(widget.id); + + let position = toolbox.getAttribute(widget.positionAttribute) || "hidden,,"; + if (node && node.parentNode.localName != "toolbarpalette") + { + if (typeof widget.onAdded == "function") + widget.onAdded(node) + + let toolbar = getToolbar(node); + position = "visible," + toolbar.id + "," + (node.nextSibling ? node.nextSibling.id : ""); + } + else + position = position.replace(/^visible,/, "hidden,") + + toolbox.setAttribute(widget.positionAttribute, position); + toolbox.ownerDocument.persist(toolbox.id, widget.positionAttribute); +} + +let CustomizableUI = exports.CustomizableUI = +{ + createWidget: function(widget) + { + if (typeof widget.id == "undefined" || + typeof widget.defaultArea == "undefined" || + typeof widget.positionAttribute == "undefined") + { + throw new Error("Unexpected: required property missing from the widget data"); + } + widgets.set(widget.id, widget); + + // Show widget in any existing windows + for (let window of UI.applicationWindows) + { + let toolbox = getToolbox(window, widget); + if (toolbox) + { + toolbox.addEventListener("aftercustomization", onToolbarCustomization, false); + restoreWidget(toolbox, widget); + } + } + }, + + destroyWidget: function(id) + { + // Don't do anything here. This function is called on shutdown, + // removeFromWindow will take care of cleaning up already. + }, + + getPlacementOfWidget: function(id) + { + let window = UI.currentWindow; + if (!window) + return null; + + let widget = window.document.getElementById(id); + if (!widget) + return null; + + let toolbar = getToolbar(widget); + if (!toolbar) + return null; + + return {area: toolbar.id}; + }, + + addWidgetToArea: function(id) + { + // Note: the official API function also has area and position parameters. + // We ignore those here and simply restore the previous position instead. + let widget = widgets.get(id); + for (let window of UI.applicationWindows) + { + let toolbox = getToolbox(window, widget); + if (!toolbox) + continue; + + let position = toolbox.getAttribute(widget.positionAttribute); + if (position) + position = position.replace(/^hidden,/, "visible,"); + showWidget(toolbox, widget, position); + } + }, + + removeWidgetFromArea: function(id) + { + let widget = widgets.get(id); + for (let window of UI.applicationWindows) + { + let toolbox = getToolbox(window, widget); + if (!toolbox) + continue; + + let position = toolbox.getAttribute(widget.positionAttribute); + if (position) + position = position.replace(/^visible,/, "hidden,"); + else + position = "hidden,,"; + showWidget(toolbox, widget, position); + } + } +}; + +let {WindowObserver} = require("windowObserver"); +new WindowObserver({ + applyToWindow: function(window) + { + let {isKnownWindow} = require("appSupport"); + if (!isKnownWindow(window)) + return; + + for (let [id, widget] of widgets) + { + let toolbox = getToolbox(window, widget); + if (toolbox) + { + toolbox.addEventListener("aftercustomization", onToolbarCustomization, false); + + // Restore widget asynchronously to allow the stylesheet to load + Utils.runAsync(restoreWidget.bind(null, toolbox, widget)); + } + } + }, + + removeFromWindow: function(window) + { + let {isKnownWindow} = require("appSupport"); + if (!isKnownWindow(window)) + return; + + for (let [id, widget] of widgets) + { + let toolbox = getToolbox(window, widget); + if (toolbox) + toolbox.removeEventListener("aftercustomization", onToolbarCustomization, false); + + removeWidget(window, widget); + } + } +}); diff --git a/data/extensions/spyblock@gnu.org/lib/downloader.js b/data/extensions/spyblock@gnu.org/lib/downloader.js new file mode 100644 index 0000000..d1ef209 --- /dev/null +++ b/data/extensions/spyblock@gnu.org/lib/downloader.js @@ -0,0 +1,381 @@ +/* + * 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/>. + */ + +/** + * @fileOverview Downloads a set of URLs in regular time intervals. + */ + +let {Utils} = require("utils"); + +let MILLIS_IN_SECOND = exports.MILLIS_IN_SECOND = 1000; +let MILLIS_IN_MINUTE = exports.MILLIS_IN_MINUTE = 60 * MILLIS_IN_SECOND; +let MILLIS_IN_HOUR = exports.MILLIS_IN_HOUR = 60 * MILLIS_IN_MINUTE; +let MILLIS_IN_DAY = exports.MILLIS_IN_DAY = 24 * MILLIS_IN_HOUR; + +/** + * Creates a new downloader instance. + * @param {Function} dataSource Function that will yield downloadable objects on each check + * @param {Integer} initialDelay Number of milliseconds to wait before the first check + * @param {Integer} checkInterval Interval between the checks + * @constructor + */ +let Downloader = exports.Downloader = function Downloader(dataSource, initialDelay, checkInterval) +{ + this.dataSource = dataSource; + this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + this._timer.initWithCallback(function() + { + this._timer.delay = checkInterval; + this._doCheck(); + }.bind(this), initialDelay, Ci.nsITimer.TYPE_REPEATING_SLACK); + this._downloading = Object.create(null); +} +Downloader.prototype = +{ + /** + * Timer triggering the downloads. + * @type nsITimer + */ + _timer: null, + + /** + * Map containing the URLs of objects currently being downloaded as its keys. + */ + _downloading: null, + + /** + * Function that will yield downloadable objects on each check. + * @type Function + */ + dataSource: null, + + /** + * Maximal time interval that the checks can be left out until the soft + * expiration interval increases. + * @type Integer + */ + maxAbsenceInterval: 1 * MILLIS_IN_DAY, + + /** + * Minimal time interval before retrying a download after an error. + * @type Integer + */ + minRetryInterval: 1 * MILLIS_IN_DAY, + + /** + * Maximal allowed expiration interval, larger expiration intervals will be + * corrected. + * @type Integer + */ + maxExpirationInterval: 14 * MILLIS_IN_DAY, + + /** + * Maximal number of redirects before the download is considered as failed. + * @type Integer + */ + maxRedirects: 5, + + /** + * Called whenever expiration intervals for an object need to be adapted. + * @type Function + */ + onExpirationChange: null, + + /** + * Callback to be triggered whenever a download starts. + * @type Function + */ + onDownloadStarted: null, + + /** + * Callback to be triggered whenever a download finishes successfully. The + * callback can return an error code to indicate that the data is wrong. + * @type Function + */ + onDownloadSuccess: null, + + /** + * Callback to be triggered whenever a download fails. + * @type Function + */ + onDownloadError: null, + + /** + * Checks whether anything needs downloading. + */ + _doCheck: function() + { + let now = Date.now(); + for (let downloadable of this.dataSource()) + { + if (downloadable.lastCheck && now - downloadable.lastCheck > this.maxAbsenceInterval) + { + // No checks for a long time interval - user must have been offline, e.g. + // during a weekend. Increase soft expiration to prevent load peaks on the + // server. + downloadable.softExpiration += now - downloadable.lastCheck; + } + downloadable.lastCheck = now; + + // Sanity check: do expiration times make sense? Make sure people changing + // system clock don't get stuck with outdated subscriptions. + if (downloadable.hardExpiration - now > this.maxExpirationInterval) + downloadable.hardExpiration = now + this.maxExpirationInterval; + if (downloadable.softExpiration - now > this.maxExpirationInterval) + downloadable.softExpiration = now + this.maxExpirationInterval; + + // Notify the caller about changes to expiration parameters + if (this.onExpirationChange) + this.onExpirationChange(downloadable); + + // Does that object need downloading? + if (downloadable.softExpiration > now && downloadable.hardExpiration > now) + continue; + + // Do not retry downloads too often + if (downloadable.lastError && now - downloadable.lastError < this.minRetryInterval) + continue; + + this._download(downloadable, 0); + } + }, + + /** + * Stops the periodic checks. + */ + cancel: function() + { + this._timer.cancel(); + }, + + /** + * Checks whether an address is currently being downloaded. + */ + isDownloading: function(/**String*/ url) /**Boolean*/ + { + return url in this._downloading; + }, + + /** + * Starts downloading for an object. + */ + download: function(/**Downloadable*/ downloadable) + { + // Make sure to detach download from the current execution context + Utils.runAsync(this._download.bind(this, downloadable, 0)); + }, + + /** + * Generates the real download URL for an object by appending various + * parameters. + */ + getDownloadUrl: function(/**Downloadable*/ downloadable) /** String*/ + { + let {addonName, addonVersion, application, applicationVersion, platform, platformVersion} = require("info"); + let url = downloadable.redirectURL || downloadable.url; + if (url.indexOf("?") >= 0) + url += "&"; + else + url += "?"; + url += "addonName=" + encodeURIComponent(addonName) + + "&addonVersion=" + encodeURIComponent(addonVersion) + + "&application=" + encodeURIComponent(application) + + "&applicationVersion=" + encodeURIComponent(applicationVersion) + + "&platform=" + encodeURIComponent(platform) + + "&platformVersion=" + encodeURIComponent(platformVersion) + + "&lastVersion=" + encodeURIComponent(downloadable.lastVersion); + return url; + }, + + _download: function(downloadable, redirects) + { + if (this.isDownloading(downloadable.url)) + return; + + let downloadUrl = this.getDownloadUrl(downloadable); + let request = null; + + let errorCallback = function errorCallback(error) + { + let channelStatus = -1; + try + { + channelStatus = request.channel.status; + } catch (e) {} + + let responseStatus = request.status; + + Cu.reportError("Adblock Plus: Downloading URL " + downloadable.url + " failed (" + error + ")\n" + + "Download address: " + downloadUrl + "\n" + + "Channel status: " + channelStatus + "\n" + + "Server response: " + responseStatus); + + if (this.onDownloadError) + { + // Allow one extra redirect if the error handler gives us a redirect URL + let redirectCallback = null; + if (redirects <= this.maxRedirects) + { + redirectCallback = function redirectCallback(url) + { + downloadable.redirectURL = url; + this._download(downloadable, redirects + 1); + }.bind(this); + } + + this.onDownloadError(downloadable, downloadUrl, error, channelStatus, responseStatus, redirectCallback); + } + }.bind(this); + + try + { + request = new XMLHttpRequest(); + request.mozBackgroundRequest = true; + request.open("GET", downloadUrl); + } + catch (e) + { + errorCallback("synchronize_invalid_url"); + return; + } + + try { + request.overrideMimeType("text/plain"); + request.channel.loadFlags = request.channel.loadFlags | + request.channel.INHIBIT_CACHING | + request.channel.VALIDATE_ALWAYS; + + // Override redirect limit from preferences, user might have set it to 1 + if (request.channel instanceof Ci.nsIHttpChannel) + request.channel.redirectionLimit = this.maxRedirects; + } + catch (e) + { + Cu.reportError(e) + } + + request.addEventListener("error", function(event) + { + if (onShutdown.done) + return; + + delete this._downloading[downloadable.url]; + errorCallback("synchronize_connection_error"); + }.bind(this), false); + + request.addEventListener("load", function(event) + { + if (onShutdown.done) + return; + + delete this._downloading[downloadable.url]; + + // Status will be 0 for non-HTTP requests + if (request.status && request.status != 200) + { + errorCallback("synchronize_connection_error"); + return; + } + + this.onDownloadSuccess(downloadable, request.responseText, errorCallback, function redirectCallback(url) + { + if (redirects >= this.maxRedirects) + errorCallback("synchronize_connection_error"); + else + { + downloadable.redirectURL = url; + this._download(downloadable, redirects + 1); + } + }.bind(this)); + }.bind(this), false); + + request.send(null); + + this._downloading[downloadable.url] = true; + if (this.onDownloadStarted) + this.onDownloadStarted(downloadable); + }, + + /** + * Produces a soft and a hard expiration interval for a given supplied + * expiration interval. + * @return {Array} soft and hard expiration interval + */ + processExpirationInterval: function(/**Integer*/ interval) + { + interval = Math.min(Math.max(interval, 0), this.maxExpirationInterval); + let soft = Math.round(interval * (Math.random() * 0.4 + 0.8)); + let hard = interval * 2; + let now = Date.now(); + return [now + soft, now + hard]; + } +}; + +/** + * An object that can be downloaded by the downloadable + * @param {String} url URL that has to be requested for the object + * @constructor + */ +let Downloadable = exports.Downloadable = function Downloadable(url) +{ + this.url = url; +} +Downloadable.prototype = +{ + /** + * URL that has to be requested for the object. + * @type String + */ + url: null, + + /** + * URL that the download was redirected to if any. + * @type String + */ + redirectURL: null, + + /** + * Time of last download error or 0 if the last download was successful. + * @type Integer + */ + lastError: 0, + + /** + * Time of last check whether the object needs downloading. + * @type Integer + */ + lastCheck: 0, + + /** + * Object version corresponding to the last successful download. + * @type Integer + */ + lastVersion: 0, + + /** + * Soft expiration interval, will increase if no checks are performed for a + * while. + * @type Integer + */ + softExpiration: 0, + + /** + * Hard expiration interval, this is fixed. + * @type Integer + */ + hardExpiration: 0, +}; diff --git a/data/extensions/spyblock@gnu.org/lib/elemHide.js b/data/extensions/spyblock@gnu.org/lib/elemHide.js new file mode 100644 index 0000000..df17a0f --- /dev/null +++ b/data/extensions/spyblock@gnu.org/lib/elemHide.js @@ -0,0 +1,419 @@ +/* + * 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/>. + */ + +/** + * @fileOverview Element hiding implementation. + */ + +Cu.import("resource://gre/modules/Services.jsm"); + +let {Utils} = require("utils"); +let {IO} = require("io"); +let {Prefs} = require("prefs"); +let {ElemHideException} = require("filterClasses"); +let {FilterNotifier} = require("filterNotifier"); +let {AboutHandler} = require("elemHideHitRegistration"); +let {TimeLine} = require("timeline"); + +/** + * Lookup table, filters by their associated key + * @type Object + */ +let filterByKey = {__proto__: null}; + +/** + * Lookup table, keys of the filters by filter text + * @type Object + */ +let keyByFilter = {__proto__: null}; + +/** + * Lookup table, keys are known element hiding exceptions + * @type Object + */ +let knownExceptions = {__proto__: null}; + +/** + * Lookup table, lists of element hiding exceptions by selector + * @type Object + */ +let exceptions = {__proto__: null}; + +/** + * Currently applied stylesheet URL + * @type nsIURI + */ +let styleURL = null; + +/** + * Element hiding component + * @class + */ +let ElemHide = exports.ElemHide = +{ + /** + * Indicates whether filters have been added or removed since the last apply() call. + * @type Boolean + */ + isDirty: false, + + /** + * Inidicates whether the element hiding stylesheet is currently applied. + * @type Boolean + */ + applied: false, + + /** + * Called on module startup. + */ + init: function() + { + TimeLine.enter("Entered ElemHide.init()"); + Prefs.addListener(function(name) + { + if (name == "enabled") + ElemHide.apply(); + }); + onShutdown.add(function() + { + ElemHide.unapply(); + }); + + TimeLine.log("done adding prefs listener"); + + let styleFile = IO.resolveFilePath(Prefs.data_directory); + styleFile.append("elemhide.css"); + styleURL = Services.io.newFileURI(styleFile).QueryInterface(Ci.nsIFileURL); + TimeLine.log("done determining stylesheet URL"); + + TimeLine.leave("ElemHide.init() done"); + }, + + /** + * Removes all known filters + */ + clear: function() + { + filterByKey = {__proto__: null}; + keyByFilter = {__proto__: null}; + knownExceptions = {__proto__: null}; + exceptions = {__proto__: null}; + ElemHide.isDirty = false; + ElemHide.unapply(); + }, + + /** + * Add a new element hiding filter + * @param {ElemHideFilter} filter + */ + add: function(filter) + { + if (filter instanceof ElemHideException) + { + if (filter.text in knownExceptions) + return; + + let selector = filter.selector; + if (!(selector in exceptions)) + exceptions[selector] = []; + exceptions[selector].push(filter); + knownExceptions[filter.text] = true; + } + else + { + if (filter.text in keyByFilter) + return; + + let key; + do { + key = Math.random().toFixed(15).substr(5); + } while (key in filterByKey); + + filterByKey[key] = filter; + keyByFilter[filter.text] = key; + ElemHide.isDirty = true; + } + }, + + /** + * Removes an element hiding filter + * @param {ElemHideFilter} filter + */ + remove: function(filter) + { + if (filter instanceof ElemHideException) + { + if (!(filter.text in knownExceptions)) + return; + + let list = exceptions[filter.selector]; + let index = list.indexOf(filter); + if (index >= 0) + list.splice(index, 1); + delete knownExceptions[filter.text]; + } + else + { + if (!(filter.text in keyByFilter)) + return; + + let key = keyByFilter[filter.text]; + delete filterByKey[key]; + delete keyByFilter[filter.text]; + ElemHide.isDirty = true; + } + }, + + /** + * Checks whether an exception rule is registered for a filter on a particular + * domain. + */ + getException: function(/**Filter*/ filter, /**String*/ docDomain) /**ElemHideException*/ + { + let selector = filter.selector; + if (!(filter.selector in exceptions)) + return null; + + let list = exceptions[filter.selector]; + for (let i = list.length - 1; i >= 0; i--) + if (list[i].isActiveOnDomain(docDomain)) + return list[i]; + + return null; + }, + + /** + * Will be set to true if apply() is running (reentrance protection). + * @type Boolean + */ + _applying: false, + + /** + * Will be set to true if an apply() call arrives while apply() is already + * running (delayed execution). + * @type Boolean + */ + _needsApply: false, + + /** + * Generates stylesheet URL and applies it globally + */ + apply: function() + { + if (this._applying) + { + this._needsApply = true; + return; + } + + TimeLine.enter("Entered ElemHide.apply()"); + + if (!ElemHide.isDirty || !Prefs.enabled) + { + // Nothing changed, looks like we merely got enabled/disabled + if (Prefs.enabled && !ElemHide.applied) + { + try + { + Utils.styleService.loadAndRegisterSheet(styleURL, Ci.nsIStyleSheetService.USER_SHEET); + ElemHide.applied = true; + } + catch (e) + { + Cu.reportError(e); + } + TimeLine.log("Applying existing stylesheet finished"); + } + else if (!Prefs.enabled && ElemHide.applied) + { + ElemHide.unapply(); + TimeLine.log("ElemHide.unapply() finished"); + } + + TimeLine.leave("ElemHide.apply() done (no file changes)"); + return; + } + + IO.writeToFile(styleURL.file, this._generateCSSContent(), function(e) + { + TimeLine.enter("ElemHide.apply() write callback"); + this._applying = false; + + // _generateCSSContent is throwing NS_ERROR_NOT_AVAILABLE to indicate that + // there are no filters. If that exception is passed through XPCOM we will + // see a proper exception here, otherwise a number. + let noFilters = (e == Cr.NS_ERROR_NOT_AVAILABLE || (e && e.result == Cr.NS_ERROR_NOT_AVAILABLE)); + if (noFilters) + { + e = null; + IO.removeFile(styleURL.file, function(e) {}); + } + else if (e) + Cu.reportError(e); + + if (this._needsApply) + { + this._needsApply = false; + this.apply(); + } + else if (!e) + { + ElemHide.isDirty = false; + + ElemHide.unapply(); + TimeLine.log("ElemHide.unapply() finished"); + + if (!noFilters) + { + try + { + Utils.styleService.loadAndRegisterSheet(styleURL, Ci.nsIStyleSheetService.USER_SHEET); + ElemHide.applied = true; + } + catch (e) + { + Cu.reportError(e); + } + TimeLine.log("Applying stylesheet finished"); + } + + FilterNotifier.triggerListeners("elemhideupdate"); + } + TimeLine.leave("ElemHide.apply() write callback done"); + }.bind(this), "ElemHideWrite"); + + this._applying = true; + + TimeLine.leave("ElemHide.apply() done", "ElemHideWrite"); + }, + + _generateCSSContent: function() + { + // Grouping selectors by domains + TimeLine.log("start grouping selectors"); + let domains = {__proto__: null}; + let hasFilters = false; + for (let key in filterByKey) + { + let filter = filterByKey[key]; + let domain = filter.selectorDomain || ""; + + let list; + if (domain in domains) + list = domains[domain]; + else + { + list = {__proto__: null}; + domains[domain] = list; + } + list[filter.selector] = key; + hasFilters = true; + } + TimeLine.log("done grouping selectors"); + + if (!hasFilters) + throw Cr.NS_ERROR_NOT_AVAILABLE; + + function escapeChar(match) + { + return "\\" + match.charCodeAt(0).toString(16) + " "; + } + + // Return CSS data + let cssTemplate = "-moz-binding: url(about:" + AboutHandler.aboutPrefix + "?%ID%#dummy) !important;"; + for (let domain in domains) + { + let rules = []; + let list = domains[domain]; + + if (domain) + yield ('@-moz-document domain("' + domain.split(",").join('"),domain("') + '"){').replace(/[^\x01-\x7F]/g, escapeChar); + else + { + // Only allow unqualified rules on a few protocols to prevent them from blocking chrome + yield '@-moz-document url-prefix("http://"),url-prefix("https://"),' + + 'url-prefix("mailbox://"),url-prefix("imap://"),' + + 'url-prefix("news://"),url-prefix("snews://"){'; + } + + for (let selector in list) + yield selector.replace(/[^\x01-\x7F]/g, escapeChar) + "{" + cssTemplate.replace("%ID%", list[selector]) + "}"; + yield '}'; + } + }, + + /** + * Unapplies current stylesheet URL + */ + unapply: function() + { + if (ElemHide.applied) + { + try + { + Utils.styleService.unregisterSheet(styleURL, Ci.nsIStyleSheetService.USER_SHEET); + } + catch (e) + { + Cu.reportError(e); + } + ElemHide.applied = false; + } + }, + + /** + * Retrieves the currently applied stylesheet URL + * @type String + */ + get styleURL() + { + return ElemHide.applied ? styleURL.spec : null; + }, + + /** + * Retrieves an element hiding filter by the corresponding protocol key + */ + getFilterByKey: function(/**String*/ key) /**Filter*/ + { + return (key in filterByKey ? filterByKey[key] : null); + }, + + /** + * Returns a list of all selectors active on a particular domain (currently + * used only in Chrome, Opera and Safari). + */ + getSelectorsForDomain: function(/**String*/ domain, /**Boolean*/ specificOnly) + { + let result = []; + for (let key in filterByKey) + { + let filter = filterByKey[key]; + + // it is important to always access filter.domains + // here, even if it isn't used, in order to + // workaround WebKit bug 132872, also see #419 + let domains = filter.domains; + + if (specificOnly && (!domains || domains[""])) + continue; + + if (filter.isActiveOnDomain(domain) && !this.getException(filter, domain)) + result.push(filter.selector); + } + return result; + } +}; diff --git a/data/extensions/spyblock@gnu.org/lib/elemHideHitRegistration.js b/data/extensions/spyblock@gnu.org/lib/elemHideHitRegistration.js new file mode 100644 index 0000000..a05f6df --- /dev/null +++ b/data/extensions/spyblock@gnu.org/lib/elemHideHitRegistration.js @@ -0,0 +1,160 @@ +/* + * 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/>. + */ + +/** + * @fileOverview Hit counts for element hiding. + */ + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +let {Utils} = require("utils"); + +/** + * about: URL module used to count hits. + * @class + */ +let AboutHandler = exports.AboutHandler = +{ + classID: Components.ID("{55fb7be0-1dd2-11b2-98e6-9e97caf8ba67}"), + classDescription: "Element hiding hit registration protocol handler", + aboutPrefix: "abp-elemhidehit", + + /** + * Registers handler on startup. + */ + init: function() + { + let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar); + registrar.registerFactory(this.classID, this.classDescription, + "@mozilla.org/network/protocol/about;1?what=" + this.aboutPrefix, this); + onShutdown.add(function() + { + registrar.unregisterFactory(this.classID, this); + }.bind(this)); + }, + + // + // Factory implementation + // + + createInstance: function(outer, iid) + { + if (outer != null) + throw Cr.NS_ERROR_NO_AGGREGATION; + + return this.QueryInterface(iid); + }, + + // + // About module implementation + // + + getURIFlags: function(uri) + { + return ("HIDE_FROM_ABOUTABOUT" in Ci.nsIAboutModule ? Ci.nsIAboutModule.HIDE_FROM_ABOUTABOUT : 0); + }, + + newChannel: function(uri) + { + let match = /\?(\d+)/.exec(uri.path) + if (!match) + throw Cr.NS_ERROR_FAILURE; + + return new HitRegistrationChannel(uri, match[1]); + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIFactory, Ci.nsIAboutModule]) +}; +AboutHandler.init(); + +/** + * Channel returning data for element hiding hits. + * @constructor + */ +function HitRegistrationChannel(uri, key) +{ + this.key = key; + this.URI = this.originalURI = uri; +} +HitRegistrationChannel.prototype = { + key: null, + URI: null, + originalURI: null, + contentCharset: "utf-8", + contentLength: 0, + contentType: "text/xml", + owner: Utils.systemPrincipal, + securityInfo: null, + notificationCallbacks: null, + loadFlags: 0, + loadGroup: null, + name: null, + status: Cr.NS_OK, + + asyncOpen: function(listener, context) + { + let stream = this.open(); + Utils.runAsync(function() + { + try { + listener.onStartRequest(this, context); + } catch(e) {} + try { + listener.onDataAvailable(this, context, stream, 0, stream.available()); + } catch(e) {} + try { + listener.onStopRequest(this, context, Cr.NS_OK); + } catch(e) {} + }, this); + }, + + open: function() + { + let {Policy} = require("contentPolicy"); + let {ElemHide} = require("elemHide"); + let data = "<bindings xmlns='http://www.mozilla.org/xbl'><binding id='dummy'/></bindings>"; + let filter = ElemHide.getFilterByKey(this.key); + if (filter) + { + let wnd = Utils.getRequestWindow(this); + if (wnd && wnd.document && !Policy.processNode(wnd, wnd.document, Policy.type.ELEMHIDE, filter)) + data = "<bindings xmlns='http://www.mozilla.org/xbl'/>"; + } + + let stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(Ci.nsIStringInputStream); + stream.setData(data, data.length); + return stream; + }, + isPending: function() + { + return false; + }, + cancel: function() + { + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + }, + suspend: function() + { + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + }, + resume: function() + { + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIChannel, Ci.nsIRequest]) +}; diff --git a/data/extensions/spyblock@gnu.org/lib/filterClasses.js b/data/extensions/spyblock@gnu.org/lib/filterClasses.js new file mode 100644 index 0000000..cb3a5b1 --- /dev/null +++ b/data/extensions/spyblock@gnu.org/lib/filterClasses.js @@ -0,0 +1,906 @@ +/* + * 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/>. + */ + +/** + * @fileOverview Definition of Filter class and its subclasses. + */ + +let {FilterNotifier} = require("filterNotifier"); + +/** + * Abstract base class for filters + * + * @param {String} text string representation of the filter + * @constructor + */ +function Filter(text) +{ + this.text = text; + this.subscriptions = []; +} +exports.Filter = Filter; + +Filter.prototype = +{ + /** + * String representation of the filter + * @type String + */ + text: null, + + /** + * Filter subscriptions the filter belongs to + * @type Array of Subscription + */ + subscriptions: null, + + /** + * Serializes the filter to an array of strings for writing out on the disk. + * @param {Array of String} buffer buffer to push the serialization results into + */ + serialize: function(buffer) + { + buffer.push("[Filter]"); + buffer.push("text=" + this.text); + }, + + toString: function() + { + return this.text; + } +}; + +/** + * Cache for known filters, maps string representation to filter objects. + * @type Object + */ +Filter.knownFilters = {__proto__: null}; + +/** + * Regular expression that element hiding filters should match + * @type RegExp + */ +Filter.elemhideRegExp = /^([^\/\*\|\@"!]*?)#(\@)?(?:([\w\-]+|\*)((?:\([\w\-]+(?:[$^*]?=[^\(\)"]*)?\))*)|#([^{}]+))$/; +/** + * Regular expression that RegExp filters specified as RegExps should match + * @type RegExp + */ +Filter.regexpRegExp = /^(@@)?\/.*\/(?:\$~?[\w\-]+(?:=[^,\s]+)?(?:,~?[\w\-]+(?:=[^,\s]+)?)*)?$/; +/** + * Regular expression that options on a RegExp filter should match + * @type RegExp + */ +Filter.optionsRegExp = /\$(~?[\w\-]+(?:=[^,\s]+)?(?:,~?[\w\-]+(?:=[^,\s]+)?)*)$/; + +/** + * Creates a filter of correct type from its text representation - does the basic parsing and + * calls the right constructor then. + * + * @param {String} text as in Filter() + * @return {Filter} + */ +Filter.fromText = function(text) +{ + if (text in Filter.knownFilters) + return Filter.knownFilters[text]; + + let ret; + let match = (text.indexOf("#") >= 0 ? Filter.elemhideRegExp.exec(text) : null); + if (match) + ret = ElemHideBase.fromText(text, match[1], match[2], match[3], match[4], match[5]); + else if (text[0] == "!") + ret = new CommentFilter(text); + else + ret = RegExpFilter.fromText(text); + + Filter.knownFilters[ret.text] = ret; + return ret; +} + +/** + * Deserializes a filter + * + * @param {Object} obj map of serialized properties and their values + * @return {Filter} filter or null if the filter couldn't be created + */ +Filter.fromObject = function(obj) +{ + let ret = Filter.fromText(obj.text); + if (ret instanceof ActiveFilter) + { + if ("disabled" in obj) + ret._disabled = (obj.disabled == "true"); + if ("hitCount" in obj) + ret._hitCount = parseInt(obj.hitCount) || 0; + if ("lastHit" in obj) + ret._lastHit = parseInt(obj.lastHit) || 0; + } + return ret; +} + +/** + * Removes unnecessary whitespaces from filter text, will only return null if + * the input parameter is null. + */ +Filter.normalize = function(/**String*/ text) /**String*/ +{ + if (!text) + return text; + + // Remove line breaks and such + text = text.replace(/[^\S ]/g, ""); + + if (/^\s*!/.test(text)) + { + // Don't remove spaces inside comments + return text.replace(/^\s+/, "").replace(/\s+$/, ""); + } + else if (Filter.elemhideRegExp.test(text)) + { + // Special treatment for element hiding filters, right side is allowed to contain spaces + let [, domain, separator, selector] = /^(.*?)(#\@?#?)(.*)$/.exec(text); + return domain.replace(/\s/g, "") + separator + selector.replace(/^\s+/, "").replace(/\s+$/, ""); + } + else + return text.replace(/\s/g, ""); +} + +/** + * Class for invalid filters + * @param {String} text see Filter() + * @param {String} reason Reason why this filter is invalid + * @constructor + * @augments Filter + */ +function InvalidFilter(text, reason) +{ + Filter.call(this, text); + + this.reason = reason; +} +exports.InvalidFilter = InvalidFilter; + +InvalidFilter.prototype = +{ + __proto__: Filter.prototype, + + /** + * Reason why this filter is invalid + * @type String + */ + reason: null, + + /** + * See Filter.serialize() + */ + serialize: function(buffer) {} +}; + +/** + * Class for comments + * @param {String} text see Filter() + * @constructor + * @augments Filter + */ +function CommentFilter(text) +{ + Filter.call(this, text); +} +exports.CommentFilter = CommentFilter; + +CommentFilter.prototype = +{ + __proto__: Filter.prototype, + + /** + * See Filter.serialize() + */ + serialize: function(buffer) {} +}; + +/** + * Abstract base class for filters that can get hits + * @param {String} text see Filter() + * @param {String} domains (optional) Domains that the filter is restricted to separated by domainSeparator e.g. "foo.com|bar.com|~baz.com" + * @constructor + * @augments Filter + */ +function ActiveFilter(text, domains) +{ + Filter.call(this, text); + + this.domainSource = domains; +} +exports.ActiveFilter = ActiveFilter; + +ActiveFilter.prototype = +{ + __proto__: Filter.prototype, + + _disabled: false, + _hitCount: 0, + _lastHit: 0, + + /** + * Defines whether the filter is disabled + * @type Boolean + */ + get disabled() + { + return this._disabled; + }, + set disabled(value) + { + if (value != this._disabled) + { + let oldValue = this._disabled; + this._disabled = value; + FilterNotifier.triggerListeners("filter.disabled", this, value, oldValue); + } + return this._disabled; + }, + + /** + * Number of hits on the filter since the last reset + * @type Number + */ + get hitCount() + { + return this._hitCount; + }, + set hitCount(value) + { + if (value != this._hitCount) + { + let oldValue = this._hitCount; + this._hitCount = value; + FilterNotifier.triggerListeners("filter.hitCount", this, value, oldValue); + } + return this._hitCount; + }, + + /** + * Last time the filter had a hit (in milliseconds since the beginning of the epoch) + * @type Number + */ + get lastHit() + { + return this._lastHit; + }, + set lastHit(value) + { + if (value != this._lastHit) + { + let oldValue = this._lastHit; + this._lastHit = value; + FilterNotifier.triggerListeners("filter.lastHit", this, value, oldValue); + } + return this._lastHit; + }, + + /** + * String that the domains property should be generated from + * @type String + */ + domainSource: null, + + /** + * Separator character used in domainSource property, must be overridden by subclasses + * @type String + */ + domainSeparator: null, + + /** + * Determines whether the trailing dot in domain names isn't important and + * should be ignored, must be overridden by subclasses. + * @type Boolean + */ + ignoreTrailingDot: true, + + /** + * Determines whether domainSource is already upper-case, + * can be overridden by subclasses. + * @type Boolean + */ + domainSourceIsUpperCase: false, + + /** + * Map containing domains that this filter should match on/not match on or null if the filter should match on all domains + * @type Object + */ + get domains() + { + let domains = null; + + if (this.domainSource) + { + let source = this.domainSource; + if (!this.domainSourceIsUpperCase) { + // RegExpFilter already have uppercase domains + source = source.toUpperCase(); + } + let list = source.split(this.domainSeparator); + if (list.length == 1 && list[0][0] != "~") + { + // Fast track for the common one-domain scenario + domains = {__proto__: null, "": false}; + if (this.ignoreTrailingDot) + list[0] = list[0].replace(/\.+$/, ""); + domains[list[0]] = true; + } + else + { + let hasIncludes = false; + for (let i = 0; i < list.length; i++) + { + let domain = list[i]; + if (this.ignoreTrailingDot) + domain = domain.replace(/\.+$/, ""); + if (domain == "") + continue; + + let include; + if (domain[0] == "~") + { + include = false; + domain = domain.substr(1); + } + else + { + include = true; + hasIncludes = true; + } + + if (!domains) + domains = {__proto__: null}; + + domains[domain] = include; + } + domains[""] = !hasIncludes; + } + + this.domainSource = null; + } + + Object.defineProperty(this, "domains", {value: domains, enumerable: true}); + return this.domains; + }, + + /** + * Checks whether this filter is active on a domain. + */ + isActiveOnDomain: function(/**String*/ docDomain) /**Boolean*/ + { + // If no domains are set the rule matches everywhere + if (!this.domains) + return true; + + // If the document has no host name, match only if the filter isn't restricted to specific domains + if (!docDomain) + return this.domains[""]; + + if (this.ignoreTrailingDot) + docDomain = docDomain.replace(/\.+$/, ""); + docDomain = docDomain.toUpperCase(); + + while (true) + { + if (docDomain in this.domains) + return this.domains[docDomain]; + + let nextDot = docDomain.indexOf("."); + if (nextDot < 0) + break; + docDomain = docDomain.substr(nextDot + 1); + } + return this.domains[""]; + }, + + /** + * Checks whether this filter is active only on a domain and its subdomains. + */ + isActiveOnlyOnDomain: function(/**String*/ docDomain) /**Boolean*/ + { + if (!docDomain || !this.domains || this.domains[""]) + return false; + + if (this.ignoreTrailingDot) + docDomain = docDomain.replace(/\.+$/, ""); + docDomain = docDomain.toUpperCase(); + + for (let domain in this.domains) + if (this.domains[domain] && domain != docDomain && (domain.length <= docDomain.length || domain.indexOf("." + docDomain) != domain.length - docDomain.length - 1)) + return false; + + return true; + }, + + /** + * See Filter.serialize() + */ + serialize: function(buffer) + { + if (this._disabled || this._hitCount || this._lastHit) + { + Filter.prototype.serialize.call(this, buffer); + if (this._disabled) + buffer.push("disabled=true"); + if (this._hitCount) + buffer.push("hitCount=" + this._hitCount); + if (this._lastHit) + buffer.push("lastHit=" + this._lastHit); + } + } +}; + +/** + * Abstract base class for RegExp-based filters + * @param {String} text see Filter() + * @param {String} regexpSource filter part that the regular expression should be build from + * @param {Number} contentType (optional) Content types the filter applies to, combination of values from RegExpFilter.typeMap + * @param {Boolean} matchCase (optional) Defines whether the filter should distinguish between lower and upper case letters + * @param {String} domains (optional) Domains that the filter is restricted to, e.g. "foo.com|bar.com|~baz.com" + * @param {Boolean} thirdParty (optional) Defines whether the filter should apply to third-party or first-party content only + * @constructor + * @augments ActiveFilter + */ +function RegExpFilter(text, regexpSource, contentType, matchCase, domains, thirdParty) +{ + ActiveFilter.call(this, text, domains); + + if (contentType != null) + this.contentType = contentType; + if (matchCase) + this.matchCase = matchCase; + if (thirdParty != null) + this.thirdParty = thirdParty; + + if (regexpSource.length >= 2 && regexpSource[0] == "/" && regexpSource[regexpSource.length - 1] == "/") + { + // The filter is a regular expression - convert it immediately to catch syntax errors + let regexp = new RegExp(regexpSource.substr(1, regexpSource.length - 2), this.matchCase ? "" : "i"); + this.__defineGetter__("regexp", () => regexp); + } + else + { + // No need to convert this filter to regular expression yet, do it on demand + this.regexpSource = regexpSource; + } +} +exports.RegExpFilter = RegExpFilter; + +RegExpFilter.prototype = +{ + __proto__: ActiveFilter.prototype, + + /** + * @see ActiveFilter.domainSourceIsUpperCase + */ + domainSourceIsUpperCase: true, + + /** + * Number of filters contained, will always be 1 (required to optimize Matcher). + * @type Integer + */ + length: 1, + + /** + * @see ActiveFilter.domainSeparator + */ + domainSeparator: "|", + + /** + * Expression from which a regular expression should be generated - for delayed creation of the regexp property + * @type String + */ + regexpSource: null, + /** + * Regular expression to be used when testing against this filter + * @type RegExp + */ + get regexp() + { + // Remove multiple wildcards + let source = this.regexpSource + .replace(/\*+/g, "*") // remove multiple wildcards + .replace(/\^\|$/, "^") // remove anchors following separator placeholder + .replace(/\W/g, "\\$&") // escape special symbols + .replace(/\\\*/g, ".*") // replace wildcards by .* + // process separator placeholders (all ANSI characters but alphanumeric characters and _%.-) + .replace(/\\\^/g, "(?:[\\x00-\\x24\\x26-\\x2C\\x2F\\x3A-\\x40\\x5B-\\x5E\\x60\\x7B-\\x7F]|$)") + .replace(/^\\\|\\\|/, "^[\\w\\-]+:\\/+(?!\\/)(?:[^\\/]+\\.)?") // process extended anchor at expression start + .replace(/^\\\|/, "^") // process anchor at expression start + .replace(/\\\|$/, "$") // process anchor at expression end + .replace(/^(\.\*)/, "") // remove leading wildcards + .replace(/(\.\*)$/, ""); // remove trailing wildcards + + let regexp = new RegExp(source, this.matchCase ? "" : "i"); + + delete this.regexpSource; + this.__defineGetter__("regexp", () => regexp); + return this.regexp; + }, + /** + * Content types the filter applies to, combination of values from RegExpFilter.typeMap + * @type Number + */ + contentType: 0x7FFFFFFF, + /** + * Defines whether the filter should distinguish between lower and upper case letters + * @type Boolean + */ + matchCase: false, + /** + * Defines whether the filter should apply to third-party or first-party content only. Can be null (apply to all content). + * @type Boolean + */ + thirdParty: null, + + /** + * Tests whether the URL matches this filter + * @param {String} location URL to be tested + * @param {String} contentType content type identifier of the URL + * @param {String} docDomain domain name of the document that loads the URL + * @param {Boolean} thirdParty should be true if the URL is a third-party request + * @return {Boolean} true in case of a match + */ + matches: function(location, contentType, docDomain, thirdParty, privatenode) + { + + if(this.subscriptions[0]) + if (this.subscriptions[0].privateMode) + if (privatenode==false) + return false; + + if (this.regexp.test(location) && + (RegExpFilter.typeMap[contentType] & this.contentType) != 0 && + (this.thirdParty == null || this.thirdParty == thirdParty) && + this.isActiveOnDomain(docDomain)) + { + return true; + } + + return false; + } +}; + +RegExpFilter.prototype.__defineGetter__("0", function() +{ + return this; +}); + +/** + * Creates a RegExp filter from its text representation + * @param {String} text same as in Filter() + */ +RegExpFilter.fromText = function(text) +{ + let blocking = true; + let origText = text; + if (text.indexOf("@@") == 0) + { + blocking = false; + text = text.substr(2); + } + + let contentType = null; + let matchCase = null; + let domains = null; + let siteKeys = null; + let thirdParty = null; + let collapse = null; + let options; + let match = (text.indexOf("$") >= 0 ? Filter.optionsRegExp.exec(text) : null); + if (match) + { + options = match[1].toUpperCase().split(","); + text = match.input.substr(0, match.index); + for (let option of options) + { + let value = null; + let separatorIndex = option.indexOf("="); + if (separatorIndex >= 0) + { + value = option.substr(separatorIndex + 1); + option = option.substr(0, separatorIndex); + } + option = option.replace(/-/, "_"); + if (option in RegExpFilter.typeMap) + { + if (contentType == null) + contentType = 0; + contentType |= RegExpFilter.typeMap[option]; + } + else if (option[0] == "~" && option.substr(1) in RegExpFilter.typeMap) + { + if (contentType == null) + contentType = RegExpFilter.prototype.contentType; + contentType &= ~RegExpFilter.typeMap[option.substr(1)]; + } + else if (option == "MATCH_CASE") + matchCase = true; + else if (option == "~MATCH_CASE") + matchCase = false; + else if (option == "DOMAIN" && typeof value != "undefined") + domains = value; + else if (option == "THIRD_PARTY") + thirdParty = true; + else if (option == "~THIRD_PARTY") + thirdParty = false; + else if (option == "COLLAPSE") + collapse = true; + else if (option == "~COLLAPSE") + collapse = false; + else if (option == "SITEKEY" && typeof value != "undefined") + siteKeys = value.split(/\|/); + else + return new InvalidFilter(origText, "Unknown option " + option.toLowerCase()); + } + } + + if (!blocking && (contentType == null || (contentType & RegExpFilter.typeMap.DOCUMENT)) && + (!options || options.indexOf("DOCUMENT") < 0) && !/^\|?[\w\-]+:/.test(text)) + { + // Exception filters shouldn't apply to pages by default unless they start with a protocol name + if (contentType == null) + contentType = RegExpFilter.prototype.contentType; + contentType &= ~RegExpFilter.typeMap.DOCUMENT; + } + if (!blocking && siteKeys) + contentType = RegExpFilter.typeMap.DOCUMENT; + + try + { + if (blocking) + return new BlockingFilter(origText, text, contentType, matchCase, domains, thirdParty, collapse); + else + return new WhitelistFilter(origText, text, contentType, matchCase, domains, thirdParty, siteKeys); + } + catch (e) + { + return new InvalidFilter(origText, e); + } +} + +/** + * Maps type strings like "SCRIPT" or "OBJECT" to bit masks + */ +RegExpFilter.typeMap = { + OTHER: 1, + SCRIPT: 2, + IMAGE: 4, + STYLESHEET: 8, + OBJECT: 16, + SUBDOCUMENT: 32, + DOCUMENT: 64, + XBL: 1, + PING: 1, + XMLHTTPREQUEST: 2048, + OBJECT_SUBREQUEST: 4096, + DTD: 1, + MEDIA: 16384, + FONT: 32768, + + BACKGROUND: 4, // Backwards compat, same as IMAGE + + POPUP: 0x10000000, + ELEMHIDE: 0x40000000 +}; + +// ELEMHIDE, POPUP option shouldn't be there by default +RegExpFilter.prototype.contentType &= ~(RegExpFilter.typeMap.ELEMHIDE | RegExpFilter.typeMap.POPUP); + +/** + * Class for blocking filters + * @param {String} text see Filter() + * @param {String} regexpSource see RegExpFilter() + * @param {Number} contentType see RegExpFilter() + * @param {Boolean} matchCase see RegExpFilter() + * @param {String} domains see RegExpFilter() + * @param {Boolean} thirdParty see RegExpFilter() + * @param {Boolean} collapse defines whether the filter should collapse blocked content, can be null + * @constructor + * @augments RegExpFilter + */ +function BlockingFilter(text, regexpSource, contentType, matchCase, domains, thirdParty, collapse) +{ + RegExpFilter.call(this, text, regexpSource, contentType, matchCase, domains, thirdParty); + + this.collapse = collapse; +} +exports.BlockingFilter = BlockingFilter; + +BlockingFilter.prototype = +{ + __proto__: RegExpFilter.prototype, + + /** + * Defines whether the filter should collapse blocked content. Can be null (use the global preference). + * @type Boolean + */ + collapse: null +}; + +/** + * Class for whitelist filters + * @param {String} text see Filter() + * @param {String} regexpSource see RegExpFilter() + * @param {Number} contentType see RegExpFilter() + * @param {Boolean} matchCase see RegExpFilter() + * @param {String} domains see RegExpFilter() + * @param {Boolean} thirdParty see RegExpFilter() + * @param {String[]} siteKeys public keys of websites that this filter should apply to + * @constructor + * @augments RegExpFilter + */ +function WhitelistFilter(text, regexpSource, contentType, matchCase, domains, thirdParty, siteKeys) +{ + RegExpFilter.call(this, text, regexpSource, contentType, matchCase, domains, thirdParty); + + if (siteKeys != null) + this.siteKeys = siteKeys; +} +exports.WhitelistFilter = WhitelistFilter; + +WhitelistFilter.prototype = +{ + __proto__: RegExpFilter.prototype, + + /** + * List of public keys of websites that this filter should apply to + * @type String[] + */ + siteKeys: null +} + +/** + * Base class for element hiding filters + * @param {String} text see Filter() + * @param {String} domains (optional) Host names or domains the filter should be restricted to + * @param {String} selector CSS selector for the HTML elements that should be hidden + * @constructor + * @augments ActiveFilter + */ +function ElemHideBase(text, domains, selector) +{ + ActiveFilter.call(this, text, domains || null); + + if (domains) + this.selectorDomain = domains.replace(/,~[^,]+/g, "").replace(/^~[^,]+,?/, "").toLowerCase(); + this.selector = selector; +} +exports.ElemHideBase = ElemHideBase; + +ElemHideBase.prototype = +{ + __proto__: ActiveFilter.prototype, + + /** + * @see ActiveFilter.domainSeparator + */ + domainSeparator: ",", + + /** + * @see ActiveFilter.ignoreTrailingDot + */ + ignoreTrailingDot: false, + + /** + * Host name or domain the filter should be restricted to (can be null for no restriction) + * @type String + */ + selectorDomain: null, + /** + * CSS selector for the HTML elements that should be hidden + * @type String + */ + selector: null +}; + +/** + * Creates an element hiding filter from a pre-parsed text representation + * + * @param {String} text same as in Filter() + * @param {String} domain domain part of the text representation (can be empty) + * @param {String} tagName tag name part (can be empty) + * @param {String} attrRules attribute matching rules (can be empty) + * @param {String} selector raw CSS selector (can be empty) + * @return {ElemHideFilter|ElemHideException|InvalidFilter} + */ +ElemHideBase.fromText = function(text, domain, isException, tagName, attrRules, selector) +{ + if (!selector) + { + if (tagName == "*") + tagName = ""; + + let id = null; + let additional = ""; + if (attrRules) { + attrRules = attrRules.match(/\([\w\-]+(?:[$^*]?=[^\(\)"]*)?\)/g); + for (let rule of attrRules) { + rule = rule.substr(1, rule.length - 2); + let separatorPos = rule.indexOf("="); + if (separatorPos > 0) { + rule = rule.replace(/=/, '="') + '"'; + additional += "[" + rule + "]"; + } + else { + if (id) + { + let {Utils} = require("utils"); + return new InvalidFilter(text, Utils.getString("filter_elemhide_duplicate_id")); + } + else + id = rule; + } + } + } + + if (id) + selector = tagName + "." + id + additional + "," + tagName + "#" + id + additional; + else if (tagName || additional) + selector = tagName + additional; + else + { + let {Utils} = require("utils"); + return new InvalidFilter(text, Utils.getString("filter_elemhide_nocriteria")); + } + } + if (isException) + return new ElemHideException(text, domain, selector); + else + return new ElemHideFilter(text, domain, selector); +} + +/** + * Class for element hiding filters + * @param {String} text see Filter() + * @param {String} domains see ElemHideBase() + * @param {String} selector see ElemHideBase() + * @constructor + * @augments ElemHideBase + */ +function ElemHideFilter(text, domains, selector) +{ + ElemHideBase.call(this, text, domains, selector); +} +exports.ElemHideFilter = ElemHideFilter; + +ElemHideFilter.prototype = +{ + __proto__: ElemHideBase.prototype +}; + +/** + * Class for element hiding exceptions + * @param {String} text see Filter() + * @param {String} domains see ElemHideBase() + * @param {String} selector see ElemHideBase() + * @constructor + * @augments ElemHideBase + */ +function ElemHideException(text, domains, selector) +{ + ElemHideBase.call(this, text, domains, selector); +} +exports.ElemHideException = ElemHideException; + +ElemHideException.prototype = +{ + __proto__: ElemHideBase.prototype +}; diff --git a/data/extensions/spyblock@gnu.org/lib/filterListener.js b/data/extensions/spyblock@gnu.org/lib/filterListener.js new file mode 100644 index 0000000..1687a26 --- /dev/null +++ b/data/extensions/spyblock@gnu.org/lib/filterListener.js @@ -0,0 +1,282 @@ +/* + * 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/>. + */ + +/** + * @fileOverview Component synchronizing filter storage with Matcher instances and ElemHide. + */ + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +let {TimeLine} = require("timeline"); +let {FilterStorage} = require("filterStorage"); +let {FilterNotifier} = require("filterNotifier"); +let {ElemHide} = require("elemHide"); +let {defaultMatcher} = require("matcher"); +let {ActiveFilter, RegExpFilter, ElemHideBase} = require("filterClasses"); +let {Prefs} = require("prefs"); + +/** + * Value of the FilterListener.batchMode property. + * @type Boolean + */ +let batchMode = false; + +/** + * Increases on filter changes, filters will be saved if it exceeds 1. + * @type Integer + */ +let isDirty = 0; + +/** + * This object can be used to change properties of the filter change listeners. + * @class + */ +let FilterListener = +{ + /** + * Set to true when executing many changes, changes will only be fully applied after this variable is set to false again. + * @type Boolean + */ + get batchMode() + { + return batchMode; + }, + set batchMode(value) + { + batchMode = value; + flushElemHide(); + }, + + /** + * Increases "dirty factor" of the filters and calls FilterStorage.saveToDisk() + * if it becomes 1 or more. Save is executed delayed to prevent multiple + * subsequent calls. If the parameter is 0 it forces saving filters if any + * changes were recorded after the previous save. + */ + setDirty: function(/**Integer*/ factor) + { + if (factor == 0 && isDirty > 0) + isDirty = 1; + else + isDirty += factor; + if (isDirty >= 1) + FilterStorage.saveToDisk(); + } +}; + +/** + * Observer listening to history purge actions. + * @class + */ +let HistoryPurgeObserver = +{ + observe: function(subject, topic, data) + { + if (topic == "browser:purge-session-history" && Prefs.clearStatsOnHistoryPurge) + { + FilterStorage.resetHitCounts(); + FilterListener.setDirty(0); // Force saving to disk + + Prefs.recentReports = []; + } + }, + QueryInterface: XPCOMUtils.generateQI([Ci.nsISupportsWeakReference, Ci.nsIObserver]) +}; + +/** + * Initializes filter listener on startup, registers the necessary hooks. + */ +function init() +{ + TimeLine.enter("Entered filter listener initialization()"); + + FilterNotifier.addListener(function(action, item, newValue, oldValue) + { + let match = /^(\w+)\.(.*)/.exec(action); + if (match && match[1] == "filter") + onFilterChange(match[2], item, newValue, oldValue); + else if (match && match[1] == "subscription") + onSubscriptionChange(match[2], item, newValue, oldValue); + else + onGenericChange(action, item); + }); + + if ("nsIStyleSheetService" in Ci) + ElemHide.init(); + else + flushElemHide = function() {}; // No global stylesheet in Chrome & Co. + FilterStorage.loadFromDisk(); + + TimeLine.log("done initializing data structures"); + + Services.obs.addObserver(HistoryPurgeObserver, "browser:purge-session-history", true); + onShutdown.add(function() + { + Services.obs.removeObserver(HistoryPurgeObserver, "browser:purge-session-history"); + }); + TimeLine.log("done adding observers"); + + TimeLine.leave("Filter listener initialization done"); +} +init(); + +/** + * Calls ElemHide.apply() if necessary. + */ +function flushElemHide() +{ + if (!batchMode && ElemHide.isDirty) + ElemHide.apply(); +} + +/** + * Notifies Matcher instances or ElemHide object about a new filter + * if necessary. + * @param {Filter} filter filter that has been added + */ +function addFilter(filter) +{ + if (!(filter instanceof ActiveFilter) || filter.disabled) + return; + + let hasEnabled = false; + for (let i = 0; i < filter.subscriptions.length; i++) + if (!filter.subscriptions[i].disabled) + hasEnabled = true; + if (!hasEnabled) + return; + + if (filter instanceof RegExpFilter) + defaultMatcher.add(filter); + else if (filter instanceof ElemHideBase) + ElemHide.add(filter); +} + +/** + * Notifies Matcher instances or ElemHide object about removal of a filter + * if necessary. + * @param {Filter} filter filter that has been removed + */ +function removeFilter(filter) +{ + if (!(filter instanceof ActiveFilter)) + return; + + if (!filter.disabled) + { + let hasEnabled = false; + for (let i = 0; i < filter.subscriptions.length; i++) + if (!filter.subscriptions[i].disabled) + hasEnabled = true; + if (hasEnabled) + return; + } + + if (filter instanceof RegExpFilter) + defaultMatcher.remove(filter); + else if (filter instanceof ElemHideBase) + ElemHide.remove(filter); +} + +/** + * Subscription change listener + */ +function onSubscriptionChange(action, subscription, newValue, oldValue) +{ + FilterListener.setDirty(1); + + if (action != "added" && action != "removed" && action != "disabled" && action != "updated") + return; + + if (action != "removed" && !(subscription.url in FilterStorage.knownSubscriptions)) + { + // Ignore updates for subscriptions not in the list + return; + } + + if ((action == "added" || action == "removed" || action == "updated") && subscription.disabled) + { + // Ignore adding/removing/updating of disabled subscriptions + return; + } + + if (action == "added" || action == "removed" || action == "disabled") + { + let method = (action == "added" || (action == "disabled" && newValue == false) ? addFilter : removeFilter); + if (subscription.filters) + subscription.filters.forEach(method); + } + else if (action == "updated") + { + subscription.oldFilters.forEach(removeFilter); + subscription.filters.forEach(addFilter); + } + + flushElemHide(); +} + +/** + * Filter change listener + */ +function onFilterChange(action, filter, newValue, oldValue) +{ + if (action == "hitCount" && newValue == 0) + { + // Filter hits are being reset, make sure these changes are saved. + FilterListener.setDirty(0); + } + else if (action == "hitCount" || action == "lastHit") + FilterListener.setDirty(0.002); + else + FilterListener.setDirty(1); + + if (action != "added" && action != "removed" && action != "disabled") + return; + + if ((action == "added" || action == "removed") && filter.disabled) + { + // Ignore adding/removing of disabled filters + return; + } + + if (action == "added" || (action == "disabled" && newValue == false)) + addFilter(filter); + else + removeFilter(filter); + flushElemHide(); +} + +/** + * Generic notification listener + */ +function onGenericChange(action) +{ + if (action == "load") + { + isDirty = 0; + + defaultMatcher.clear(); + ElemHide.clear(); + for (let subscription of FilterStorage.subscriptions) + if (!subscription.disabled) + subscription.filters.forEach(addFilter); + flushElemHide(); + } + else if (action == "save") + isDirty = 0; +} diff --git a/data/extensions/spyblock@gnu.org/lib/filterNotifier.js b/data/extensions/spyblock@gnu.org/lib/filterNotifier.js new file mode 100644 index 0000000..010081e --- /dev/null +++ b/data/extensions/spyblock@gnu.org/lib/filterNotifier.js @@ -0,0 +1,73 @@ +/* + * 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/>. + */ + +/** + * @fileOverview This component manages listeners and calls them to distributes + * messages about filter changes. + */ + +/** + * List of registered listeners + * @type Array of function(action, item, newValue, oldValue) + */ +let listeners = []; + +/** + * This class allows registering and triggering listeners for filter events. + * @class + */ +let FilterNotifier = exports.FilterNotifier = +{ + /** + * Adds a listener + */ + addListener: function(/**function(action, item, newValue, oldValue)*/ listener) + { + if (listeners.indexOf(listener) >= 0) + return; + + listeners.push(listener); + }, + + /** + * Removes a listener that was previosly added via addListener + */ + removeListener: function(/**function(action, item, newValue, oldValue)*/ listener) + { + let index = listeners.indexOf(listener); + if (index >= 0) + listeners.splice(index, 1); + }, + + /** + * Notifies listeners about an event + * @param {String} action event code ("load", "save", "elemhideupdate", + * "subscription.added", "subscription.removed", + * "subscription.disabled", "subscription.title", + * "subscription.lastDownload", "subscription.downloadStatus", + * "subscription.homepage", "subscription.updated", + * "filter.added", "filter.removed", "filter.moved", + * "filter.disabled", "filter.hitCount", "filter.lastHit") + * @param {Subscription|Filter} item item that the change applies to + */ + triggerListeners: function(action, item, param1, param2, param3) + { + let list = listeners.slice(); + for (let listener of list) + listener(action, item, param1, param2, param3); + } +}; diff --git a/data/extensions/spyblock@gnu.org/lib/filterStorage.js b/data/extensions/spyblock@gnu.org/lib/filterStorage.js new file mode 100644 index 0000000..546f788 --- /dev/null +++ b/data/extensions/spyblock@gnu.org/lib/filterStorage.js @@ -0,0 +1,897 @@ +/* + * 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/>. + */ + +/** + * @fileOverview FilterStorage class responsible for managing user's subscriptions and filters. + */ + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/FileUtils.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +let {IO} = require("io"); +let {Prefs} = require("prefs"); +let {Filter, ActiveFilter} = require("filterClasses"); +let {Subscription, SpecialSubscription, ExternalSubscription} = require("subscriptionClasses"); +let {FilterNotifier} = require("filterNotifier"); +let {Utils} = require("utils"); +let {TimeLine} = require("timeline"); + +/** + * Version number of the filter storage file format. + * @type Integer + */ +let formatVersion = 4; + +/** + * This class reads user's filters from disk, manages them in memory and writes them back. + * @class + */ +let FilterStorage = exports.FilterStorage = +{ + /** + * Version number of the patterns.ini format used. + * @type Integer + */ + get formatVersion() + { + return formatVersion; + }, + + /** + * File that the filter list has been loaded from and should be saved to + * @type nsIFile + */ + get sourceFile() + { + let file = null; + if (Prefs.patternsfile) + { + // Override in place, use it instead of placing the file in the regular data dir + file = IO.resolveFilePath(Prefs.patternsfile); + } + if (!file) + { + // Place the file in the data dir + file = IO.resolveFilePath(Prefs.data_directory); + if (file) + file.append("patterns.ini"); + } + if (!file) + { + // Data directory pref misconfigured? Try the default value + try + { + file = IO.resolveFilePath(Services.prefs.getDefaultBranch("extensions.adblockplus.").getCharPref("data_directory")); + if (file) + file.append("patterns.ini"); + } catch(e) {} + } + + if (!file) + Cu.reportError("Adblock Plus: Failed to resolve filter file location from extensions.adblockplus.patternsfile preference"); + + this.__defineGetter__("sourceFile", () => file); + return this.sourceFile; + }, + + /** + * Will be set to true if no patterns.ini file exists. + * @type Boolean + */ + firstRun: false, + + /** + * Map of properties listed in the filter storage file before the sections + * start. Right now this should be only the format version. + */ + fileProperties: {__proto__: null}, + + /** + * List of filter subscriptions containing all filters + * @type Array of Subscription + */ + subscriptions: [], + + /** + * Map of subscriptions already on the list, by their URL/identifier + * @type Object + */ + knownSubscriptions: {__proto__: null}, + + /** + * Finds the filter group that a filter should be added to by default. Will + * return null if this group doesn't exist yet. + */ + getGroupForFilter: function(/**Filter*/ filter) /**SpecialSubscription*/ + { + let generalSubscription = null; + for (let subscription of FilterStorage.subscriptions) + { + if (subscription instanceof SpecialSubscription && !subscription.disabled) + { + // Always prefer specialized subscriptions + if (subscription.isDefaultFor(filter)) + return subscription; + + // If this is a general subscription - store it as fallback + if (!generalSubscription && (!subscription.defaults || !subscription.defaults.length)) + generalSubscription = subscription; + } + } + return generalSubscription; + }, + + /** + * Adds a filter subscription to the list + * @param {Subscription} subscription filter subscription to be added + * @param {Boolean} silent if true, no listeners will be triggered (to be used when filter list is reloaded) + */ + addSubscription: function(subscription, silent) + { + if (subscription.url in FilterStorage.knownSubscriptions) + return; + + FilterStorage.subscriptions.push(subscription); + FilterStorage.knownSubscriptions[subscription.url] = subscription; + addSubscriptionFilters(subscription); + + if (!silent) + FilterNotifier.triggerListeners("subscription.added", subscription); + }, + + /** + * Removes a filter subscription from the list + * @param {Subscription} subscription filter subscription to be removed + * @param {Boolean} silent if true, no listeners will be triggered (to be used when filter list is reloaded) + */ + removeSubscription: function(subscription, silent) + { + for (let i = 0; i < FilterStorage.subscriptions.length; i++) + { + if (FilterStorage.subscriptions[i].url == subscription.url) + { + removeSubscriptionFilters(subscription); + + FilterStorage.subscriptions.splice(i--, 1); + delete FilterStorage.knownSubscriptions[subscription.url]; + if (!silent) + FilterNotifier.triggerListeners("subscription.removed", subscription); + return; + } + } + }, + + /** + * Moves a subscription in the list to a new position. + * @param {Subscription} subscription filter subscription to be moved + * @param {Subscription} [insertBefore] filter subscription to insert before + * (if omitted the subscription will be put at the end of the list) + */ + moveSubscription: function(subscription, insertBefore) + { + let currentPos = FilterStorage.subscriptions.indexOf(subscription); + if (currentPos < 0) + return; + + let newPos = insertBefore ? FilterStorage.subscriptions.indexOf(insertBefore) : -1; + if (newPos < 0) + newPos = FilterStorage.subscriptions.length; + + if (currentPos < newPos) + newPos--; + if (currentPos == newPos) + return; + + FilterStorage.subscriptions.splice(currentPos, 1); + FilterStorage.subscriptions.splice(newPos, 0, subscription); + FilterNotifier.triggerListeners("subscription.moved", subscription); + }, + + /** + * Replaces the list of filters in a subscription by a new list + * @param {Subscription} subscription filter subscription to be updated + * @param {Array of Filter} filters new filter lsit + */ + updateSubscriptionFilters: function(subscription, filters) + { + removeSubscriptionFilters(subscription); + subscription.oldFilters = subscription.filters; + subscription.filters = filters; + addSubscriptionFilters(subscription); + FilterNotifier.triggerListeners("subscription.updated", subscription); + delete subscription.oldFilters; + }, + + /** + * Adds a user-defined filter to the list + * @param {Filter} filter + * @param {SpecialSubscription} [subscription] particular group that the filter should be added to + * @param {Integer} [position] position within the subscription at which the filter should be added + * @param {Boolean} silent if true, no listeners will be triggered (to be used when filter list is reloaded) + */ + addFilter: function(filter, subscription, position, silent) + { + if (!subscription) + { + if (filter.subscriptions.some(s => s instanceof SpecialSubscription && !s.disabled)) + return; // No need to add + subscription = FilterStorage.getGroupForFilter(filter); + } + if (!subscription) + { + // No group for this filter exists, create one + subscription = SpecialSubscription.createForFilter(filter); + this.addSubscription(subscription); + return; + } + + if (typeof position == "undefined") + position = subscription.filters.length; + + if (filter.subscriptions.indexOf(subscription) < 0) + filter.subscriptions.push(subscription); + subscription.filters.splice(position, 0, filter); + if (!silent) + FilterNotifier.triggerListeners("filter.added", filter, subscription, position); + }, + + /** + * Removes a user-defined filter from the list + * @param {Filter} filter + * @param {SpecialSubscription} [subscription] a particular filter group that + * the filter should be removed from (if ommited will be removed from all subscriptions) + * @param {Integer} [position] position inside the filter group at which the + * filter should be removed (if ommited all instances will be removed) + */ + removeFilter: function(filter, subscription, position) + { + let subscriptions = (subscription ? [subscription] : filter.subscriptions.slice()); + for (let i = 0; i < subscriptions.length; i++) + { + let subscription = subscriptions[i]; + if (subscription instanceof SpecialSubscription) + { + let positions = []; + if (typeof position == "undefined") + { + let index = -1; + do + { + index = subscription.filters.indexOf(filter, index + 1); + if (index >= 0) + positions.push(index); + } while (index >= 0); + } + else + positions.push(position); + + for (let j = positions.length - 1; j >= 0; j--) + { + let position = positions[j]; + if (subscription.filters[position] == filter) + { + subscription.filters.splice(position, 1); + if (subscription.filters.indexOf(filter) < 0) + { + let index = filter.subscriptions.indexOf(subscription); + if (index >= 0) + filter.subscriptions.splice(index, 1); + } + FilterNotifier.triggerListeners("filter.removed", filter, subscription, position); + } + } + } + } + }, + + /** + * Moves a user-defined filter to a new position + * @param {Filter} filter + * @param {SpecialSubscription} subscription filter group where the filter is located + * @param {Integer} oldPosition current position of the filter + * @param {Integer} newPosition new position of the filter + */ + moveFilter: function(filter, subscription, oldPosition, newPosition) + { + if (!(subscription instanceof SpecialSubscription) || subscription.filters[oldPosition] != filter) + return; + + newPosition = Math.min(Math.max(newPosition, 0), subscription.filters.length - 1); + if (oldPosition == newPosition) + return; + + subscription.filters.splice(oldPosition, 1); + subscription.filters.splice(newPosition, 0, filter); + FilterNotifier.triggerListeners("filter.moved", filter, subscription, oldPosition, newPosition); + }, + + /** + * Increases the hit count for a filter by one + * @param {Filter} filter + * @param {Window} window Window that the match originated in (required + * to recognize private browsing mode) + */ + increaseHitCount: function(filter, wnd) + { + if (!Prefs.savestats || PrivateBrowsing.enabledForWindow(wnd) || + PrivateBrowsing.enabled || !(filter instanceof ActiveFilter)) + { + return; + } + + filter.hitCount++; + filter.lastHit = Date.now(); + }, + + /** + * Resets hit count for some filters + * @param {Array of Filter} filters filters to be reset, if null all filters will be reset + */ + resetHitCounts: function(filters) + { + if (!filters) + { + filters = []; + for (let text in Filter.knownFilters) + filters.push(Filter.knownFilters[text]); + } + for (let filter of filters) + { + filter.hitCount = 0; + filter.lastHit = 0; + } + }, + + _loading: false, + + /** + * Loads all subscriptions from the disk + * @param {nsIFile} [sourceFile] File to read from + */ + loadFromDisk: function(sourceFile) + { + if (this._loading) + return; + + TimeLine.enter("Entered FilterStorage.loadFromDisk()"); + this._loading = true; + + let readFile = function(sourceFile, backupIndex) + { + TimeLine.enter("FilterStorage.loadFromDisk() -> readFile()"); + + let parser = new INIParser(); + IO.readFromFile(sourceFile, parser, function(e) + { + TimeLine.enter("FilterStorage.loadFromDisk() read callback"); + if (!e && parser.subscriptions.length == 0) + { + // No filter subscriptions in the file, this isn't right. + e = new Error("No data in the file"); + } + + if (e) + Cu.reportError(e); + + if (e && !explicitFile) + { + // Attempt to load a backup + sourceFile = this.sourceFile; + if (sourceFile) + { + let [, part1, part2] = /^(.*)(\.\w+)$/.exec(sourceFile.leafName) || [null, sourceFile.leafName, ""]; + + sourceFile = sourceFile.clone(); + sourceFile.leafName = part1 + "-backup" + (++backupIndex) + part2; + + IO.statFile(sourceFile, function(e, statData) + { + if (!e && statData.exists) + readFile(sourceFile, backupIndex); + else + doneReading(parser); + }); + TimeLine.leave("FilterStorage.loadFromDisk() read callback done"); + return; + } + } + doneReading(parser); + }.bind(this), "FilterStorageRead"); + + TimeLine.leave("FilterStorage.loadFromDisk() <- readFile()", "FilterStorageRead"); + }.bind(this); + + var doneReading = function(parser) + { + // Old special groups might have been converted, remove them if they are empty + let specialMap = {"~il~": true, "~wl~": true, "~fl~": true, "~eh~": true}; + let knownSubscriptions = {__proto__: null}; + for (let i = 0; i < parser.subscriptions.length; i++) + { + let subscription = parser.subscriptions[i]; + if (subscription instanceof SpecialSubscription && subscription.filters.length == 0 && subscription.url in specialMap) + parser.subscriptions.splice(i--, 1); + else + knownSubscriptions[subscription.url] = subscription; + } + + this.fileProperties = parser.fileProperties; + this.subscriptions = parser.subscriptions; + this.knownSubscriptions = knownSubscriptions; + Filter.knownFilters = parser.knownFilters; + Subscription.knownSubscriptions = parser.knownSubscriptions; + + if (parser.userFilters) + { + for (let i = 0; i < parser.userFilters.length; i++) + { + let filter = Filter.fromText(parser.userFilters[i]); + this.addFilter(filter, null, undefined, true); + } + } + TimeLine.log("Initializing data done, triggering observers") + + this._loading = false; + FilterNotifier.triggerListeners("load"); + + if (sourceFile != this.sourceFile) + this.saveToDisk(); + + TimeLine.leave("FilterStorage.loadFromDisk() read callback done"); + }.bind(this); + + let explicitFile; + if (sourceFile) + { + explicitFile = true; + readFile(sourceFile, 0); + } + else + { + explicitFile = false; + sourceFile = FilterStorage.sourceFile; + + let callback = function(e, statData) + { + if (e || !statData.exists) + { + this.firstRun = true; + this._loading = false; + FilterNotifier.triggerListeners("load"); + + TimeLine.leave("FilterStorage.loadFromDisk() read callback done"); + } + else + readFile(sourceFile, 0); + }.bind(this); + + if (sourceFile) + IO.statFile(sourceFile, callback); + else + callback(true); + } + + TimeLine.leave("FilterStorage.loadFromDisk() done"); + }, + + _generateFilterData: function(subscriptions) + { + yield "# Adblock Plus preferences"; + yield "version=" + formatVersion; + + let saved = {__proto__: null}; + let buf = []; + + // Save filter data + for (let i = 0; i < subscriptions.length; i++) + { + let subscription = subscriptions[i]; + for (let j = 0; j < subscription.filters.length; j++) + { + let filter = subscription.filters[j]; + if (!(filter.text in saved)) + { + filter.serialize(buf); + saved[filter.text] = filter; + for (let k = 0; k < buf.length; k++) + yield buf[k]; + buf.splice(0); + } + } + } + + // Save subscriptions + for (let i = 0; i < subscriptions.length; i++) + { + let subscription = subscriptions[i]; + + yield ""; + + subscription.serialize(buf); + if (subscription.filters.length) + { + buf.push("", "[Subscription filters]") + subscription.serializeFilters(buf); + } + for (let k = 0; k < buf.length; k++) + yield buf[k]; + buf.splice(0); + } + }, + + /** + * Will be set to true if saveToDisk() is running (reentrance protection). + * @type Boolean + */ + _saving: false, + + /** + * Will be set to true if a saveToDisk() call arrives while saveToDisk() is + * already running (delayed execution). + * @type Boolean + */ + _needsSave: false, + + /** + * Saves all subscriptions back to disk + * @param {nsIFile} [targetFile] File to be written + */ + saveToDisk: function(targetFile) + { + let explicitFile = true; + if (!targetFile) + { + targetFile = FilterStorage.sourceFile; + explicitFile = false; + } + if (!targetFile) + return; + + if (!explicitFile && this._saving) + { + this._needsSave = true; + return; + } + + TimeLine.enter("Entered FilterStorage.saveToDisk()"); + + // Make sure the file's parent directory exists + try { + targetFile.parent.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); + } catch (e) {} + + let writeFilters = function() + { + TimeLine.enter("FilterStorage.saveToDisk() -> writeFilters()"); + IO.writeToFile(targetFile, this._generateFilterData(subscriptions), function(e) + { + TimeLine.enter("FilterStorage.saveToDisk() write callback"); + if (!explicitFile) + this._saving = false; + + if (e) + Cu.reportError(e); + + if (!explicitFile && this._needsSave) + { + this._needsSave = false; + this.saveToDisk(); + } + else + FilterNotifier.triggerListeners("save"); + TimeLine.leave("FilterStorage.saveToDisk() write callback done"); + }.bind(this), "FilterStorageWrite"); + TimeLine.leave("FilterStorage.saveToDisk() -> writeFilters()", "FilterStorageWrite"); + }.bind(this); + + let checkBackupRequired = function(callbackNotRequired, callbackRequired) + { + if (explicitFile || Prefs.patternsbackups <= 0) + callbackNotRequired(); + else + { + IO.statFile(targetFile, function(e, statData) + { + if (e || !statData.exists) + callbackNotRequired(); + else + { + let [, part1, part2] = /^(.*)(\.\w+)$/.exec(targetFile.leafName) || [null, targetFile.leafName, ""]; + let newestBackup = targetFile.clone(); + newestBackup.leafName = part1 + "-backup1" + part2; + IO.statFile(newestBackup, function(e, statData) + { + if (!e && (!statData.exists || (Date.now() - statData.lastModified) / 3600000 >= Prefs.patternsbackupinterval)) + callbackRequired(part1, part2) + else + callbackNotRequired(); + }); + } + }); + } + }.bind(this); + + let removeLastBackup = function(part1, part2) + { + TimeLine.enter("FilterStorage.saveToDisk() -> removeLastBackup()"); + let file = targetFile.clone(); + file.leafName = part1 + "-backup" + Prefs.patternsbackups + part2; + IO.removeFile(file, (e) => renameBackup(part1, part2, Prefs.patternsbackups - 1)); + TimeLine.leave("FilterStorage.saveToDisk() <- removeLastBackup()"); + }.bind(this); + + let renameBackup = function(part1, part2, index) + { + TimeLine.enter("FilterStorage.saveToDisk() -> renameBackup()"); + if (index > 0) + { + let fromFile = targetFile.clone(); + fromFile.leafName = part1 + "-backup" + index + part2; + + let toName = part1 + "-backup" + (index + 1) + part2; + + IO.renameFile(fromFile, toName, (e) => renameBackup(part1, part2, index - 1)); + } + else + { + let toFile = targetFile.clone(); + toFile.leafName = part1 + "-backup" + (index + 1) + part2; + + IO.copyFile(targetFile, toFile, writeFilters); + } + TimeLine.leave("FilterStorage.saveToDisk() <- renameBackup()"); + }.bind(this); + + // Do not persist external subscriptions + let subscriptions = this.subscriptions.filter((s) => !(s instanceof ExternalSubscription)); + if (!explicitFile) + this._saving = true; + + checkBackupRequired(writeFilters, removeLastBackup); + + TimeLine.leave("FilterStorage.saveToDisk() done"); + }, + + /** + * Returns the list of existing backup files. + */ + getBackupFiles: function() /**nsIFile[]*/ + { + // TODO: This method should be asynchronous + let result = []; + + let [, part1, part2] = /^(.*)(\.\w+)$/.exec(FilterStorage.sourceFile.leafName) || [null, FilterStorage.sourceFile.leafName, ""]; + for (let i = 1; ; i++) + { + let file = FilterStorage.sourceFile.clone(); + file.leafName = part1 + "-backup" + i + part2; + if (file.exists()) + result.push(file); + else + break; + } + return result; + } +}; + +/** + * Joins subscription's filters to the subscription without any notifications. + * @param {Subscription} subscription filter subscription that should be connected to its filters + */ +function addSubscriptionFilters(subscription) +{ + if (!(subscription.url in FilterStorage.knownSubscriptions)) + return; + + for (let filter of subscription.filters) + filter.subscriptions.push(subscription); +} + +/** + * Removes subscription's filters from the subscription without any notifications. + * @param {Subscription} subscription filter subscription to be removed + */ +function removeSubscriptionFilters(subscription) +{ + if (!(subscription.url in FilterStorage.knownSubscriptions)) + return; + + for (let filter of subscription.filters) + { + let i = filter.subscriptions.indexOf(subscription); + if (i >= 0) + filter.subscriptions.splice(i, 1); + } +} + +/** + * Observer listening to private browsing mode changes. + * @class + */ +let PrivateBrowsing = exports.PrivateBrowsing = +{ + /** + * Will be set to true when the private browsing mode is switched on globally. + * @type Boolean + */ + enabled: false, + + /** + * Checks whether private browsing is enabled for a particular window. + */ + enabledForWindow: function(/**Window*/ wnd) /**Boolean*/ + { + try + { + return wnd.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsILoadContext) + .usePrivateBrowsing; + } + catch (e) + { + // Gecko 19 and below will throw NS_NOINTERFACE, this is expected + if (e.result != Cr.NS_NOINTERFACE) + Cu.reportError(e); + return false; + } + }, + + init: function() + { + if ("@mozilla.org/privatebrowsing;1" in Cc) + { + try + { + this.enabled = Cc["@mozilla.org/privatebrowsing;1"].getService(Ci.nsIPrivateBrowsingService).privateBrowsingEnabled; + Services.obs.addObserver(this, "private-browsing", true); + onShutdown.add(function() + { + Services.obs.removeObserver(this, "private-browsing"); + }.bind(this)); + } + catch(e) + { + Cu.reportError(e); + } + } + }, + + observe: function(subject, topic, data) + { + if (topic == "private-browsing") + { + if (data == "enter") + this.enabled = true; + else if (data == "exit") + this.enabled = false; + } + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsISupportsWeakReference, Ci.nsIObserver]) +}; +PrivateBrowsing.init(); + +/** + * IO.readFromFile() listener to parse filter data. + * @constructor + */ +function INIParser() +{ + this.fileProperties = this.curObj = {}; + this.subscriptions = []; + this.knownFilters = {__proto__: null}; + this.knownSubscriptions = {__proto__: null}; +} +INIParser.prototype = +{ + linesProcessed: 0, + subscriptions: null, + knownFilters: null, + knownSubscriptions : null, + wantObj: true, + fileProperties: null, + curObj: null, + curSection: null, + userFilters: null, + + process: function(val) + { + let origKnownFilters = Filter.knownFilters; + Filter.knownFilters = this.knownFilters; + let origKnownSubscriptions = Subscription.knownSubscriptions; + Subscription.knownSubscriptions = this.knownSubscriptions; + let match; + try + { + if (this.wantObj === true && (match = /^(\w+)=(.*)$/.exec(val))) + this.curObj[match[1]] = match[2]; + else if (val === null || (match = /^\s*\[(.+)\]\s*$/.exec(val))) + { + if (this.curObj) + { + // Process current object before going to next section + switch (this.curSection) + { + case "filter": + case "pattern": + if ("text" in this.curObj) + Filter.fromObject(this.curObj); + break; + case "subscription": + let subscription = Subscription.fromObject(this.curObj); + if (subscription) + this.subscriptions.push(subscription); + break; + case "subscription filters": + case "subscription patterns": + if (this.subscriptions.length) + { + let subscription = this.subscriptions[this.subscriptions.length - 1]; + for (let text of this.curObj) + { + let filter = Filter.fromText(text); + subscription.filters.push(filter); + filter.subscriptions.push(subscription); + } + } + break; + case "user patterns": + this.userFilters = this.curObj; + break; + } + } + + if (val === null) + return; + + this.curSection = match[1].toLowerCase(); + switch (this.curSection) + { + case "filter": + case "pattern": + case "subscription": + this.wantObj = true; + this.curObj = {}; + break; + case "subscription filters": + case "subscription patterns": + case "user patterns": + this.wantObj = false; + this.curObj = []; + break; + default: + this.wantObj = undefined; + this.curObj = null; + } + } + else if (this.wantObj === false && val) + this.curObj.push(val.replace(/\\\[/g, "[")); + } + finally + { + Filter.knownFilters = origKnownFilters; + Subscription.knownSubscriptions = origKnownSubscriptions; + } + + // Allow events to be processed every now and then. + // Note: IO.readFromFile() will deal with the potential reentrance here. + this.linesProcessed++; + if (this.linesProcessed % 1000 == 0) + Utils.yield(); + } +}; diff --git a/data/extensions/spyblock@gnu.org/lib/io.js b/data/extensions/spyblock@gnu.org/lib/io.js new file mode 100644 index 0000000..ea362b7 --- /dev/null +++ b/data/extensions/spyblock@gnu.org/lib/io.js @@ -0,0 +1,365 @@ +/* + * 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/>. + */ + +/** + * @fileOverview Module containing file I/O helpers. + */ + +let {Services} = Cu.import("resource://gre/modules/Services.jsm", null); +let {FileUtils} = Cu.import("resource://gre/modules/FileUtils.jsm", null); +let {OS} = Cu.import("resource://gre/modules/osfile.jsm", null); +let {Task} = Cu.import("resource://gre/modules/Task.jsm", null); + +let {TimeLine} = require("timeline"); +let {Prefs} = require("prefs"); +let {Utils} = require("utils"); + +let firstRead = true; +const BUFFER_SIZE = 0x8000; // 32kB + +let IO = exports.IO = +{ + /** + * Retrieves the platform-dependent line break string. + */ + get lineBreak() + { + let lineBreak = (Services.appinfo.OS == "WINNT" ? "\r\n" : "\n"); + delete IO.lineBreak; + IO.__defineGetter__("lineBreak", () => lineBreak); + return IO.lineBreak; + }, + + /** + * Tries to interpret a file path as an absolute path or a path relative to + * user's profile. Returns a file or null on failure. + */ + resolveFilePath: function(/**String*/ path) /**nsIFile*/ + { + if (!path) + return null; + + try { + // Assume an absolute path first + return new FileUtils.File(path); + } catch (e) {} + + try { + // Try relative path now + return FileUtils.getFile("ProfD", path.split("/")); + } catch (e) {} + + return null; + }, + + /** + * Reads strings from a file asynchronously, calls listener.process() with + * each line read and with a null parameter once the read operation is done. + * The callback will be called when the operation is done. + */ + readFromFile: function(/**nsIFile*/ file, /**Object*/ listener, /**Function*/ callback, /**String*/ timeLineID) + { + try + { + let processing = false; + let buffer = ""; + let loaded = false; + let error = null; + + let onProgress = function(data) + { + if (timeLineID) + { + TimeLine.asyncStart(timeLineID); + } + + let index = (processing ? -1 : Math.max(data.lastIndexOf("\n"), data.lastIndexOf("\r"))); + if (index >= 0) + { + // Protect against reentrance in case the listener processes events. + processing = true; + try + { + let oldBuffer = buffer; + buffer = data.substr(index + 1); + data = data.substr(0, index + 1); + let lines = data.split(/[\r\n]+/); + lines.pop(); + lines[0] = oldBuffer + lines[0]; + for (let i = 0; i < lines.length; i++) + listener.process(lines[i]); + } + finally + { + processing = false; + data = buffer; + buffer = ""; + onProgress(data); + + if (loaded) + { + loaded = false; + onSuccess(); + } + + if (error) + { + let param = error; + error = null; + onError(param); + } + } + } + else + buffer += data; + + if (timeLineID) + { + TimeLine.asyncEnd(timeLineID); + } + }; + + let onSuccess = function() + { + if (processing) + { + // Still processing data, delay processing this event. + loaded = true; + return; + } + + if (timeLineID) + { + TimeLine.asyncStart(timeLineID); + } + + if (buffer !== "") + listener.process(buffer); + listener.process(null); + + if (timeLineID) + { + TimeLine.asyncEnd(timeLineID); + TimeLine.asyncDone(timeLineID); + } + + callback(null); + }; + + let onError = function(e) + { + if (processing) + { + // Still processing data, delay processing this event. + error = e; + return; + } + + callback(e); + + if (timeLineID) + { + TimeLine.asyncDone(timeLineID); + } + }; + + let decoder = new TextDecoder(); + let array = new Uint8Array(BUFFER_SIZE); + Task.spawn(function() + { + if (firstRead && Services.vc.compare(Utils.platformVersion, "23.0a1") <= 0) + { + // See https://issues.adblockplus.org/ticket/530 - the first file + // opened cannot be closed due to Gecko bug 858723. Make sure that + // our patterns.ini file doesn't stay locked by opening a dummy file + // first. + try + { + let dummyPath = IO.resolveFilePath(Prefs.data_directory + "/dummy").path; + let dummy = yield OS.File.open(dummyPath, {write: true, truncate: true}); + yield dummy.close(); + } + catch (e) + { + // Dummy might be locked already, we don't care + } + } + firstRead = false; + + let f = yield OS.File.open(file.path, {read: true}); + let numBytes; + do + { + numBytes = yield f.readTo(array); + if (numBytes) + { + let data = decoder.decode(numBytes == BUFFER_SIZE ? + array : + array.subarray(0, numBytes), {stream: true}); + onProgress(data); + } + } while (numBytes); + + yield f.close(); + }.bind(this)).then(onSuccess, onError); + } + catch (e) + { + callback(e); + } + }, + + /** + * Writes string data to a file in UTF-8 format asynchronously. The callback + * will be called when the write operation is done. + */ + writeToFile: function(/**nsIFile*/ file, /**Iterator*/ data, /**Function*/ callback, /**String*/ timeLineID) + { + try + { + let encoder = new TextEncoder(); + + Task.spawn(function() + { + // This mimics OS.File.writeAtomic() but writes in chunks. + let tmpPath = file.path + ".tmp"; + let f = yield OS.File.open(tmpPath, {write: true, truncate: true}); + + let buf = []; + let bufLen = 0; + let lineBreak = this.lineBreak; + + function writeChunk() + { + let array = encoder.encode(buf.join(lineBreak) + lineBreak); + buf = []; + bufLen = 0; + return f.write(array); + } + + for (let line in data) + { + buf.push(line); + bufLen += line.length; + if (bufLen >= BUFFER_SIZE) + yield writeChunk(); + } + + if (bufLen) + yield writeChunk(); + + // OS.File.flush() isn't exposed prior to Gecko 27, see bug 912457. + if (typeof f.flush == "function") + yield f.flush(); + yield f.close(); + yield OS.File.move(tmpPath, file.path, {noCopy: true}); + }.bind(this)).then(callback.bind(null, null), callback); + } + catch (e) + { + callback(e); + } + }, + + /** + * Copies a file asynchronously. The callback will be called when the copy + * operation is done. + */ + copyFile: function(/**nsIFile*/ fromFile, /**nsIFile*/ toFile, /**Function*/ callback) + { + try + { + let promise = OS.File.copy(fromFile.path, toFile.path); + promise.then(callback.bind(null, null), callback); + } + catch (e) + { + callback(e); + } + }, + + /** + * Renames a file within the same directory, will call callback when done. + */ + renameFile: function(/**nsIFile*/ fromFile, /**String*/ newName, /**Function*/ callback) + { + try + { + toFile = fromFile.clone(); + toFile.leafName = newName; + let promise = OS.File.move(fromFile.path, toFile.path); + promise.then(callback.bind(null, null), callback); + } + catch(e) + { + callback(e); + } + }, + + /** + * Removes a file, will call callback when done. + */ + removeFile: function(/**nsIFile*/ file, /**Function*/ callback) + { + try + { + let promise = OS.File.remove(file.path); + promise.then(callback.bind(null, null), callback); + } + catch(e) + { + callback(e); + } + }, + + /** + * Gets file information such as whether the file exists. + */ + statFile: function(/**nsIFile*/ file, /**Function*/ callback) + { + try + { + let promise = OS.File.stat(file.path); + promise.then(function onSuccess(info) + { + callback(null, { + exists: true, + isDirectory: info.isDir, + isFile: !info.isDir, + lastModified: info.lastModificationDate.getTime() + }); + }, function onError(e) + { + if (e.becauseNoSuchFile) + { + callback(null, { + exists: false, + isDirectory: false, + isFile: false, + lastModified: 0 + }); + } + else + callback(e); + }); + } + catch(e) + { + callback(e); + } + } +} diff --git a/data/extensions/spyblock@gnu.org/lib/keySelector.js b/data/extensions/spyblock@gnu.org/lib/keySelector.js new file mode 100644 index 0000000..20847d9 --- /dev/null +++ b/data/extensions/spyblock@gnu.org/lib/keySelector.js @@ -0,0 +1,228 @@ +/* + * This file is part of the Adblock Plus build tools, + * 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/Services.jsm"); + +let validModifiers = +{ + ACCEL: null, + CTRL: "control", + CONTROL: "control", + SHIFT: "shift", + ALT: "alt", + META: "meta", + __proto__: null +}; + +/** + * Sets the correct value of validModifiers.ACCEL. + */ +function initAccelKey() +{ + validModifiers.ACCEL = "control"; + try + { + let accelKey = Services.prefs.getIntPref("ui.key.accelKey"); + if (accelKey == Ci.nsIDOMKeyEvent.DOM_VK_CONTROL) + validModifiers.ACCEL = "control"; + else if (accelKey == Ci.nsIDOMKeyEvent.DOM_VK_ALT) + validModifiers.ACCEL = "alt"; + else if (accelKey == Ci.nsIDOMKeyEvent.DOM_VK_META) + validModifiers.ACCEL = "meta"; + } + catch(e) + { + Cu.reportError(e); + } +} + +exports.KeySelector = KeySelector; + +/** + * This class provides capabilities to find and use available keyboard shortcut + * keys. + * @param {ChromeWindow} window the window where to look up existing shortcut + * keys + * @constructor + */ +function KeySelector(window) +{ + this._initExistingShortcuts(window); +} +KeySelector.prototype = +{ + /** + * Map listing existing shortcut keys as its keys. + * @type Object + */ + _existingShortcuts: null, + + /** + * Sets up _existingShortcuts property for a window. + */ + _initExistingShortcuts: function(/**ChromeWindow*/ window) + { + if (!validModifiers.ACCEL) + initAccelKey(); + + this._existingShortcuts = {__proto__: null}; + + let keys = window.document.getElementsByTagName("key"); + for (let i = 0; i < keys.length; i++) + { + let key = keys[i]; + let keyData = + { + shift: false, + meta: false, + alt: false, + control: false, + char: null, + code: null + }; + + let keyChar = key.getAttribute("key"); + if (keyChar && keyChar.length == 1) + keyData.char = keyChar.toUpperCase(); + + let keyCode = key.getAttribute("keycode"); + if (keyCode && "DOM_" + keyCode.toUpperCase() in Ci.nsIDOMKeyEvent) + keyData.code = Ci.nsIDOMKeyEvent["DOM_" + keyCode.toUpperCase()]; + + if (!keyData.char && !keyData.code) + continue; + + let keyModifiers = key.getAttribute("modifiers"); + if (keyModifiers) + for each (let modifier in keyModifiers.toUpperCase().match(/\w+/g)) + if (modifier in validModifiers) + keyData[validModifiers[modifier]] = true; + + let canonical = [keyData.shift, keyData.meta, keyData.alt, keyData.control, keyData.char || keyData.code].join(" "); + this._existingShortcuts[canonical] = true; + } + }, + + /** + * Selects a keyboard shortcut variant that isn't already taken, + * parses it into an object. + */ + selectKey: function(/**String*/ variants) /**Object*/ + { + for each (let variant in variants.split(/\s*,\s*/)) + { + if (!variant) + continue; + + let keyData = + { + shift: false, + meta: false, + alt: false, + control: false, + char: null, + code: null, + codeName: null + }; + for each (let part in variant.toUpperCase().split(/\s+/)) + { + if (part in validModifiers) + keyData[validModifiers[part]] = true; + else if (part.length == 1) + keyData.char = part; + else if ("DOM_VK_" + part in Ci.nsIDOMKeyEvent) + { + keyData.code = Ci.nsIDOMKeyEvent["DOM_VK_" + part]; + keyData.codeName = "VK_" + part; + } + } + + if (!keyData.char && !keyData.code) + continue; + + let canonical = [keyData.shift, keyData.meta, keyData.alt, keyData.control, keyData.char || keyData.code].join(" "); + if (canonical in this._existingShortcuts) + continue; + + return keyData; + } + + return null; + } +}; + +/** + * Creates the text representation for a key. + * @static + */ +KeySelector.getTextForKey = function (/**Object*/ key) /**String*/ +{ + if (!key) + return null; + + if (!("text" in key)) + { + key.text = null; + try + { + let stringBundle = Services.strings.createBundle("chrome://global-platform/locale/platformKeys.properties"); + let parts = []; + if (key.control) + parts.push(stringBundle.GetStringFromName("VK_CONTROL")); + if (key.alt) + parts.push(stringBundle.GetStringFromName("VK_ALT")); + if (key.meta) + parts.push(stringBundle.GetStringFromName("VK_META")); + if (key.shift) + parts.push(stringBundle.GetStringFromName("VK_SHIFT")); + if (key.char) + parts.push(key.char.toUpperCase()); + else + { + let stringBundle2 = Services.strings.createBundle("chrome://global/locale/keys.properties"); + parts.push(stringBundle2.GetStringFromName(key.codeName)); + } + key.text = parts.join(stringBundle.GetStringFromName("MODIFIER_SEPARATOR")); + } + catch (e) + { + Cu.reportError(e); + return null; + } + } + return key.text; +}; + +/** + * Tests whether a keypress event matches the given key. + * @static + */ +KeySelector.matchesKey = function(/**Event*/ event, /**Object*/ key) /**Boolean*/ +{ + if (event.defaultPrevented || !key) + return false; + if (key.shift != event.shiftKey || key.alt != event.altKey) + return false; + if (key.meta != event.metaKey || key.control != event.ctrlKey) + return false; + + if (key.char && event.charCode && String.fromCharCode(event.charCode).toUpperCase() == key.char) + return true; + if (key.code && event.keyCode && event.keyCode == key.code) + return true; + return false; +}; diff --git a/data/extensions/spyblock@gnu.org/lib/main.js b/data/extensions/spyblock@gnu.org/lib/main.js new file mode 100644 index 0000000..f18cc05 --- /dev/null +++ b/data/extensions/spyblock@gnu.org/lib/main.js @@ -0,0 +1,73 @@ +/* + * 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/>. + */ + +/** + * @fileOverview Starts up Adblock Plus + */ + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +let {TimeLine} = require("timeline"); + +TimeLine.enter("Adblock Plus startup"); +registerPublicAPI(); +TimeLine.log("Done registering public API"); +require("filterListener"); +TimeLine.log("Done loading filter listener"); +require("contentPolicy"); +TimeLine.log("Done loading content policy"); +require("synchronizer"); +TimeLine.log("Done loading subscription synchronizer"); +require("notification"); +TimeLine.log("Done loading notification downloader"); +require("sync"); +TimeLine.log("Done loading sync support"); +require("ui"); +TimeLine.log("Done loading UI integration code"); +TimeLine.leave("Started up"); + +function registerPublicAPI() +{ + let {addonRoot} = require("info"); + + let uri = Services.io.newURI(addonRoot + "lib/Public.jsm", null, null); + if (uri instanceof Ci.nsIMutable) + uri.mutable = false; + + let classID = Components.ID("5e447bce-1dd2-11b2-b151-ec21c2b6a135"); + let contractID = "@adblockplus.org/abp/public;1"; + let factory = + { + createInstance: function(outer, iid) + { + if (outer) + throw Cr.NS_ERROR_NO_AGGREGATION; + return uri.QueryInterface(iid); + }, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIFactory]) + }; + + let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar); + registrar.registerFactory(classID, "Adblock Plus public API URL", contractID, factory); + + onShutdown.add(function() + { + registrar.unregisterFactory(classID, factory); + Cu.unload(uri.spec); + }); +} diff --git a/data/extensions/spyblock@gnu.org/lib/matcher.js b/data/extensions/spyblock@gnu.org/lib/matcher.js new file mode 100644 index 0000000..908b0b8 --- /dev/null +++ b/data/extensions/spyblock@gnu.org/lib/matcher.js @@ -0,0 +1,446 @@ +/* + * 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/>. + */ + +/** + * @fileOverview Matcher class implementing matching addresses against a list of filters. + */ + +let {Filter, RegExpFilter, WhitelistFilter} = require("filterClasses"); + +/** + * Blacklist/whitelist filter matching + * @constructor + */ +function Matcher() +{ + this.clear(); +} +exports.Matcher = Matcher; + +Matcher.prototype = { + /** + * Lookup table for filters by their associated keyword + * @type Object + */ + filterByKeyword: null, + + /** + * Lookup table for keywords by the filter text + * @type Object + */ + keywordByFilter: null, + + /** + * Removes all known filters + */ + clear: function() + { + this.filterByKeyword = {__proto__: null}; + this.keywordByFilter = {__proto__: null}; + }, + + /** + * Adds a filter to the matcher + * @param {RegExpFilter} filter + */ + add: function(filter) + { + if (filter.text in this.keywordByFilter) + return; + + // Look for a suitable keyword + let keyword = this.findKeyword(filter); + let oldEntry = this.filterByKeyword[keyword]; + if (typeof oldEntry == "undefined") + this.filterByKeyword[keyword] = filter; + else if (oldEntry.length == 1) + this.filterByKeyword[keyword] = [oldEntry, filter]; + else + oldEntry.push(filter); + this.keywordByFilter[filter.text] = keyword; + }, + + /** + * Removes a filter from the matcher + * @param {RegExpFilter} filter + */ + remove: function(filter) + { + if (!(filter.text in this.keywordByFilter)) + return; + + let keyword = this.keywordByFilter[filter.text]; + let list = this.filterByKeyword[keyword]; + if (list.length <= 1) + delete this.filterByKeyword[keyword]; + else + { + let index = list.indexOf(filter); + if (index >= 0) + { + list.splice(index, 1); + if (list.length == 1) + this.filterByKeyword[keyword] = list[0]; + } + } + + delete this.keywordByFilter[filter.text]; + }, + + /** + * Chooses a keyword to be associated with the filter + * @param {String} text text representation of the filter + * @return {String} keyword (might be empty string) + */ + findKeyword: function(filter) + { + let result = ""; + let text = filter.text; + if (Filter.regexpRegExp.test(text)) + return result; + + // Remove options + let match = Filter.optionsRegExp.exec(text); + if (match) + text = match.input.substr(0, match.index); + + // Remove whitelist marker + if (text.substr(0, 2) == "@@") + text = text.substr(2); + + let candidates = text.toLowerCase().match(/[^a-z0-9%*][a-z0-9%]{3,}(?=[^a-z0-9%*])/g); + if (!candidates) + return result; + + let hash = this.filterByKeyword; + let resultCount = 0xFFFFFF; + let resultLength = 0; + for (let i = 0, l = candidates.length; i < l; i++) + { + let candidate = candidates[i].substr(1); + let count = (candidate in hash ? hash[candidate].length : 0); + if (count < resultCount || (count == resultCount && candidate.length > resultLength)) + { + result = candidate; + resultCount = count; + resultLength = candidate.length; + } + } + return result; + }, + + /** + * Checks whether a particular filter is being matched against. + */ + hasFilter: function(/**RegExpFilter*/ filter) /**Boolean*/ + { + return (filter.text in this.keywordByFilter); + }, + + /** + * Returns the keyword used for a filter, null for unknown filters. + */ + getKeywordForFilter: function(/**RegExpFilter*/ filter) /**String*/ + { + if (filter.text in this.keywordByFilter) + return this.keywordByFilter[filter.text]; + else + return null; + }, + + /** + * Checks whether the entries for a particular keyword match a URL + */ + _checkEntryMatch: function(keyword, location, contentType, docDomain, thirdParty, privatenode) + { + let list = this.filterByKeyword[keyword]; + for (let i = 0; i < list.length; i++) + { + let filter = list[i]; + if (filter.matches(location, contentType, docDomain, thirdParty,privatenode)) + return filter; + } + return null; + }, + + /** + * Tests whether the URL matches any of the known filters + * @param {String} location URL to be tested + * @param {String} contentType content type identifier of the URL + * @param {String} docDomain domain name of the document that loads the URL + * @param {Boolean} thirdParty should be true if the URL is a third-party request + * @return {RegExpFilter} matching filter or null + */ + matchesAny: function(location, contentType, docDomain, thirdParty) + { + let candidates = location.toLowerCase().match(/[a-z0-9%]{3,}/g); + if (candidates === null) + candidates = []; + candidates.push(""); + for (let i = 0, l = candidates.length; i < l; i++) + { + let substr = candidates[i]; + if (substr in this.filterByKeyword) + { + let result = this._checkEntryMatch(substr, location, contentType, docDomain, thirdParty); + if (result) + return result; + } + } + + return null; + } +}; + +/** + * Combines a matcher for blocking and exception rules, automatically sorts + * rules into two Matcher instances. + * @constructor + */ +function CombinedMatcher() +{ + this.blacklist = new Matcher(); + this.whitelist = new Matcher(); + this.keys = {__proto__: null}; + this.resultCache = {__proto__: null}; +} +exports.CombinedMatcher = CombinedMatcher; + +/** + * Maximal number of matching cache entries to be kept + * @type Number + */ +CombinedMatcher.maxCacheEntries = 1000; + +CombinedMatcher.prototype = +{ + /** + * Matcher for blocking rules. + * @type Matcher + */ + blacklist: null, + + /** + * Matcher for exception rules. + * @type Matcher + */ + whitelist: null, + + /** + * Exception rules that are limited by public keys, mapped by the corresponding keys. + * @type Object + */ + keys: null, + + /** + * Lookup table of previous matchesAny results + * @type Object + */ + resultCache: null, + + /** + * Number of entries in resultCache + * @type Number + */ + cacheEntries: 0, + + /** + * @see Matcher#clear + */ + clear: function() + { + this.blacklist.clear(); + this.whitelist.clear(); + this.keys = {__proto__: null}; + this.resultCache = {__proto__: null}; + this.cacheEntries = 0; + }, + + /** + * @see Matcher#add + */ + add: function(filter) + { + if (filter instanceof WhitelistFilter) + { + if (filter.siteKeys) + { + for (let i = 0; i < filter.siteKeys.length; i++) + this.keys[filter.siteKeys[i]] = filter.text; + } + else + this.whitelist.add(filter); + } + else + this.blacklist.add(filter); + + if (this.cacheEntries > 0) + { + this.resultCache = {__proto__: null}; + this.cacheEntries = 0; + } + }, + + /** + * @see Matcher#remove + */ + remove: function(filter) + { + if (filter instanceof WhitelistFilter) + { + if (filter.siteKeys) + { + for (let i = 0; i < filter.siteKeys.length; i++) + delete this.keys[filter.siteKeys[i]]; + } + else + this.whitelist.remove(filter); + } + else + this.blacklist.remove(filter); + + if (this.cacheEntries > 0) + { + this.resultCache = {__proto__: null}; + this.cacheEntries = 0; + } + }, + + /** + * @see Matcher#findKeyword + */ + findKeyword: function(filter) + { + if (filter instanceof WhitelistFilter) + return this.whitelist.findKeyword(filter); + else + return this.blacklist.findKeyword(filter); + }, + + /** + * @see Matcher#hasFilter + */ + hasFilter: function(filter) + { + if (filter instanceof WhitelistFilter) + return this.whitelist.hasFilter(filter); + else + return this.blacklist.hasFilter(filter); + }, + + /** + * @see Matcher#getKeywordForFilter + */ + getKeywordForFilter: function(filter) + { + if (filter instanceof WhitelistFilter) + return this.whitelist.getKeywordForFilter(filter); + else + return this.blacklist.getKeywordForFilter(filter); + }, + + /** + * Checks whether a particular filter is slow + */ + isSlowFilter: function(/**RegExpFilter*/ filter) /**Boolean*/ + { + let matcher = (filter instanceof WhitelistFilter ? this.whitelist : this.blacklist); + if (matcher.hasFilter(filter)) + return !matcher.getKeywordForFilter(filter); + else + return !matcher.findKeyword(filter); + }, + + /** + * Optimized filter matching testing both whitelist and blacklist matchers + * simultaneously. For parameters see Matcher.matchesAny(). + * @see Matcher#matchesAny + */ + matchesAnyInternal: function(location, contentType, docDomain, thirdParty, privatenode) + { + let candidates = location.toLowerCase().match(/[a-z0-9%]{3,}/g); + if (candidates === null) + candidates = []; + candidates.push(""); + + let blacklistHit = null; + for (let i = 0, l = candidates.length; i < l; i++) + { + let substr = candidates[i]; + if (substr in this.whitelist.filterByKeyword) + { + let result = this.whitelist._checkEntryMatch(substr, location, contentType, docDomain, thirdParty, privatenode); + if (result) + return result; + } + if (substr in this.blacklist.filterByKeyword && blacklistHit === null) + blacklistHit = this.blacklist._checkEntryMatch(substr, location, contentType, docDomain, thirdParty, privatenode); + } + return blacklistHit; + }, + + /** + * @see Matcher#matchesAny + */ + matchesAny: function(location, contentType, docDomain, thirdParty, privatenode) + { + let key = location + " " + contentType + " " + docDomain + " " + thirdParty; + if (!privatenode){ + if (key in this.resultCache) + return this.resultCache[key]; + } + + let result = this.matchesAnyInternal(location, contentType, docDomain, thirdParty, privatenode); + + if (this.cacheEntries >= CombinedMatcher.maxCacheEntries) + { + this.resultCache = {__proto__: null}; + this.cacheEntries = 0; + } + + if (!privatenode){ + this.resultCache[key] = result; + this.cacheEntries++; + } + + return result; + }, + + /** + * Looks up whether any filters match the given website key. + */ + matchesByKey: function(/**String*/ location, /**String*/ key, /**String*/ docDomain) + { + key = key.toUpperCase(); + if (key in this.keys) + { + let filter = Filter.knownFilters[this.keys[key]]; + if (filter && filter.matches(location, "DOCUMENT", docDomain, false)) + return filter; + else + return null; + } + else + return null; + } +} + +/** + * Shared CombinedMatcher instance that should usually be used. + * @type CombinedMatcher + */ +let defaultMatcher = exports.defaultMatcher = new CombinedMatcher(); diff --git a/data/extensions/spyblock@gnu.org/lib/notification.js b/data/extensions/spyblock@gnu.org/lib/notification.js new file mode 100644 index 0000000..06e949e --- /dev/null +++ b/data/extensions/spyblock@gnu.org/lib/notification.js @@ -0,0 +1,339 @@ +/* + * 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/>. + */ + +/** + * @fileOverview Handles notifications. + */ + +Cu.import("resource://gre/modules/Services.jsm"); + +let {TimeLine} = require("timeline"); +let {Prefs} = require("prefs"); +let {Downloader, Downloadable, MILLIS_IN_MINUTE, MILLIS_IN_HOUR, MILLIS_IN_DAY} = require("downloader"); +let {Utils} = require("utils"); +let {Matcher} = require("matcher"); +let {Filter} = require("filterClasses"); + +let INITIAL_DELAY = 12 * MILLIS_IN_MINUTE; +let CHECK_INTERVAL = 1 * MILLIS_IN_HOUR; +let EXPIRATION_INTERVAL = 1 * MILLIS_IN_DAY; +let TYPE = { + information: 0, + question: 1, + critical: 2 +}; + +let listeners = {}; + +function getNumericalSeverity(notification) +{ + return (notification.type in TYPE ? TYPE[notification.type] : TYPE.information); +} + +function saveNotificationData() +{ + // HACK: JSON values aren't saved unless they are assigned a different object. + Prefs.notificationdata = JSON.parse(JSON.stringify(Prefs.notificationdata)); +} + +function localize(translations, locale) +{ + if (locale in translations) + return translations[locale]; + + let languagePart = locale.substring(0, locale.indexOf("-")); + if (languagePart && languagePart in translations) + return translations[languagePart]; + + let defaultLocale = "en-US"; + return translations[defaultLocale]; +} + +/** + * The object providing actual downloading functionality. + * @type Downloader + */ +let downloader = null; +let localData = []; + +/** + * Regularly fetches notifications and decides which to show. + * @class + */ +let Notification = exports.Notification = +{ + /** + * Called on module startup. + */ + init: function() + { + TimeLine.enter("Entered Notification.init()"); + + downloader = new Downloader(this._getDownloadables.bind(this), INITIAL_DELAY, CHECK_INTERVAL); + onShutdown.add(function() + { + downloader.cancel(); + }); + + downloader.onExpirationChange = this._onExpirationChange.bind(this); + downloader.onDownloadSuccess = this._onDownloadSuccess.bind(this); + downloader.onDownloadError = this._onDownloadError.bind(this); + + TimeLine.leave("Notification.init() done"); + }, + + /** + * Yields a Downloadable instances for the notifications download. + */ + _getDownloadables: function() + { + let downloadable = new Downloadable(Prefs.notificationurl); + if (typeof Prefs.notificationdata.lastError === "number") + downloadable.lastError = Prefs.notificationdata.lastError; + if (typeof Prefs.notificationdata.lastCheck === "number") + downloadable.lastCheck = Prefs.notificationdata.lastCheck; + if (typeof Prefs.notificationdata.data === "object" && "version" in Prefs.notificationdata.data) + downloadable.lastVersion = Prefs.notificationdata.data.version; + if (typeof Prefs.notificationdata.softExpiration === "number") + downloadable.softExpiration = Prefs.notificationdata.softExpiration; + if (typeof Prefs.notificationdata.hardExpiration === "number") + downloadable.hardExpiration = Prefs.notificationdata.hardExpiration; + yield downloadable; + }, + + _onExpirationChange: function(downloadable) + { + Prefs.notificationdata.lastCheck = downloadable.lastCheck; + Prefs.notificationdata.softExpiration = downloadable.softExpiration; + Prefs.notificationdata.hardExpiration = downloadable.hardExpiration; + saveNotificationData(); + }, + + _onDownloadSuccess: function(downloadable, responseText, errorCallback, redirectCallback) + { + try + { + let data = JSON.parse(responseText); + for (let notification of data.notifications) + { + if ("severity" in notification) + { + if (!("type" in notification)) + notification.type = notification.severity; + delete notification.severity; + } + } + Prefs.notificationdata.data = data; + } + catch (e) + { + Cu.reportError(e); + errorCallback("synchronize_invalid_data"); + return; + } + + Prefs.notificationdata.lastError = 0; + Prefs.notificationdata.downloadStatus = "synchronize_ok"; + [Prefs.notificationdata.softExpiration, Prefs.notificationdata.hardExpiration] = downloader.processExpirationInterval(EXPIRATION_INTERVAL); + saveNotificationData(); + }, + + _onDownloadError: function(downloadable, downloadURL, error, channelStatus, responseStatus, redirectCallback) + { + Prefs.notificationdata.lastError = Date.now(); + Prefs.notificationdata.downloadStatus = error; + saveNotificationData(); + }, + + /** + * Determines which notification is to be shown next. + * @param {String} url URL to match notifications to (optional) + * @return {Object} notification to be shown, or null if there is none + */ + getNextToShow: function(url) + { + function checkTarget(target, parameter, name, version) + { + let minVersionKey = parameter + "MinVersion"; + let maxVersionKey = parameter + "MaxVersion"; + return !((parameter in target && target[parameter] != name) || + (minVersionKey in target && Services.vc.compare(version, target[minVersionKey]) < 0) || + (maxVersionKey in target && Services.vc.compare(version, target[maxVersionKey]) > 0)); + } + + let remoteData = []; + if (typeof Prefs.notificationdata.data == "object" && Prefs.notificationdata.data.notifications instanceof Array) + remoteData = Prefs.notificationdata.data.notifications; + + if (!(Prefs.notificationdata.shown instanceof Array)) + { + Prefs.notificationdata.shown = []; + saveNotificationData(); + } + + let notifications = localData.concat(remoteData); + if (notifications.length === 0) + return null; + + let {addonName, addonVersion, application, applicationVersion, platform, platformVersion} = require("info"); + let notificationToShow = null; + for (let notification of notifications) + { + if ((typeof notification.type === "undefined" || notification.type !== "critical") + && Prefs.notificationdata.shown.indexOf(notification.id) !== -1) + continue; + + if (typeof url === "string" || notification.urlFilters instanceof Array) + { + if (typeof url === "string" && notification.urlFilters instanceof Array) + { + let matcher = new Matcher(); + for (let urlFilter of notification.urlFilters) + matcher.add(Filter.fromText(urlFilter)); + if (!matcher.matchesAny(url, "DOCUMENT", url)) + continue; + } + else + continue; + } + + if (notification.targets instanceof Array) + { + let match = false; + for (let target of notification.targets) + { + if (checkTarget(target, "extension", addonName, addonVersion) && + checkTarget(target, "application", application, applicationVersion) && + checkTarget(target, "platform", platform, platformVersion)) + { + match = true; + break; + } + } + if (!match) + continue; + } + + if (!notificationToShow + || getNumericalSeverity(notification) > getNumericalSeverity(notificationToShow)) + notificationToShow = notification; + } + + if (notificationToShow && "id" in notificationToShow) + { + if (notificationToShow.type !== "question") + this.markAsShown(notificationToShow.id); + } + + return notificationToShow; + }, + + markAsShown: function(id) + { + if (Prefs.notificationdata.shown.indexOf(id) > -1) + return; + + Prefs.notificationdata.shown.push(id); + saveNotificationData(); + }, + + /** + * Localizes the texts of the supplied notification. + * @param {Object} notification notification to translate + * @param {String} locale the target locale (optional, defaults to the + * application locale) + * @return {Object} the translated texts + */ + getLocalizedTexts: function(notification, locale) + { + locale = locale || Utils.appLocale; + let textKeys = ["title", "message"]; + let localizedTexts = []; + for (let key of textKeys) + { + if (key in notification) + { + if (typeof notification[key] == "string") + localizedTexts[key] = notification[key]; + else + localizedTexts[key] = localize(notification[key], locale); + } + } + return localizedTexts; + }, + + /** + * Adds a local notification. + * @param {Object} notification notification to add + */ + addNotification: function(notification) + { + if (localData.indexOf(notification) == -1) + localData.push(notification); + }, + + /** + * Removes an existing local notification. + * @param {Object} notification notification to remove + */ + removeNotification: function(notification) + { + let index = localData.indexOf(notification); + if (index > -1) + localData.splice(index, 1); + }, + + /** + * Adds a listener for question-type notifications + */ + addQuestionListener: function(/**string*/ id, /**function(approved)*/ listener) + { + if (!(id in listeners)) + listeners[id] = []; + if (listeners[id].indexOf(listener) === -1) + listeners[id].push(listener); + }, + + /** + * Removes a listener that was previously added via addQuestionListener + */ + removeQuestionListener: function(/**string*/ id, /**function(approved)*/ listener) + { + if (!(id in listeners)) + return; + let index = listeners[id].indexOf(listener); + if (index > -1) + listeners[id].splice(index, 1); + if (listeners[id].length === 0) + delete listeners[id]; + }, + + /** + * Notifies listeners about interactions with a notification + * @param {String} id notification ID + * @param {Boolean} approved indicator whether notification has been approved or not + */ + triggerQuestionListeners: function(id, approved) + { + if (!(id in listeners)) + return; + let questionListeners = listeners[id]; + for (let listener of questionListeners) + listener(approved); + } +}; +Notification.init(); diff --git a/data/extensions/spyblock@gnu.org/lib/objectTabs.js b/data/extensions/spyblock@gnu.org/lib/objectTabs.js new file mode 100644 index 0000000..1227490 --- /dev/null +++ b/data/extensions/spyblock@gnu.org/lib/objectTabs.js @@ -0,0 +1,492 @@ +/* + * 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/>. + */ + +/** + * @fileOverview Code responsible for showing and hiding object tabs. + */ + +/** + * Class responsible for showing and hiding object tabs. + * @class + */ +var objTabs = +{ + /** + * Number of milliseconds to wait until hiding tab after the mouse moves away. + * @type Integer + */ + HIDE_DELAY: 1000, + + /** + * Flag used to trigger object tabs initialization first time object tabs are + * used. + * @type Boolean + */ + initialized: false, + + /** + * Will be set to true while initialization is in progress. + * @type Boolean + */ + initializing: false, + + /** + * Parameters for _showTab, to be called once initialization is complete. + */ + delayedShowParams: null, + + /** + * Randomly generated class to be used for visible object tabs on top of object. + * @type String + */ + objTabClassVisibleTop: null, + + /** + * Randomly generated class to be used for visible object tabs at the bottom of the object. + * @type String + */ + objTabClassVisibleBottom: null, + + /** + * Randomly generated class to be used for invisible object tabs. + * @type String + */ + objTabClassHidden: null, + + /** + * Document element the object tab is currently being displayed for. + * @type Element + */ + currentElement: null, + + /** + * Windows that the window event handler is currently registered for. + * @type Array of Window + */ + windowListeners: null, + + /** + * Panel element currently used as object tab. + * @type Element + */ + objtabElement: null, + + /** + * Time of previous position update. + * @type Integer + */ + prevPositionUpdate: 0, + + /** + * Timer used to update position of the object tab. + * @type nsITimer + */ + positionTimer: null, + + /** + * Timer used to delay hiding of the object tab. + * @type nsITimer + */ + hideTimer: null, + + /** + * Used when hideTimer is running, time when the tab should be hidden. + * @type Integer + */ + hideTargetTime: 0, + + /** + * Initializes object tabs (generates random classes and registers stylesheet). + */ + _initCSS: function() + { + function processCSSData(request) + { + if (onShutdown.done) + return; + + let data = request.responseText; + + let rnd = []; + let offset = "a".charCodeAt(0); + for (let i = 0; i < 60; i++) + rnd.push(offset + Math.random() * 26); + + this.objTabClassVisibleTop = String.fromCharCode.apply(String, rnd.slice(0, 20)); + this.objTabClassVisibleBottom = String.fromCharCode.apply(String, rnd.slice(20, 40)); + this.objTabClassHidden = String.fromCharCode.apply(String, rnd.slice(40, 60)); + + let {Utils} = require("utils"); + let url = Utils.makeURI("data:text/css," + encodeURIComponent(data.replace(/%%CLASSVISIBLETOP%%/g, this.objTabClassVisibleTop) + .replace(/%%CLASSVISIBLEBOTTOM%%/g, this.objTabClassVisibleBottom) + .replace(/%%CLASSHIDDEN%%/g, this.objTabClassHidden))); + Utils.styleService.loadAndRegisterSheet(url, Ci.nsIStyleSheetService.USER_SHEET); + onShutdown.add(function() + { + Utils.styleService.unregisterSheet(url, Ci.nsIStyleSheetService.USER_SHEET); + }); + + this.initializing = false; + this.initialized = true; + + if (this.delayedShowParams) + this._showTab.apply(this, this.delayedShowParams); + } + + this.delayedShowParams = arguments; + + if (!this.initializing) + { + this.initializing = true; + + // Load CSS asynchronously + try { + let request = new XMLHttpRequest(); + request.mozBackgroundRequest = true; + request.open("GET", "chrome://adblockplus/content/objtabs.css"); + request.overrideMimeType("text/plain"); + + request.addEventListener("load", processCSSData.bind(this, request), false); + request.send(null); + } + catch (e) + { + Cu.reportError(e); + this.initializing = false; + } + } + }, + + /** + * Called to show object tab for an element. + */ + showTabFor: function(/**Element*/ element) + { + // Object tabs aren't usable in Fennec + let {application} = require("info"); + if (application == "fennec" || application == "fennec2") + return; + + let {Prefs} = require("prefs"); + if (!Prefs.frameobjects) + return; + + if (this.hideTimer) + { + this.hideTimer.cancel(); + this.hideTimer = null; + } + + if (this.objtabElement) + this.objtabElement.style.setProperty("opacity", "1", "important"); + + if (this.currentElement != element) + { + this._hideTab(); + + let {Policy} = require("contentPolicy"); + let {RequestNotifier} = require("requestNotifier"); + let data = RequestNotifier.getDataForNode(element, true, Policy.type.OBJECT); + if (data) + { + if (this.initialized) + this._showTab(element, data[1]); + else + this._initCSS(element, data[1]); + } + } + }, + + /** + * Called to hide object tab for an element (actual hiding happens delayed). + */ + hideTabFor: function(/**Element*/ element) + { + if (element != this.currentElement || this.hideTimer) + return; + + this.hideTargetTime = Date.now() + this.HIDE_DELAY; + this.hideTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + this.hideTimer.init(this, 40, Ci.nsITimer.TYPE_REPEATING_SLACK); + }, + + /** + * Makes the tab element visible. + */ + _showTab: function(/**Element*/ element, /**RequestEntry*/ data) + { + let {UI} = require("ui"); + if (!UI.overlay) + return; + + let doc = element.ownerDocument.defaultView.top.document; + + this.objtabElement = doc.createElementNS("http://www.w3.org/1999/xhtml", "a"); + this.objtabElement.textContent = UI.overlay.attributes.objtabtext; + this.objtabElement.setAttribute("title", UI.overlay.attributes.objtabtooltip); + this.objtabElement.setAttribute("href", data.location); + this.objtabElement.setAttribute("class", this.objTabClassHidden); + this.objtabElement.style.setProperty("opacity", "1", "important"); + this.objtabElement.nodeData = data; + + this.currentElement = element; + + // Register paint listeners for the relevant windows + this.windowListeners = []; + let wnd = element.ownerDocument.defaultView; + while (wnd) + { + wnd.addEventListener("MozAfterPaint", objectWindowEventHandler, false); + this.windowListeners.push(wnd); + wnd = (wnd.parent != wnd ? wnd.parent : null); + } + + // Register mouse listeners on the object tab + this.objtabElement.addEventListener("mouseover", objectTabEventHander, false); + this.objtabElement.addEventListener("mouseout", objectTabEventHander, false); + this.objtabElement.addEventListener("click", objectTabEventHander, true); + + // Insert the tab into the document and adjust its position + doc.documentElement.appendChild(this.objtabElement); + if (!this.positionTimer) + { + this.positionTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + this.positionTimer.init(this, 200, Ci.nsITimer.TYPE_REPEATING_SLACK); + } + this._positionTab(); + }, + + /** + * Hides the tab element. + */ + _hideTab: function() + { + this.delayedShowParams = null; + + if (this.objtabElement) + { + // Prevent recursive calls via popuphidden handler + let objtab = this.objtabElement; + this.objtabElement = null; + this.currentElement = null; + + if (this.hideTimer) + { + this.hideTimer.cancel(); + this.hideTimer = null; + } + + if (this.positionTimer) + { + this.positionTimer.cancel(); + this.positionTimer = null; + } + + try { + objtab.parentNode.removeChild(objtab); + } catch (e) {} + objtab.removeEventListener("mouseover", objectTabEventHander, false); + objtab.removeEventListener("mouseout", objectTabEventHander, false); + objtab.nodeData = null; + + for (let wnd of this.windowListeners) + wnd.removeEventListener("MozAfterPaint", objectWindowEventHandler, false); + this.windowListeners = null; + } + }, + + /** + * Updates position of the tab element. + */ + _positionTab: function() + { + // Test whether element is still in document + let elementDoc = null; + try + { + elementDoc = this.currentElement.ownerDocument; + } catch (e) {} // Ignore "can't access dead object" error + if (!elementDoc || !this.currentElement.offsetWidth || !this.currentElement.offsetHeight || + !elementDoc.defaultView || !elementDoc.documentElement) + { + this._hideTab(); + return; + } + + let objRect = this._getElementPosition(this.currentElement); + + let className = this.objTabClassVisibleTop; + let left = objRect.right - this.objtabElement.offsetWidth; + let top = objRect.top - this.objtabElement.offsetHeight; + if (top < 0) + { + top = objRect.bottom; + className = this.objTabClassVisibleBottom; + } + + if (this.objtabElement.style.left != left + "px") + this.objtabElement.style.setProperty("left", left + "px", "important"); + if (this.objtabElement.style.top != top + "px") + this.objtabElement.style.setProperty("top", top + "px", "important"); + + if (this.objtabElement.getAttribute("class") != className) + this.objtabElement.setAttribute("class", className); + + this.prevPositionUpdate = Date.now(); + }, + + /** + * Calculates element's position relative to the top frame and considering + * clipping due to scrolling. + * @return {left: Number, top: Number, right: Number, bottom: Number} + */ + _getElementPosition: function(/**Element*/ element) + { + // Restrict rectangle coordinates by the boundaries of a window's client area + function intersectRect(rect, wnd) + { + // Cannot use wnd.innerWidth/Height because they won't account for scrollbars + let doc = wnd.document; + let wndWidth = doc.documentElement.clientWidth; + let wndHeight = doc.documentElement.clientHeight; + if (doc.compatMode == "BackCompat") // clientHeight will be bogus in quirks mode + wndHeight = Math.max(doc.documentElement.offsetHeight, doc.body.offsetHeight) - wnd.scrollMaxY - 1; + + rect.left = Math.max(rect.left, 0); + rect.top = Math.max(rect.top, 0); + rect.right = Math.min(rect.right, wndWidth); + rect.bottom = Math.min(rect.bottom, wndHeight); + } + + let rect = element.getBoundingClientRect(); + let wnd = element.ownerDocument.defaultView; + + let style = wnd.getComputedStyle(element, null); + let offsets = [ + parseFloat(style.borderLeftWidth) + parseFloat(style.paddingLeft), + parseFloat(style.borderTopWidth) + parseFloat(style.paddingTop), + parseFloat(style.borderRightWidth) + parseFloat(style.paddingRight), + parseFloat(style.borderBottomWidth) + parseFloat(style.paddingBottom) + ]; + + rect = {left: rect.left + offsets[0], top: rect.top + offsets[1], + right: rect.right - offsets[2], bottom: rect.bottom - offsets[3]}; + while (true) + { + intersectRect(rect, wnd); + + if (!wnd.frameElement) + break; + + // Recalculate coordinates to be relative to frame's parent window + let frameElement = wnd.frameElement; + wnd = frameElement.ownerDocument.defaultView; + + let frameRect = frameElement.getBoundingClientRect(); + let frameStyle = wnd.getComputedStyle(frameElement, null); + let relLeft = frameRect.left + parseFloat(frameStyle.borderLeftWidth) + parseFloat(frameStyle.paddingLeft); + let relTop = frameRect.top + parseFloat(frameStyle.borderTopWidth) + parseFloat(frameStyle.paddingTop); + + rect.left += relLeft; + rect.right += relLeft; + rect.top += relTop; + rect.bottom += relTop; + } + + return rect; + }, + + doBlock: function() + { + let {UI} = require("ui"); + let {Utils} = require("utils"); + let chromeWindow = Utils.getChromeWindow(this.currentElement.ownerDocument.defaultView); + UI.blockItem(chromeWindow, this.currentElement, this.objtabElement.nodeData); + }, + + /** + * Called whenever a timer fires. + */ + observe: function(/**nsISupport*/ subject, /**String*/ topic, /**String*/ data) + { + if (subject == this.positionTimer) + { + // Don't update position if it was already updated recently (via MozAfterPaint) + if (Date.now() - this.prevPositionUpdate > 100) + this._positionTab(); + } + else if (subject == this.hideTimer) + { + let now = Date.now(); + if (now >= this.hideTargetTime) + this._hideTab(); + else if (this.hideTargetTime - now < this.HIDE_DELAY / 2) + this.objtabElement.style.setProperty("opacity", (this.hideTargetTime - now) * 2 / this.HIDE_DELAY, "important"); + } + } +}; + +onShutdown.add(objTabs._hideTab.bind(objTabs)); + +/** + * Function called whenever the mouse enters or leaves an object. + */ +function objectMouseEventHander(/**Event*/ event) +{ + if (!event.isTrusted) + return; + + if (event.type == "mouseover") + objTabs.showTabFor(event.target); + else if (event.type == "mouseout") + objTabs.hideTabFor(event.target); +} + +/** + * Function called for paint events of the object tab window. + */ +function objectWindowEventHandler(/**Event*/ event) +{ + if (!event.isTrusted) + return; + + // Don't trigger update too often, avoid overusing CPU on frequent page updates + if (event.type == "MozAfterPaint" && Date.now() - objTabs.prevPositionUpdate > 20) + objTabs._positionTab(); +} + +/** + * Function called whenever the mouse enters or leaves an object tab. + */ +function objectTabEventHander(/**Event*/ event) +{ + if (onShutdown.done || !event.isTrusted) + return; + + if (event.type == "click" && event.button == 0) + { + event.preventDefault(); + event.stopPropagation(); + + objTabs.doBlock(); + } + else if (event.type == "mouseover") + objTabs.showTabFor(objTabs.currentElement); + else if (event.type == "mouseout") + objTabs.hideTabFor(objTabs.currentElement); +} +exports.objectMouseEventHander = objectMouseEventHander; diff --git a/data/extensions/spyblock@gnu.org/lib/prefs.js b/data/extensions/spyblock@gnu.org/lib/prefs.js new file mode 100644 index 0000000..e1b6ae0 --- /dev/null +++ b/data/extensions/spyblock@gnu.org/lib/prefs.js @@ -0,0 +1,203 @@ +/* + * This file is part of the Adblock Plus build tools, + * 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/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +let {addonRoot, addonName} = require("info"); +let branchName = "extensions." + addonName + "."; +let branch = Services.prefs.getBranch(branchName); +let ignorePrefChanges = false; + +function init() +{ + // Load default preferences and set up properties for them + let defaultBranch = Services.prefs.getDefaultBranch(branchName); + let scope = + { + pref: function(pref, value) + { + if (pref.substr(0, branchName.length) != branchName) + { + Cu.reportError(new Error("Ignoring default preference " + pref + ", wrong branch.")); + return; + } + pref = pref.substr(branchName.length); + + let [getter, setter] = typeMap[typeof value]; + setter(defaultBranch, pref, value); + defineProperty(pref, false, getter, setter); + } + }; + Services.scriptloader.loadSubScript(addonRoot + "defaults/prefs.js", scope); + + // Add preference change observer + try + { + branch.QueryInterface(Ci.nsIPrefBranch2).addObserver("", Prefs, true); + onShutdown.add(function() branch.removeObserver("", Prefs)); + } + catch (e) + { + Cu.reportError(e); + } +} + +/** + * Sets up getter/setter on Prefs object for preference. + */ +function defineProperty(/**String*/ name, defaultValue, /**Function*/ readFunc, /**Function*/ writeFunc) +{ + let value = defaultValue; + Prefs["_update_" + name] = function() + { + try + { + value = readFunc(branch, name); + triggerListeners(name); + } + catch(e) + { + Cu.reportError(e); + } + }; + Prefs.__defineGetter__(name, function() value); + Prefs.__defineSetter__(name, function(newValue) + { + if (value == newValue) + return value; + + try + { + ignorePrefChanges = true; + writeFunc(branch, name, newValue); + value = newValue; + Services.prefs.savePrefFile(null); + triggerListeners(name); + } + catch(e) + { + Cu.reportError(e); + } + finally + { + ignorePrefChanges = false; + } + return value; + }); + Prefs["_update_" + name](); +} + +let listeners = []; +function triggerListeners(/**String*/ name) +{ + for (let i = 0; i < listeners.length; i++) + { + try + { + listeners[i](name); + } + catch(e) + { + Cu.reportError(e); + } + } +} + +/** + * Manages the preferences for an extension, object properties corresponding + * to extension's preferences are added automatically. Setting the property + * will automatically change the preference, external preference changes are + * also recognized automatically. + */ +let Prefs = exports.Prefs = +{ + /** + * Migrates an old preference to a new name. + */ + migrate: function(/**String*/ oldName, /**String*/ newName) + { + if (newName in this && Services.prefs.prefHasUserValue(oldName)) + { + let [getter, setter] = typeMap[typeof this[newName]]; + try + { + this[newName] = getter(Services.prefs, oldName); + } catch(e) {} + Services.prefs.clearUserPref(oldName); + } + }, + + /** + * Adds a preferences listener that will be fired whenever a preference + * changes. + */ + addListener: function(/**Function*/ listener) + { + if (listeners.indexOf(listener) < 0) + listeners.push(listener); + }, + + /** + * Removes a preferences listener. + */ + removeListener: function(/**Function*/ listener) + { + let index = listeners.indexOf(listener); + if (index >= 0) + listeners.splice(index, 1); + }, + + observe: function(subject, topic, data) + { + if (ignorePrefChanges || topic != "nsPref:changed") + return; + + if ("_update_" + data in this) + this["_update_" + data](); + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsISupportsWeakReference, Ci.nsIObserver]) +}; + +// Getter/setter functions for difference preference types +let typeMap = +{ + boolean: [getBoolPref, setBoolPref], + number: [getIntPref, setIntPref], + string: [getCharPref, setCharPref], + object: [getJSONPref, setJSONPref] +}; + +function getIntPref(branch, pref) branch.getIntPref(pref) +function setIntPref(branch, pref, newValue) branch.setIntPref(pref, newValue) + +function getBoolPref(branch, pref) branch.getBoolPref(pref) +function setBoolPref(branch, pref, newValue) branch.setBoolPref(pref, newValue) + +function getCharPref(branch, pref) branch.getComplexValue(pref, Ci.nsISupportsString).data +function setCharPref(branch, pref, newValue) +{ + let str = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString); + str.data = newValue; + branch.setComplexValue(pref, Ci.nsISupportsString, str); +} + +function getJSONPref(branch, pref) JSON.parse(getCharPref(branch, pref)) +function setJSONPref(branch, pref, newValue) setCharPref(branch, pref, JSON.stringify(newValue)) + +init(); diff --git a/data/extensions/spyblock@gnu.org/lib/requestNotifier.js b/data/extensions/spyblock@gnu.org/lib/requestNotifier.js new file mode 100644 index 0000000..2ee9ec3 --- /dev/null +++ b/data/extensions/spyblock@gnu.org/lib/requestNotifier.js @@ -0,0 +1,378 @@ +/* + * 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/>. + */ + +/** + * @fileOverview Stores Adblock Plus data to be attached to a window. + */ + +Cu.import("resource://gre/modules/Services.jsm"); + +let {Utils} = require("utils"); +let {BlockingFilter, WhitelistFilter, ElemHideBase, ElemHideFilter, ElemHideException} = require("filterClasses"); + +let nodeData = new WeakMap(); +let windowStats = new WeakMap(); +let windowSelection = new WeakMap(); + +let setEntry, hasEntry, getEntry; +// Last issue(Bug 982561) preventing us from using WeakMap fixed for FF version 32 +if (Services.vc.compare(Utils.platformVersion, "32.0a1") >= 0) +{ + setEntry = (map, key, value) => map.set(key, value); + hasEntry = (map, key) => map.has(key); + getEntry = (map, key) => map.get(key); +} +else +{ + // Fall back to user data + let dataSeed = Math.random(); + let nodeDataProp = "abpNodeData" + dataSeed; + let windowStatsProp = "abpWindowStats" + dataSeed; + let windowSelectionProp = "abpWindowSelection" + dataSeed; + let getProp = function(map) + { + switch (map) + { + case nodeData: + return nodeDataProp; + case windowStats: + return windowStatsProp; + case windowSelection: + return windowSelectionProp; + default: + return null; + } + }; + + setEntry = (map, key, value) => key.setUserData(getProp(map), value, null); + hasEntry = (map, key) => key.getUserData(getProp(map)); + getEntry = (map, key) => key.getUserData(getProp(map)) || undefined; +} + +/** + * List of notifiers in use - these notifiers need to receive notifications on + * new requests. + * @type RequestNotifier[] + */ +let activeNotifiers = []; + +/** + * Creates a notifier object for a particular window. After creation the window + * will first be scanned for previously saved requests. Once that scan is + * complete only new requests for this window will be reported. + * @param {Window} wnd window to attach the notifier to + * @param {Function} listener listener to be called whenever a new request is found + * @param {Object} [listenerObj] "this" pointer to be used when calling the listener + */ +function RequestNotifier(wnd, listener, listenerObj) +{ + this.window = wnd; + this.listener = listener; + this.listenerObj = listenerObj || null; + activeNotifiers.push(this); + if (wnd) + this.startScan(wnd); + else + this.scanComplete = true; +} +exports.RequestNotifier = RequestNotifier; + +RequestNotifier.prototype = +{ + /** + * The window this notifier is associated with. + * @type Window + */ + window: null, + + /** + * The listener to be called when a new request is found. + * @type Function + */ + listener: null, + + /** + * "this" pointer to be used when calling the listener. + * @type Object + */ + listenerObj: null, + + /** + * Will be set to true once the initial window scan is complete. + * @type Boolean + */ + scanComplete: false, + + /** + * Shuts down the notifier once it is no longer used. The listener + * will no longer be called after that. + */ + shutdown: function() + { + delete this.window; + delete this.listener; + delete this.listenerObj; + + for (let i = activeNotifiers.length - 1; i >= 0; i--) + if (activeNotifiers[i] == this) + activeNotifiers.splice(i, 1); + }, + + /** + * Notifies listener about a new request. + */ + notifyListener: function(/**Window*/ wnd, /**Node*/ node, /**RequestEntry*/ entry) + { + this.listener.call(this.listenerObj, wnd, node, entry, this.scanComplete); + }, + + /** + * Number of currently posted scan events (will be 0 when the scan finishes + * running). + */ + eventsPosted: 0, + + /** + * Starts the initial scan of the window (will recurse into frames). + * @param {Window} wnd the window to be scanned + */ + startScan: function(wnd) + { + let doc = wnd.document; + let walker = doc.createTreeWalker(doc, Ci.nsIDOMNodeFilter.SHOW_ELEMENT, null, false); + + let process = function() + { + if (!this.listener) + return; + + let node = walker.currentNode; + let data = getEntry(nodeData, node); + if (typeof data != "undefined") + for (let k in data) + this.notifyListener(wnd, node, data[k]); + + if (walker.nextNode()) + Utils.runAsync(process); + else + { + // Done with the current window, start the scan for its frames + for (let i = 0; i < wnd.frames.length; i++) + this.startScan(wnd.frames[i]); + + this.eventsPosted--; + if (!this.eventsPosted) + { + this.scanComplete = true; + this.notifyListener(wnd, null, null); + } + } + }.bind(this); + + // Process each node in a separate event to allow other events to process + this.eventsPosted++; + Utils.runAsync(process); + } +}; + +RequestNotifier.storeSelection = function(/**Window*/ wnd, /**String*/ selection) +{ + setEntry(windowSelection, wnd.document, selection); +}; +RequestNotifier.getSelection = function(/**Window*/ wnd) /**String*/ +{ + if (hasEntry(windowSelection, wnd.document)) + return getEntry(windowSelection, wnd.document); + else + return null; +}; + +/** + * Attaches request data to a DOM node. + * @param {Node} node node to attach data to + * @param {Window} topWnd top-level window the node belongs to + * @param {Integer} contentType request type, one of the Policy.type.* constants + * @param {String} docDomain domain of the document that initiated the request + * @param {Boolean} thirdParty will be true if a third-party server has been requested + * @param {String} location the address that has been requested + * @param {Filter} filter filter applied to the request or null if none + */ +RequestNotifier.addNodeData = function(/**Node*/ node, /**Window*/ topWnd, /**Integer*/ contentType, /**String*/ docDomain, /**Boolean*/ thirdParty, /**String*/ location, /**Filter*/ filter) +{ + return new RequestEntry(node, topWnd, contentType, docDomain, thirdParty, location, filter); +} + +/** + * Retrieves the statistics for a window. + * @result {Object} Object with the properties items, blocked, whitelisted, hidden, filters containing statistics for the window (might be null) + */ +RequestNotifier.getWindowStatistics = function(/**Window*/ wnd) +{ + if (hasEntry(windowStats, wnd.document)) + return getEntry(windowStats, wnd.document); + else + return null; +} + +/** + * Retrieves the request entry associated with a DOM node. + * @param {Node} node + * @param {Boolean} noParent if missing or false, the search will extend to the parent nodes until one is found that has data associated with it + * @param {Integer} [type] request type to be looking for + * @param {String} [location] request location to be looking for + * @result {[Node, RequestEntry]} + * @static + */ +RequestNotifier.getDataForNode = function(node, noParent, type, location) +{ + while (node) + { + let data = getEntry(nodeData, node); + if (typeof data != "undefined") + { + // Look for matching entry + for (let k in data) + { + let entry = data[k]; + if ((typeof type == "undefined" || entry.type == type) && + (typeof location == "undefined" || entry.location == location)) + { + return [node, entry]; + } + } + } + + // If we don't have any match on this node then maybe its parent will do + if ((typeof noParent != "boolean" || !noParent) && + node.parentNode instanceof Ci.nsIDOMElement) + { + node = node.parentNode; + } + else + { + node = null; + } + } + + return null; +}; + +function RequestEntry(node, topWnd, contentType, docDomain, thirdParty, location, filter) +{ + this.type = contentType; + this.docDomain = docDomain; + this.thirdParty = thirdParty; + this.location = location; + this.filter = filter; + + this.attachToNode(node); + + // Update window statistics + if (!hasEntry(windowStats, topWnd.document)) + { + setEntry(windowStats, topWnd.document, { + items: 0, + hidden: 0, + blocked: 0, + whitelisted: 0, + filters: {} + }); + } + + let stats = getEntry(windowStats, topWnd.document); + if (!filter || !(filter instanceof ElemHideBase)) + stats.items++; + if (filter) + { + if (filter instanceof BlockingFilter) + stats.blocked++; + else if (filter instanceof WhitelistFilter || filter instanceof ElemHideException) + stats.whitelisted++; + else if (filter instanceof ElemHideFilter) + stats.hidden++; + + if (filter.text in stats.filters) + stats.filters[filter.text]++; + else + stats.filters[filter.text] = 1; + } + + // Notify listeners + for (let notifier of activeNotifiers) + if (!notifier.window || notifier.window == topWnd) + notifier.notifyListener(topWnd, node, this); +} +RequestEntry.prototype = +{ + /** + * Content type of the request (one of the nsIContentPolicy constants) + * @type Integer + */ + type: null, + /** + * Domain name of the requesting document + * @type String + */ + docDomain: null, + /** + * True if the request goes to a different domain than the domain of the containing document + * @type Boolean + */ + thirdParty: false, + /** + * Address being requested + * @type String + */ + location: null, + /** + * Filter that was applied to this request (if any) + * @type Filter + */ + filter: null, + /** + * String representation of the content type, e.g. "subdocument" + * @type String + */ + get typeDescr() + { + return require("contentPolicy").Policy.typeDescr[this.type]; + }, + /** + * User-visible localized representation of the content type, e.g. "frame" + * @type String + */ + get localizedDescr() + { + return require("contentPolicy").Policy.localizedDescr[this.type]; + }, + + /** + * Attaches this request object to a DOM node. + */ + attachToNode: function(/**Node*/ node) + { + let existingData = getEntry(nodeData, node); + if (typeof existingData == "undefined") + { + existingData = {}; + setEntry(nodeData, node, existingData); + } + + // Add this request to the node data + existingData[this.type + " " + this.location] = this; + } +}; diff --git a/data/extensions/spyblock@gnu.org/lib/subscriptionClasses.js b/data/extensions/spyblock@gnu.org/lib/subscriptionClasses.js new file mode 100644 index 0000000..13dceaf --- /dev/null +++ b/data/extensions/spyblock@gnu.org/lib/subscriptionClasses.js @@ -0,0 +1,597 @@ +/* + * 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/>. + */ + +/** + * @fileOverview Definition of Subscription class and its subclasses. + */ + +Cu.import("resource://gre/modules/Services.jsm"); + +let {ActiveFilter, BlockingFilter, WhitelistFilter, ElemHideBase} = require("filterClasses"); +let {FilterNotifier} = require("filterNotifier"); + +/** + * Abstract base class for filter subscriptions + * + * @param {String} url download location of the subscription + * @param {String} [title] title of the filter subscription + * @constructor + */ +function Subscription(url, title) +{ + this.url = url; + this.filters = []; + if (title) + this._title = title; + else + { + let {Utils} = require("utils"); + this._title = Utils.getString("newGroup_title"); + } + Subscription.knownSubscriptions[url] = this; +} +exports.Subscription = Subscription; + +Subscription.prototype = +{ + /** + * Download location of the subscription + * @type String + */ + url: null, + + /** + * Filters contained in the filter subscription + * @type Array of Filter + */ + filters: null, + + _title: null, + _fixedTitle: false, + _disabled: false, + + /** + * Title of the filter subscription + * @type String + */ + get title() + { + return this._title; + }, + set title(value) + { + if (value != this._title) + { + let oldValue = this._title; + this._title = value; + FilterNotifier.triggerListeners("subscription.title", this, value, oldValue); + } + return this._title; + }, + + /** + * Determines whether the title should be editable + * @type Boolean + */ + get fixedTitle() + { + return this._fixedTitle; + }, + set fixedTitle(value) + { + if (value != this._fixedTitle) + { + let oldValue = this._fixedTitle; + this._fixedTitle = value; + FilterNotifier.triggerListeners("subscription.fixedTitle", this, value, oldValue); + } + return this._fixedTitle; + }, + + /** + * Defines whether the filters in the subscription should be disabled + * @type Boolean + */ + get disabled() + { + return this._disabled; + }, + set disabled(value) + { + if (value != this._disabled) + { + let oldValue = this._disabled; + this._disabled = value; + FilterNotifier.triggerListeners("subscription.disabled", this, value, oldValue); + } + return this._disabled; + }, + + /** + * Serializes the filter to an array of strings for writing out on the disk. + * @param {Array of String} buffer buffer to push the serialization results into + */ + serialize: function(buffer) + { + buffer.push("[Subscription]"); + buffer.push("url=" + this.url); + buffer.push("title=" + this._title); + if (this._fixedTitle) + buffer.push("fixedTitle=true"); + if (this._disabled) + buffer.push("disabled=true"); + }, + + serializeFilters: function(buffer) + { + for (let filter of this.filters) + buffer.push(filter.text.replace(/\[/g, "\\[")); + }, + + toString: function() + { + let buffer = []; + this.serialize(buffer); + return buffer.join("\n"); + } +}; + +/** + * Cache for known filter subscriptions, maps URL to subscription objects. + * @type Object + */ +Subscription.knownSubscriptions = {__proto__: null}; + +/** + * Returns a subscription from its URL, creates a new one if necessary. + * @param {String} url URL of the subscription + * @return {Subscription} subscription or null if the subscription couldn't be created + */ +Subscription.fromURL = function(url) +{ + if (url in Subscription.knownSubscriptions) + return Subscription.knownSubscriptions[url]; + + try + { + // Test URL for validity + url = Services.io.newURI(url, null, null).spec; + return new DownloadableSubscription(url, null); + } + catch (e) + { + return new SpecialSubscription(url); + } +} + +/** + * Deserializes a subscription + * + * @param {Object} obj map of serialized properties and their values + * @return {Subscription} subscription or null if the subscription couldn't be created + */ +Subscription.fromObject = function(obj) +{ + let result; + try + { + obj.url = Services.io.newURI(obj.url, null, null).spec; + + // URL is valid - this is a downloadable subscription + result = new DownloadableSubscription(obj.url, obj.title); + if ("downloadStatus" in obj) + result._downloadStatus = obj.downloadStatus; + if ("lastSuccess" in obj) + result.lastSuccess = parseInt(obj.lastSuccess) || 0; + if ("lastCheck" in obj) + result._lastCheck = parseInt(obj.lastCheck) || 0; + if ("expires" in obj) + result.expires = parseInt(obj.expires) || 0; + if ("softExpiration" in obj) + result.softExpiration = parseInt(obj.softExpiration) || 0; + if ("errors" in obj) + result._errors = parseInt(obj.errors) || 0; + if ("version" in obj) + result.version = parseInt(obj.version) || 0; + if ("requiredVersion" in obj) + { + let {addonVersion} = require("info"); + result.requiredVersion = obj.requiredVersion; + if (Services.vc.compare(result.requiredVersion, addonVersion) > 0) + result.upgradeRequired = true; + } + if ("homepage" in obj) + result._homepage = obj.homepage; + if ("lastDownload" in obj) + result._lastDownload = parseInt(obj.lastDownload) || 0; + } + catch (e) + { + // Invalid URL - custom filter group + if (!("title" in obj)) + { + // Backwards compatibility - titles and filter types were originally + // determined by group identifier. + if (obj.url == "~wl~") + obj.defaults = "whitelist"; + else if (obj.url == "~fl~") + obj.defaults = "blocking"; + else if (obj.url == "~eh~") + obj.defaults = "elemhide"; + if ("defaults" in obj) + { + let {Utils} = require("utils"); + obj.title = Utils.getString(obj.defaults + "Group_title"); + } + } + result = new SpecialSubscription(obj.url, obj.title); + if ("defaults" in obj) + result.defaults = obj.defaults.split(" "); + } + if ("fixedTitle" in obj) + result._fixedTitle = (obj.fixedTitle == "true"); + if ("privateMode" in obj) + result.privateMode = (obj.privateMode == "true"); + if ("disabled" in obj) + result._disabled = (obj.disabled == "true"); + + return result; +} + +/** + * Class for special filter subscriptions (user's filters) + * @param {String} url see Subscription() + * @param {String} [title] see Subscription() + * @constructor + * @augments Subscription + */ +function SpecialSubscription(url, title) +{ + Subscription.call(this, url, title); +} +exports.SpecialSubscription = SpecialSubscription; + +SpecialSubscription.prototype = +{ + __proto__: Subscription.prototype, + + /** + * Filter types that should be added to this subscription by default + * (entries should correspond to keys in SpecialSubscription.defaultsMap). + * @type Array of String + */ + defaults: null, + + /** + * Tests whether a filter should be added to this group by default + * @param {Filter} filter filter to be tested + * @return {Boolean} + */ + isDefaultFor: function(filter) + { + if (this.defaults && this.defaults.length) + { + for (let type of this.defaults) + { + if (filter instanceof SpecialSubscription.defaultsMap[type]) + return true; + if (!(filter instanceof ActiveFilter) && type == "blacklist") + return true; + } + } + + return false; + }, + + /** + * See Subscription.serialize() + */ + serialize: function(buffer) + { + Subscription.prototype.serialize.call(this, buffer); + if (this.defaults && this.defaults.length) + buffer.push("defaults=" + this.defaults.filter((type) => type in SpecialSubscription.defaultsMap).join(" ")); + if (this._lastDownload) + buffer.push("lastDownload=" + this._lastDownload); + } +}; + +SpecialSubscription.defaultsMap = { + __proto__: null, + "whitelist": WhitelistFilter, + "blocking": BlockingFilter, + "elemhide": ElemHideBase +}; + +/** + * Creates a new user-defined filter group. + * @param {String} [title] title of the new filter group + * @result {SpecialSubscription} + */ +SpecialSubscription.create = function(title) +{ + let url; + do + { + url = "~user~" + Math.round(Math.random()*1000000); + } while (url in Subscription.knownSubscriptions); + return new SpecialSubscription(url, title) +}; + +/** + * Creates a new user-defined filter group and adds the given filter to it. + * This group will act as the default group for this filter type. + */ +SpecialSubscription.createForFilter = function(/**Filter*/ filter) /**SpecialSubscription*/ +{ + let subscription = SpecialSubscription.create(); + subscription.filters.push(filter); + for (let type in SpecialSubscription.defaultsMap) + { + if (filter instanceof SpecialSubscription.defaultsMap[type]) + subscription.defaults = [type]; + } + if (!subscription.defaults) + subscription.defaults = ["blocking"]; + + let {Utils} = require("utils"); + subscription.title = Utils.getString(subscription.defaults[0] + "Group_title"); + return subscription; +}; + +/** + * Abstract base class for regular filter subscriptions (both internally and externally updated) + * @param {String} url see Subscription() + * @param {String} [title] see Subscription() + * @constructor + * @augments Subscription + */ +function RegularSubscription(url, title) +{ + Subscription.call(this, url, title || url); +} +exports.RegularSubscription = RegularSubscription; + +RegularSubscription.prototype = +{ + __proto__: Subscription.prototype, + + _homepage: null, + _lastDownload: 0, + + /** + * Filter subscription homepage if known + * @type String + */ + get homepage() + { + return this._homepage; + }, + set homepage(value) + { + if (value != this._homepage) + { + let oldValue = this._homepage; + this._homepage = value; + FilterNotifier.triggerListeners("subscription.homepage", this, value, oldValue); + } + return this._homepage; + }, + + /** + * Time of the last subscription download (in seconds since the beginning of the epoch) + * @type Number + */ + get lastDownload() + { + return this._lastDownload; + }, + set lastDownload(value) + { + if (value != this._lastDownload) + { + let oldValue = this._lastDownload; + this._lastDownload = value; + FilterNotifier.triggerListeners("subscription.lastDownload", this, value, oldValue); + } + return this._lastDownload; + }, + + /** + * See Subscription.serialize() + */ + serialize: function(buffer) + { + Subscription.prototype.serialize.call(this, buffer); + if (this._homepage) + buffer.push("homepage=" + this._homepage); + if (this._lastDownload) + buffer.push("lastDownload=" + this._lastDownload); + } +}; + +/** + * Class for filter subscriptions updated by externally (by other extension) + * @param {String} url see Subscription() + * @param {String} [title] see Subscription() + * @constructor + * @augments RegularSubscription + */ +function ExternalSubscription(url, title) +{ + RegularSubscription.call(this, url, title); +} +exports.ExternalSubscription = ExternalSubscription; + +ExternalSubscription.prototype = +{ + __proto__: RegularSubscription.prototype, + + /** + * See Subscription.serialize() + */ + serialize: function(buffer) + { + throw new Error("Unexpected call, external subscriptions should not be serialized"); + } +}; + +/** + * Class for filter subscriptions updated by externally (by other extension) + * @param {String} url see Subscription() + * @param {String} [title] see Subscription() + * @constructor + * @augments RegularSubscription + */ +function DownloadableSubscription(url, title) +{ + RegularSubscription.call(this, url, title); +} +exports.DownloadableSubscription = DownloadableSubscription; + +DownloadableSubscription.prototype = +{ + __proto__: RegularSubscription.prototype, + + _downloadStatus: null, + _lastCheck: 0, + _errors: 0, + + /** + * Status of the last download (ID of a string) + * @type String + */ + get downloadStatus() + { + return this._downloadStatus; + }, + set downloadStatus(value) + { + let oldValue = this._downloadStatus; + this._downloadStatus = value; + FilterNotifier.triggerListeners("subscription.downloadStatus", this, value, oldValue); + return this._downloadStatus; + }, + + /** + * Time of the last successful download (in seconds since the beginning of the + * epoch). + */ + lastSuccess: 0, + + /** + * Time when the subscription was considered for an update last time (in seconds + * since the beginning of the epoch). This will be used to increase softExpiration + * if the user doesn't use Adblock Plus for some time. + * @type Number + */ + get lastCheck() + { + return this._lastCheck; + }, + set lastCheck(value) + { + if (value != this._lastCheck) + { + let oldValue = this._lastCheck; + this._lastCheck = value; + FilterNotifier.triggerListeners("subscription.lastCheck", this, value, oldValue); + } + return this._lastCheck; + }, + + /** + * Hard expiration time of the filter subscription (in seconds since the beginning of the epoch) + * @type Number + */ + expires: 0, + + /** + * Soft expiration time of the filter subscription (in seconds since the beginning of the epoch) + * @type Number + */ + softExpiration: 0, + + /** + * Number of download failures since last success + * @type Number + */ + get errors() + { + return this._errors; + }, + set errors(value) + { + if (value != this._errors) + { + let oldValue = this._errors; + this._errors = value; + FilterNotifier.triggerListeners("subscription.errors", this, value, oldValue); + } + return this._errors; + }, + + /** + * Version of the subscription data retrieved on last successful download + * @type Number + */ + version: 0, + + /** + * Minimal Adblock Plus version required for this subscription + * @type String + */ + requiredVersion: null, + + /** + * Should be true if requiredVersion is higher than current Adblock Plus version + * @type Boolean + */ + upgradeRequired: false, + + /** + * Should be true if the Privatemode: header is set to true in the subscription + * @type Boolean + */ + privateMode: false, + + /** + * See Subscription.serialize() + */ + serialize: function(buffer) + { + RegularSubscription.prototype.serialize.call(this, buffer); + if (this.downloadStatus) + buffer.push("downloadStatus=" + this.downloadStatus); + if (this.lastSuccess) + buffer.push("lastSuccess=" + this.lastSuccess); + if (this.lastCheck) + buffer.push("lastCheck=" + this.lastCheck); + if (this.expires) + buffer.push("expires=" + this.expires); + if (this.softExpiration) + buffer.push("softExpiration=" + this.softExpiration); + if (this.errors) + buffer.push("errors=" + this.errors); + if (this.version) + buffer.push("version=" + this.version); + if (this.requiredVersion) + buffer.push("requiredVersion=" + this.requiredVersion); + if (this.privateMode) + buffer.push("privateMode=" + this.privateMode); + } +}; diff --git a/data/extensions/spyblock@gnu.org/lib/sync.js b/data/extensions/spyblock@gnu.org/lib/sync.js new file mode 100644 index 0000000..05eeced --- /dev/null +++ b/data/extensions/spyblock@gnu.org/lib/sync.js @@ -0,0 +1,459 @@ +/* + * 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/>. + */ + +/** + * @fileOverview Firefox Sync integration + */ + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +let {FilterStorage} = require("filterStorage"); +let {FilterNotifier} = require("filterNotifier"); +let {Synchronizer} = require("synchronizer"); +let {Subscription, SpecialSubscription, DownloadableSubscription, ExternalSubscription} = require("subscriptionClasses"); +let {Filter, ActiveFilter} = require("filterClasses"); + +// Firefox Sync classes are set later in initEngine() +let Service, Engines, SyncEngine, Store, Tracker; + +/** + * ID of the only record stored + * @type String + */ +let filtersRecordID = "6fad6286-8207-46b6-aa39-8e0ce0bd7c49"; + +let Sync = exports.Sync = +{ + /** + * Will be set to true if/when Weave starts up. + * @type Boolean + */ + initialized: false, + + /** + * Whether Weave requested us to track changes. + * @type Boolean + */ + trackingEnabled: false, + + /** + * Returns Adblock Plus sync engine. + * @result Engine + */ + getEngine: function() + { + if (this.initialized) + return Engines.get("adblockplus"); + else + return null; + } +}; + +/** + * Listens to notifications from Sync service. + */ +let SyncServiceObserver = +{ + init: function() + { + try + { + let {Status, STATUS_DISABLED, CLIENT_NOT_CONFIGURED} = Cu.import("resource://services-sync/status.js", null); + Sync.initialized = Status.ready; + Sync.trackingEnabled = (Status.service != STATUS_DISABLED && Status.service != CLIENT_NOT_CONFIGURED); + } + catch (e) + { + return; + } + + if (Sync.initialized) + this.initEngine(); + else + Services.obs.addObserver(this, "weave:service:ready", true); + Services.obs.addObserver(this, "weave:engine:start-tracking", true); + Services.obs.addObserver(this, "weave:engine:stop-tracking", true); + + onShutdown.add(function() + { + try + { + Services.obs.removeObserver(this, "weave:service:ready"); + } catch (e) {} + Services.obs.removeObserver(this, "weave:engine:start-tracking"); + Services.obs.removeObserver(this, "weave:engine:stop-tracking"); + }.bind(this)); + }, + + initEngine: function() + { + ({Engines, SyncEngine, Store, Tracker} = Cu.import("resource://services-sync/engines.js")); + if (typeof Engines == "undefined") + { + ({Service} = Cu.import("resource://services-sync/service.js")); + Engines = Service.engineManager; + } + + ABPEngine.prototype.__proto__ = SyncEngine.prototype; + ABPStore.prototype.__proto__ = Store.prototype; + ABPTracker.prototype.__proto__ = Tracker.prototype; + + Engines.register(ABPEngine); + onShutdown.add(function() + { + Engines.unregister("adblockplus"); + }); + }, + + observe: function(subject, topic, data) + { + switch (topic) + { + case "weave:service:ready": + if (Sync.initialized) + return; + + this.initEngine(); + Sync.initialized = true; + break; + case "weave:engine:start-tracking": + Sync.trackingEnabled = true; + if (trackerInstance) + trackerInstance.startTracking(); + break; + case "weave:engine:stop-tracking": + Sync.trackingEnabled = false; + if (trackerInstance) + trackerInstance.stopTracking(); + break; + } + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]), +}; + +function ABPEngine() +{ + SyncEngine.call(this, "AdblockPlus", Service); +} +ABPEngine.prototype = +{ + _storeObj: ABPStore, + _trackerObj: ABPTracker, + version: 1, + + _reconcile: function(item) + { + // Always process server data, we will do the merging ourselves + return true; + } +}; + +function ABPStore(name, engine) +{ + Store.call(this, name, engine); +} +ABPStore.prototype = +{ + getAllIDs: function() + { + let result = {} + result[filtersRecordID] = true; + return result; + }, + + changeItemID: function(oldId, newId) + { + // This should not be called, our engine doesn't implement _findDupe + throw Cr.NS_ERROR_UNEXPECTED; + }, + + itemExists: function(id) + { + // Only one id exists so far + return (id == filtersRecordID); + }, + + createRecord: function(id, collection) + { + let record = new ABPEngine.prototype._recordObj(collection, id); + if (id == filtersRecordID) + { + record.cleartext = { + id: id, + subscriptions: [], + }; + for (let subscription of FilterStorage.subscriptions) + { + if (subscription instanceof ExternalSubscription) + continue; + + let subscriptionEntry = + { + url: subscription.url, + disabled: subscription.disabled + }; + if (subscription instanceof SpecialSubscription) + { + subscriptionEntry.filters = []; + for (let filter of subscription.filters) + { + let filterEntry = {text: filter.text}; + if (filter instanceof ActiveFilter) + filterEntry.disabled = filter.disabled; + subscriptionEntry.filters.push(filterEntry); + } + } + else + subscriptionEntry.title = subscription.title; + record.cleartext.subscriptions.push(subscriptionEntry); + } + + // Data sent, forget about local changes now + trackerInstance.clearPrivateChanges() + } + else + record.deleted = true; + + return record; + }, + + create: function(record) + { + // This should not be called because our record list doesn't change but + // call update just in case. + this.update(record); + }, + + update: function(record) + { + if (record.id != filtersRecordID) + return; + + this._log.trace("Merging in remote data"); + + let data = record.cleartext.subscriptions; + + // First make sure we have the same subscriptions on both sides + let seenSubscription = {__proto__: null}; + for (let remoteSubscription of data) + { + seenSubscription[remoteSubscription.url] = true; + if (remoteSubscription.url in FilterStorage.knownSubscriptions) + { + let subscription = FilterStorage.knownSubscriptions[remoteSubscription.url]; + if (!trackerInstance.didSubscriptionChange(remoteSubscription)) + { + // Only change local subscription if there were no changes, otherwise dismiss remote changes + subscription.disabled = remoteSubscription.disabled; + if (subscription instanceof DownloadableSubscription) + subscription.title = remoteSubscription.title; + } + } + else if (!trackerInstance.didSubscriptionChange(remoteSubscription)) + { + // Subscription was added remotely, add it locally as well + let subscription = Subscription.fromURL(remoteSubscription.url); + if (!subscription) + continue; + + subscription.disabled = remoteSubscription.disabled; + if (subscription instanceof DownloadableSubscription) + { + subscription.title = remoteSubscription.title; + FilterStorage.addSubscription(subscription); + Synchronizer.execute(subscription); + } + } + } + + for (let subscription of FilterStorage.subscriptions.slice()) + { + if (!(subscription.url in seenSubscription) && subscription instanceof DownloadableSubscription && !trackerInstance.didSubscriptionChange(subscription)) + { + // Subscription was removed remotely, remove it locally as well + FilterStorage.removeSubscription(subscription); + } + } + + // Now sync the custom filters + let seenFilter = {__proto__: null}; + for (let remoteSubscription of data) + { + if (!("filters" in remoteSubscription)) + continue; + + for (let remoteFilter of remoteSubscription.filters) + { + seenFilter[remoteFilter.text] = true; + + let filter = Filter.fromText(remoteFilter.text); + if (trackerInstance.didFilterChange(filter)) + continue; + + if (filter.subscriptions.some((subscription) => subscription instanceof SpecialSubscription)) + { + // Filter might have been changed remotely + if (filter instanceof ActiveFilter) + filter.disabled = remoteFilter.disabled; + } + else + { + // Filter was added remotely, add it locally as well + FilterStorage.addFilter(filter); + } + } + } + + for (let subscription of FilterStorage.subscriptions) + { + if (!(subscription instanceof SpecialSubscription)) + continue; + + for (let filter of subscription.filters.slice()) + { + if (!(filter.text in seenFilter) && !trackerInstance.didFilterChange(filter)) + { + // Filter was removed remotely, remove it locally as well + FilterStorage.removeFilter(filter); + } + } + } + + // Merge done, forget about local changes now + trackerInstance.clearPrivateChanges() + }, + + remove: function(record) + { + // Shouldn't be called but if it is - ignore + }, + + wipe: function() + { + this._log.trace("Got wipe command, removing all data"); + + for (let subscription of FilterStorage.subscriptions.slice()) + { + if (subscription instanceof DownloadableSubscription) + FilterStorage.removeSubscription(subscription); + else if (subscription instanceof SpecialSubscription) + { + for (let filter of subscription.filters.slice()) + FilterStorage.removeFilter(filter); + } + } + + // Data wiped, forget about local changes now + trackerInstance.clearPrivateChanges() + } +}; + +/** + * Hack to allow store to use the tracker - store tracker pointer globally. + */ +let trackerInstance = null; + +function ABPTracker(name, engine) +{ + Tracker.call(this, name, engine); + + this.privateTracker = new Tracker(name + ".private", engine); + trackerInstance = this; + + this.onChange = this.onChange.bind(this); + + if (Sync.trackingEnabled) + this.startTracking(); +} +ABPTracker.prototype = +{ + privateTracker: null, + + startTracking: function() + { + FilterNotifier.addListener(this.onChange); + }, + + stopTracking: function() + { + FilterNotifier.removeListener(this.onChange); + }, + + clearPrivateChanges: function() + { + this.privateTracker.clearChangedIDs(); + }, + + addPrivateChange: function(id) + { + // Ignore changes during syncing + if (this.ignoreAll) + return; + + this.addChangedID(filtersRecordID); + this.privateTracker.addChangedID(id); + this.score += 10; + }, + + didSubscriptionChange: function(subscription) + { + return ("subscription " + subscription.url) in this.privateTracker.changedIDs; + }, + + didFilterChange: function(filter) + { + return ("filter " + filter.text) in this.privateTracker.changedIDs; + }, + + onChange: function(action, item) + { + switch (action) + { + case "subscription.updated": + if ("oldSubscription" in item) + { + // Subscription moved to a new address + this.addPrivateChange("subscription " + item.url); + this.addPrivateChange("subscription " + item.oldSubscription.url); + } + else if (item instanceof SpecialSubscription) + { + // User's filters changed via Preferences window + for (let filter of item.filters) + this.addPrivateChange("filter " + filter.text); + for (let filter of item.oldFilters) + this.addPrivateChange("filter " + filter.text); + } + break; + case "subscription.added": + case "subscription.removed": + case "subscription.disabled": + case "subscription.title": + this.addPrivateChange("subscription " + item.url); + break; + case "filter.added": + case "filter.removed": + case "filter.disabled": + this.addPrivateChange("filter " + item.text); + break; + } + } +}; + +SyncServiceObserver.init(); diff --git a/data/extensions/spyblock@gnu.org/lib/synchronizer.js b/data/extensions/spyblock@gnu.org/lib/synchronizer.js new file mode 100644 index 0000000..b9f9e29 --- /dev/null +++ b/data/extensions/spyblock@gnu.org/lib/synchronizer.js @@ -0,0 +1,330 @@ +/* + * 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/>. + */ + +/** + * @fileOverview Manages synchronization of filter subscriptions. + */ + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +let {TimeLine} = require("timeline"); +let {Downloader, Downloadable, + MILLIS_IN_SECOND, MILLIS_IN_MINUTE, MILLIS_IN_HOUR, MILLIS_IN_DAY} = require("downloader"); +let {Filter, CommentFilter} = require("filterClasses"); +let {FilterStorage} = require("filterStorage"); +let {FilterNotifier} = require("filterNotifier"); +let {Prefs} = require("prefs"); +let {Subscription, DownloadableSubscription} = require("subscriptionClasses"); +let {Utils} = require("utils"); + +let INITIAL_DELAY = 6 * MILLIS_IN_MINUTE; +let CHECK_INTERVAL = 1 * MILLIS_IN_HOUR; +let DEFAULT_EXPIRATION_INTERVAL = 5 * MILLIS_IN_DAY; + +/** + * The object providing actual downloading functionality. + * @type Downloader + */ +let downloader = null; + +/** + * This object is responsible for downloading filter subscriptions whenever + * necessary. + * @class + */ +let Synchronizer = exports.Synchronizer = +{ + /** + * Called on module startup. + */ + init: function() + { + TimeLine.enter("Entered Synchronizer.init()"); + + downloader = new Downloader(this._getDownloadables.bind(this), INITIAL_DELAY, CHECK_INTERVAL); + onShutdown.add(function() + { + downloader.cancel(); + }); + + downloader.onExpirationChange = this._onExpirationChange.bind(this); + downloader.onDownloadStarted = this._onDownloadStarted.bind(this); + downloader.onDownloadSuccess = this._onDownloadSuccess.bind(this); + downloader.onDownloadError = this._onDownloadError.bind(this); + + TimeLine.leave("Synchronizer.init() done"); + }, + + /** + * Checks whether a subscription is currently being downloaded. + * @param {String} url URL of the subscription + * @return {Boolean} + */ + isExecuting: function(url) + { + return downloader.isDownloading(url); + }, + + /** + * Starts the download of a subscription. + * @param {DownloadableSubscription} subscription Subscription to be downloaded + * @param {Boolean} manual true for a manually started download (should not trigger fallback requests) + */ + execute: function(subscription, manual) + { + downloader.download(this._getDownloadable(subscription, manual)); + }, + + /** + * Yields Downloadable instances for all subscriptions that can be downloaded. + */ + _getDownloadables: function() + { + if (!Prefs.subscriptions_autoupdate) + return; + + for (let subscription of FilterStorage.subscriptions) + { + if (subscription instanceof DownloadableSubscription) + yield this._getDownloadable(subscription, false); + } + }, + + /** + * Creates a Downloadable instance for a subscription. + */ + _getDownloadable: function(/**Subscription*/ subscription, /**Boolean*/ manual) /**Downloadable*/ + { + let result = new Downloadable(subscription.url); + if (subscription.lastDownload != subscription.lastSuccess) + result.lastError = subscription.lastDownload * MILLIS_IN_SECOND; + result.lastCheck = subscription.lastCheck * MILLIS_IN_SECOND; + result.lastVersion = subscription.version; + result.softExpiration = subscription.softExpiration * MILLIS_IN_SECOND; + result.hardExpiration = subscription.expires * MILLIS_IN_SECOND; + result.manual = manual; + result.privateMode = subscription.privateMode; + return result; + }, + + _onExpirationChange: function(downloadable) + { + let subscription = Subscription.fromURL(downloadable.url); + subscription.lastCheck = Math.round(downloadable.lastCheck / MILLIS_IN_SECOND); + subscription.softExpiration = Math.round(downloadable.softExpiration / MILLIS_IN_SECOND); + subscription.expires = Math.round(downloadable.hardExpiration / MILLIS_IN_SECOND); + }, + + _onDownloadStarted: function(downloadable) + { + let subscription = Subscription.fromURL(downloadable.url); + FilterNotifier.triggerListeners("subscription.downloadStatus", subscription); + }, + + _onDownloadSuccess: function(downloadable, responseText, errorCallback, redirectCallback) + { + let lines = responseText.split(/[\r\n]+/); + let match = /\[Adblock(?:\s*Plus\s*([\d\.]+)?)?\]/i.exec(lines[0]); + if (!match) + return errorCallback("synchronize_invalid_data"); + let minVersion = match[1]; + + // Don't remove parameter comments immediately but add them to a list first, + // they need to be considered in the checksum calculation. + let remove = []; + let params = { + redirect: null, + homepage: null, + title: null, + version: null, + privatemode: null, + expires: null + }; + for (let i = 0; i < lines.length; i++) + { + let match = /^\s*!\s*(\w+)\s*:\s*(.*)/.exec(lines[i]); + if (match) + { + let keyword = match[1].toLowerCase(); + let value = match[2]; + if (keyword in params) + { + params[keyword] = value; + remove.push(i); + } + else if (keyword == "checksum") + { + lines.splice(i--, 1); + let checksum = Utils.generateChecksum(lines); + if (checksum && checksum != value.replace(/=+$/, "")) + return errorCallback("synchronize_checksum_mismatch"); + } + } + } + + if (params.redirect) + return redirectCallback(params.redirect); + + // Handle redirects + let subscription = Subscription.fromURL(downloadable.redirectURL || downloadable.url); + if (downloadable.redirectURL && downloadable.redirectURL != downloadable.url) + { + let oldSubscription = Subscription.fromURL(downloadable.url); + subscription.title = oldSubscription.title; + subscription.disabled = oldSubscription.disabled; + subscription.lastCheck = oldSubscription.lastCheck; + + let listed = (oldSubscription.url in FilterStorage.knownSubscriptions); + if (listed) + FilterStorage.removeSubscription(oldSubscription); + + delete Subscription.knownSubscriptions[oldSubscription.url]; + + if (listed) + FilterStorage.addSubscription(subscription); + } + + // The download actually succeeded + subscription.lastSuccess = subscription.lastDownload = Math.round(Date.now() / MILLIS_IN_SECOND); + subscription.downloadStatus = "synchronize_ok"; + subscription.errors = 0; + + // Remove lines containing parameters + for (let i = remove.length - 1; i >= 0; i--) + lines.splice(remove[i], 1); + + // Process parameters + if (params.homepage) + { + let uri = Utils.makeURI(params.homepage); + if (uri && (uri.scheme == "http" || uri.scheme == "https")) + subscription.homepage = uri.spec; + } + + if (params.privatemode) + { + subscription.privateMode = (params.privatemode == "true"); + } + + if (params.title) + { + subscription.title = params.title; + subscription.fixedTitle = true; + } + else + subscription.fixedTitle = false; + + subscription.version = (params.version ? parseInt(params.version, 10) : 0); + + let expirationInterval = DEFAULT_EXPIRATION_INTERVAL; + if (params.expires) + { + let match = /^(\d+)\s*(h)?/.exec(params.expires); + if (match) + { + let interval = parseInt(match[1], 10); + if (match[2]) + expirationInterval = interval * MILLIS_IN_HOUR; + else + expirationInterval = interval * MILLIS_IN_DAY; + } + } + + let [softExpiration, hardExpiration] = downloader.processExpirationInterval(expirationInterval); + subscription.softExpiration = Math.round(softExpiration / MILLIS_IN_SECOND); + subscription.expires = Math.round(hardExpiration / MILLIS_IN_SECOND); + + delete subscription.requiredVersion; + delete subscription.upgradeRequired; + if (minVersion) + { + let {addonVersion} = require("info"); + subscription.requiredVersion = minVersion; + if (Services.vc.compare(minVersion, addonVersion) > 0) + subscription.upgradeRequired = true; + } + + // Process filters + lines.shift(); + let filters = []; + for (let line of lines) + { + line = Filter.normalize(line); + if (line) + filters.push(Filter.fromText(line)); + } + + FilterStorage.updateSubscriptionFilters(subscription, filters); + + return undefined; + }, + + _onDownloadError: function(downloadable, downloadURL, error, channelStatus, responseStatus, redirectCallback) + { + let subscription = Subscription.fromURL(downloadable.url); + subscription.lastDownload = Math.round(Date.now() / MILLIS_IN_SECOND); + subscription.downloadStatus = error; + + // Request fallback URL if necessary - for automatic updates only + if (!downloadable.manual) + { + subscription.errors++; + + if (redirectCallback && subscription.errors >= Prefs.subscriptions_fallbackerrors && /^https?:\/\//i.test(subscription.url)) + { + subscription.errors = 0; + + let fallbackURL = Prefs.subscriptions_fallbackurl; + let {addonVersion} = require("info"); + fallbackURL = fallbackURL.replace(/%VERSION%/g, encodeURIComponent(addonVersion)); + fallbackURL = fallbackURL.replace(/%SUBSCRIPTION%/g, encodeURIComponent(subscription.url)); + fallbackURL = fallbackURL.replace(/%URL%/g, encodeURIComponent(downloadURL)); + fallbackURL = fallbackURL.replace(/%ERROR%/g, encodeURIComponent(error)); + fallbackURL = fallbackURL.replace(/%CHANNELSTATUS%/g, encodeURIComponent(channelStatus)); + fallbackURL = fallbackURL.replace(/%RESPONSESTATUS%/g, encodeURIComponent(responseStatus)); + + let request = new XMLHttpRequest(); + request.mozBackgroundRequest = true; + request.open("GET", fallbackURL); + request.overrideMimeType("text/plain"); + request.channel.loadFlags = request.channel.loadFlags | + request.channel.INHIBIT_CACHING | + request.channel.VALIDATE_ALWAYS; + request.addEventListener("load", function(ev) + { + if (onShutdown.done) + return; + + if (!(subscription.url in FilterStorage.knownSubscriptions)) + return; + + let match = /^(\d+)(?:\s+(\S+))?$/.exec(request.responseText); + if (match && match[1] == "301" && match[2] && /^https?:\/\//i.test(match[2])) // Moved permanently + redirectCallback(match[2]); + else if (match && match[1] == "410") // Gone + { + let data = "[Adblock]\n" + subscription.filters.map((f) => f.text).join("\n"); + redirectCallback("data:text/plain," + encodeURIComponent(data)); + } + }, false); + request.send(null); + } + } + }, +}; +Synchronizer.init(); diff --git a/data/extensions/spyblock@gnu.org/lib/timeline.js b/data/extensions/spyblock@gnu.org/lib/timeline.js new file mode 100644 index 0000000..18c10fb --- /dev/null +++ b/data/extensions/spyblock@gnu.org/lib/timeline.js @@ -0,0 +1,155 @@ +/* + * 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/>. + */ + +/** + * @fileOverview Debugging module used for load time measurements. + */ + +let nestingCounter = 0; +let firstTimeStamp = null; +let lastTimeStamp = null; + +let asyncActions = {__proto__: null}; + +/** + * Time logging module, used to measure startup time of Adblock Plus (development builds only). + * @class + */ +let TimeLine = exports.TimeLine = { + /** + * Logs an event to console together with the time it took to get there. + */ + log: function(/**String*/ message, /**Boolean*/ _forceDisplay) + { + if (!_forceDisplay && nestingCounter <= 0) + return; + + let now = Date.now(); + let diff = lastTimeStamp ? Math.round(now - lastTimeStamp) : "first event"; + lastTimeStamp = now; + + // Indent message depending on current nesting level + for (let i = 0; i < nestingCounter; i++) + message = "* " + message; + + // Pad message with spaces + let padding = []; + for (let i = message.toString().length; i < 80; i++) + padding.push(" "); + dump("[" + now + "] ABP timeline: " + message + padding.join("") + "\t (" + diff + ")\n"); + }, + + /** + * Called to indicate that application entered a block that needs to be timed. + */ + enter: function(/**String*/ message) + { + if (nestingCounter <= 0) + firstTimeStamp = Date.now(); + + this.log(message, true); + nestingCounter = (nestingCounter <= 0 ? 1 : nestingCounter + 1); + }, + + /** + * Called when application exited a block that TimeLine.enter() was called for. + * @param {String} message message to be logged + * @param {String} [asyncAction] identifier of a pending async action + */ + leave: function(message, asyncAction) + { + if (typeof asyncAction != "undefined") + message += " (async action pending)"; + + nestingCounter--; + this.log(message, true); + + if (nestingCounter <= 0) + { + if (firstTimeStamp !== null) + dump("ABP timeline: Total time elapsed: " + Math.round(Date.now() - firstTimeStamp) + "\n"); + firstTimeStamp = null; + lastTimeStamp = null; + } + + if (typeof asyncAction != "undefined") + { + if (asyncAction in asyncActions) + dump("ABP timeline: Warning: Async action " + asyncAction + " already executing\n"); + asyncActions[asyncAction] = {start: Date.now(), total: 0}; + } + }, + + /** + * Called when the application starts processing of an async action. + */ + asyncStart: function(/**String*/ asyncAction) + { + if (asyncAction in asyncActions) + { + let action = asyncActions[asyncAction]; + if ("currentStart" in action) + dump("ABP timeline: Warning: Processing reentered for async action " + asyncAction + "\n"); + action.currentStart = Date.now(); + } + else + dump("ABP timeline: Warning: Async action " + asyncAction + " is unknown\n"); + }, + + /** + * Called when the application finishes processing of an async action. + */ + asyncEnd: function(/**String*/ asyncAction) + { + if (asyncAction in asyncActions) + { + let action = asyncActions[asyncAction]; + if ("currentStart" in action) + { + action.total += Date.now() - action.currentStart; + delete action.currentStart; + } + else + dump("ABP timeline: Warning: Processing not entered for async action " + asyncAction + "\n"); + } + else + dump("ABP timeline: Warning: Async action " + asyncAction + " is unknown\n"); + }, + + /** + * Called when an async action is done and its time can be logged. + */ + asyncDone: function(/**String*/ asyncAction) + { + if (asyncAction in asyncActions) + { + let action = asyncActions[asyncAction]; + let now = Date.now(); + let diff = now - action.start; + if ("currentStart" in action) + dump("ABP timeline: Warning: Still processing for async action " + asyncAction + "\n"); + + let message = "Async action " + asyncAction + " done"; + let padding = []; + for (let i = message.toString().length; i < 80; i++) + padding.push(" "); + dump("[" + now + "] ABP timeline: " + message + padding.join("") + "\t (" + action.total + "/" + diff + ")\n"); + } + else + dump("ABP timeline: Warning: Async action " + asyncAction + " is unknown\n"); + } +}; 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); +}); diff --git a/data/extensions/spyblock@gnu.org/lib/utils.js b/data/extensions/spyblock@gnu.org/lib/utils.js new file mode 100644 index 0000000..8fbdf3e --- /dev/null +++ b/data/extensions/spyblock@gnu.org/lib/utils.js @@ -0,0 +1,787 @@ +/* + * 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/>. + */ + +/** + * @fileOverview Module containing a bunch of utility functions. + */ + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +let sidebarParams = null; + +/** + * Provides a bunch of utility functions. + * @class + */ +let Utils = exports.Utils = +{ + /** + * Returns the add-on ID used by Adblock Plus + */ + get addonID() + { + let {addonID} = require("info"); + return addonID; + }, + + /** + * Returns the installed Adblock Plus version + */ + get addonVersion() + { + let {addonVersion} = require("info"); + return addonVersion; + }, + + /** + * Returns whether we are running in Fennec, for Fennec-specific hacks + * @type Boolean + */ + get isFennec() + { + let {application} = require("info"); + let result = (application == "fennec" || application == "fennec2"); + Utils.__defineGetter__("isFennec", () => result); + return result; + }, + + /** + * Returns the user interface locale selected for adblockplus chrome package. + */ + get appLocale() + { + let locale = "en-US"; + try + { + locale = Utils.chromeRegistry.getSelectedLocale("adblockplus"); + } + catch (e) + { + Cu.reportError(e); + } + Utils.__defineGetter__("appLocale", () => locale); + return Utils.appLocale; + }, + + /** + * Returns version of the Gecko platform + */ + get platformVersion() + { + let platformVersion = Services.appinfo.platformVersion; + Utils.__defineGetter__("platformVersion", () => platformVersion); + return Utils.platformVersion; + }, + + /** + * Retrieves a string from global.properties string bundle, will throw if string isn't found. + * + * @param {String} name string name + * @return {String} + */ + getString: function(name) + { + // Randomize URI to work around bug 719376 + let stringBundle = Services.strings.createBundle("chrome://adblockplus/locale/global.properties?" + Math.random()); + Utils.getString = function(name) + { + return stringBundle.GetStringFromName(name); + } + return Utils.getString(name); + }, + + /** + * Shows an alert message like window.alert() but with a custom title. + * + * @param {Window} parentWindow parent window of the dialog (can be null) + * @param {String} message message to be displayed + * @param {String} [title] dialog title, default title will be used if omitted + */ + alert: function(parentWindow, message, title) + { + if (!title) + title = Utils.getString("default_dialog_title"); + Utils.promptService.alert(parentWindow, title, message); + }, + + /** + * Asks the user for a confirmation like window.confirm() but with a custom title. + * + * @param {Window} parentWindow parent window of the dialog (can be null) + * @param {String} message message to be displayed + * @param {String} [title] dialog title, default title will be used if omitted + * @return {Bool} + */ + confirm: function(parentWindow, message, title) + { + if (!title) + title = Utils.getString("default_dialog_title"); + return Utils.promptService.confirm(parentWindow, title, message); + }, + + /** + * Retrieves the window for a document node. + * @return {Window} will be null if the node isn't associated with a window + */ + getWindow: function(/**Node*/ node) + { + if ("ownerDocument" in node && node.ownerDocument) + node = node.ownerDocument; + + if ("defaultView" in node) + return node.defaultView; + + return null; + }, + + /** + * Retrieves the top-level chrome window for a content window. + */ + getChromeWindow: function(/**Window*/ window) /**Window*/ + { + return window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShellTreeItem) + .rootTreeItem + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow); + }, + + /** + * If the window doesn't have its own security context (e.g. about:blank or + * data: URL) walks up the parent chain until a window is found that has a + * security context. + */ + getOriginWindow: function(/**Window*/ wnd) /**Window*/ + { + while (wnd != wnd.parent) + { + let uri = Utils.makeURI(wnd.location.href); + if (uri.spec != "about:blank" && uri.spec != "moz-safe-about:blank" && + !Utils.netUtils.URIChainHasFlags(uri, Ci.nsIProtocolHandler.URI_INHERITS_SECURITY_CONTEXT)) + { + break; + } + wnd = wnd.parent; + } + return wnd; + }, + + /** + * If a protocol using nested URIs like jar: is used - retrieves innermost + * nested URI. + */ + unwrapURL: function(/**nsIURI or String*/ url) /**nsIURI*/ + { + if (!(url instanceof Ci.nsIURI)) + url = Utils.makeURI(url); + + if (url instanceof Ci.nsINestedURI) + return url.innermostURI; + else + return url; + }, + + /** + * Translates a string URI into its nsIURI representation, will return null for + * invalid URIs. + */ + makeURI: function(/**String*/ url) /**nsIURI*/ + { + try + { + return Utils.ioService.newURI(url, null, null); + } + catch (e) { + return null; + } + }, + + /** + * Posts an action to the event queue of the current thread to run it + * asynchronously. Any additional parameters to this function are passed + * as parameters to the callback. + */ + runAsync: function(/**Function*/ callback, /**Object*/ thisPtr) + { + let params = Array.prototype.slice.call(arguments, 2); + let runnable = { + run: function() + { + callback.apply(thisPtr, params); + } + }; + Services.tm.currentThread.dispatch(runnable, Ci.nsIEventTarget.DISPATCH_NORMAL); + }, + + /** + * Gets the DOM window associated with a particular request (if any). + */ + getRequestWindow: function(/**nsIChannel*/ channel) /**nsIDOMWindow*/ + { + try + { + if (channel.notificationCallbacks) + return channel.notificationCallbacks.getInterface(Ci.nsILoadContext).associatedWindow; + } catch(e) {} + + try + { + if (channel.loadGroup && channel.loadGroup.notificationCallbacks) + return channel.loadGroup.notificationCallbacks.getInterface(Ci.nsILoadContext).associatedWindow; + } catch(e) {} + + return null; + }, + + /** + * Generates filter subscription checksum. + * + * @param {Array of String} lines filter subscription lines (with checksum line removed) + * @return {String} checksum or null + */ + generateChecksum: function(lines) + { + let stream = null; + try + { + // Checksum is an MD5 checksum (base64-encoded without the trailing "=") of + // all lines in UTF-8 without the checksum line, joined with "\n". + + let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].createInstance(Ci.nsIScriptableUnicodeConverter); + converter.charset = "UTF-8"; + stream = converter.convertToInputStream(lines.join("\n")); + + let hashEngine = Cc["@mozilla.org/security/hash;1"].createInstance(Ci.nsICryptoHash); + hashEngine.init(hashEngine.MD5); + hashEngine.updateFromStream(stream, stream.available()); + return hashEngine.finish(true).replace(/=+$/, ""); + } + catch (e) + { + return null; + } + finally + { + if (stream) + stream.close(); + } + }, + + /** + * Formats a unix time according to user's locale. + * @param {Integer} time unix time in milliseconds + * @return {String} formatted date and time + */ + formatTime: function(time) + { + try + { + let date = new Date(time); + return Utils.dateFormatter.FormatDateTime("", Ci.nsIScriptableDateFormat.dateFormatShort, + Ci.nsIScriptableDateFormat.timeFormatNoSeconds, + date.getFullYear(), date.getMonth() + 1, date.getDate(), + date.getHours(), date.getMinutes(), date.getSeconds()); + } + catch(e) + { + // Make sure to return even on errors + Cu.reportError(e); + return ""; + } + }, + + /** + * Checks whether any of the prefixes listed match the application locale, + * returns matching prefix if any. + */ + checkLocalePrefixMatch: function(/**String*/ prefixes) /**String*/ + { + if (!prefixes) + return null; + + let appLocale = Utils.appLocale; + for (let prefix of prefixes.split(/,/)) + if (new RegExp("^" + prefix + "\\b").test(appLocale)) + return prefix; + + return null; + }, + + /** + * Chooses the best filter subscription for user's language. + */ + chooseFilterSubscription: function(/**NodeList*/ subscriptions) /**Node*/ + { + let selectedItem = null; + let selectedPrefix = null; + let matchCount = 0; + for (let i = 0; i < subscriptions.length; i++) + { + let subscription = subscriptions[i]; + if (!selectedItem) + selectedItem = subscription; + + let prefix = Utils.checkLocalePrefixMatch(subscription.getAttribute("prefixes")); + if (prefix) + { + if (!selectedPrefix || selectedPrefix.length < prefix.length) + { + selectedItem = subscription; + selectedPrefix = prefix; + matchCount = 1; + } + else if (selectedPrefix && selectedPrefix.length == prefix.length) + { + matchCount++; + + // If multiple items have a matching prefix of the same length: + // Select one of the items randomly, probability should be the same + // for all items. So we replace the previous match here with + // probability 1/N (N being the number of matches). + if (Math.random() * matchCount < 1) + { + selectedItem = subscription; + selectedPrefix = prefix; + } + } + } + } + return selectedItem; + }, + + /** + * Pauses code execution and allows events to be processed. Warning: + * other extension code might execute, the extension might even shut down. + */ + yield: function() + { + let {Prefs} = require("prefs"); + if (Prefs.please_kill_startup_performance) + { + this.yield = function() {}; + return; + } + let thread = Services.tm.currentThread; + while (thread.processNextEvent(false)); + }, + + /** + * Saves sidebar state before detaching/reattaching + */ + setParams: function(params) + { + sidebarParams = params; + }, + + /** + * Retrieves and removes sidebar state after detaching/reattaching + */ + getParams: function() + { + let ret = sidebarParams; + sidebarParams = null; + return ret; + }, + + /** + * Verifies RSA signature. The public key and signature should be base64-encoded. + */ + verifySignature: function(/**String*/ key, /**String*/ signature, /**String*/ data) /**Boolean*/ + { + if (!Utils.crypto) + return false; + + // Maybe we did the same check recently, look it up in the cache + if (!("_cache" in Utils.verifySignature)) + Utils.verifySignature._cache = new Cache(5); + let cache = Utils.verifySignature._cache; + let cacheKey = key + " " + signature + " " + data; + if (cacheKey in cache.data) + return cache.data[cacheKey]; + else + cache.add(cacheKey, false); + + let keyInfo, pubKey, context; + try + { + let keyItem = Utils.crypto.getSECItem(atob(key)); + keyInfo = Utils.crypto.SECKEY_DecodeDERSubjectPublicKeyInfo(keyItem.address()); + if (keyInfo.isNull()) + throw new Error("SECKEY_DecodeDERSubjectPublicKeyInfo failed"); + + pubKey = Utils.crypto.SECKEY_ExtractPublicKey(keyInfo); + if (pubKey.isNull()) + throw new Error("SECKEY_ExtractPublicKey failed"); + + let signatureItem = Utils.crypto.getSECItem(atob(signature)); + + context = Utils.crypto.VFY_CreateContext(pubKey, signatureItem.address(), Utils.crypto.SEC_OID_ISO_SHA_WITH_RSA_SIGNATURE, null); + if (context.isNull()) + return false; // This could happen if the signature is invalid + + let error = Utils.crypto.VFY_Begin(context); + if (error < 0) + throw new Error("VFY_Begin failed"); + + error = Utils.crypto.VFY_Update(context, data, data.length); + if (error < 0) + throw new Error("VFY_Update failed"); + + error = Utils.crypto.VFY_End(context); + if (error < 0) + return false; + + cache.data[cacheKey] = true; + return true; + } + catch (e) + { + Cu.reportError(e); + return false; + } + finally + { + if (keyInfo && !keyInfo.isNull()) + Utils.crypto.SECKEY_DestroySubjectPublicKeyInfo(keyInfo); + if (pubKey && !pubKey.isNull()) + Utils.crypto.SECKEY_DestroyPublicKey(pubKey); + if (context && !context.isNull()) + Utils.crypto.VFY_DestroyContext(context, true); + } + }, + + /** + * Returns the documentation link from the preferences. + */ + getDocLink: function(/**String*/ linkID) + { + let {Prefs} = require("prefs"); + let docLink = Prefs.documentation_link; + return docLink.replace(/%LINK%/g, linkID).replace(/%LANG%/g, Utils.appLocale); + }, + + /** + * Splits up a combined label into the label and access key components. + * + * @return {Array} An array with two strings: label and access key + */ + splitLabel: function(/**String*/ label) + { + let match = /^(.*)\s*\(&(.)\)\s*(\u2026?)$/.exec(label); + if (match) + { + // Access key not part of the label + return [match[1] + match[3], match[2]]; + } + else + { + // Access key part of the label + let pos = label.indexOf("&"); + if (pos >= 0 && pos < label.length - 1) + return [label.substr(0, pos) + label.substr(pos + 1), label[pos + 1]]; + else + return [label, ""]; + } + }, + + /** + * Split all labels starting from a particular DOM node. + */ + splitAllLabels: function(/**DOMNode*/ root) + { + let attrMap = { + __proto__: null, + "label": "value", + "setting": "title" + }; + + let elements = root.querySelectorAll("*[label], label[value], setting[title]"); + for (let i = 0; i < elements.length; i++) + { + let element = elements[i]; + let attr = (element.localName in attrMap ? attrMap[element.localName] : "label"); + let origLabel = element.getAttribute(attr); + + let [label, accesskey] = this.splitLabel(origLabel); + if (label != origLabel) + element.setAttribute(attr, label); + if (accesskey != "") + element.setAttribute("accesskey", accesskey); + + // Labels forward changes of the accessKey property to their control, only + // set it for actual controls. + if (element.localName != "label") + element.accessKey = accesskey; + } + } +}; + +/** + * A cache with a fixed capacity, newer entries replace entries that have been + * stored first. + * @constructor + */ +function Cache(/**Integer*/ size) +{ + this._ringBuffer = new Array(size); + this.data = {__proto__: null}; +} +exports.Cache = Cache; + +Cache.prototype = +{ + /** + * Ring buffer storing hash keys, allows determining which keys need to be + * evicted. + * @type Array + */ + _ringBuffer: null, + + /** + * Index in the ring buffer to be written next. + * @type Integer + */ + _bufferIndex: 0, + + /** + * Cache data, maps values to the keys. Read-only access, for writing use + * add() method. + * @type Object + */ + data: null, + + /** + * Adds a key and the corresponding value to the cache. + */ + add: function(/**String*/ key, value) + { + if (!(key in this.data)) + { + // This is a new key - we need to add it to the ring buffer and evict + // another entry instead. + let oldKey = this._ringBuffer[this._bufferIndex]; + if (typeof oldKey != "undefined") + delete this.data[oldKey]; + this._ringBuffer[this._bufferIndex] = key; + + this._bufferIndex++; + if (this._bufferIndex >= this._ringBuffer.length) + this._bufferIndex = 0; + } + + this.data[key] = value; + }, + + /** + * Clears cache contents. + */ + clear: function() + { + this._ringBuffer = new Array(this._ringBuffer.length); + this.data = {__proto__: null}; + } +} + +// Getters for common services, this should be replaced by Services.jsm in future + +XPCOMUtils.defineLazyServiceGetter(Utils, "categoryManager", "@mozilla.org/categorymanager;1", "nsICategoryManager"); +XPCOMUtils.defineLazyServiceGetter(Utils, "ioService", "@mozilla.org/network/io-service;1", "nsIIOService"); +XPCOMUtils.defineLazyServiceGetter(Utils, "promptService", "@mozilla.org/embedcomp/prompt-service;1", "nsIPromptService"); +XPCOMUtils.defineLazyServiceGetter(Utils, "effectiveTLD", "@mozilla.org/network/effective-tld-service;1", "nsIEffectiveTLDService"); +XPCOMUtils.defineLazyServiceGetter(Utils, "netUtils", "@mozilla.org/network/util;1", "nsINetUtil"); +XPCOMUtils.defineLazyServiceGetter(Utils, "styleService", "@mozilla.org/content/style-sheet-service;1", "nsIStyleSheetService"); +XPCOMUtils.defineLazyServiceGetter(Utils, "prefService", "@mozilla.org/preferences-service;1", "nsIPrefService"); +XPCOMUtils.defineLazyServiceGetter(Utils, "versionComparator", "@mozilla.org/xpcom/version-comparator;1", "nsIVersionComparator"); +XPCOMUtils.defineLazyServiceGetter(Utils, "windowMediator", "@mozilla.org/appshell/window-mediator;1", "nsIWindowMediator"); +XPCOMUtils.defineLazyServiceGetter(Utils, "windowWatcher", "@mozilla.org/embedcomp/window-watcher;1", "nsIWindowWatcher"); +XPCOMUtils.defineLazyServiceGetter(Utils, "chromeRegistry", "@mozilla.org/chrome/chrome-registry;1", "nsIXULChromeRegistry"); +XPCOMUtils.defineLazyServiceGetter(Utils, "systemPrincipal", "@mozilla.org/systemprincipal;1", "nsIPrincipal"); +XPCOMUtils.defineLazyServiceGetter(Utils, "dateFormatter", "@mozilla.org/intl/scriptabledateformat;1", "nsIScriptableDateFormat"); +XPCOMUtils.defineLazyServiceGetter(Utils, "childMessageManager", "@mozilla.org/childprocessmessagemanager;1", "nsISyncMessageSender"); +XPCOMUtils.defineLazyServiceGetter(Utils, "parentMessageManager", "@mozilla.org/parentprocessmessagemanager;1", "nsIFrameMessageManager"); +XPCOMUtils.defineLazyServiceGetter(Utils, "httpProtocol", "@mozilla.org/network/protocol;1?name=http", "nsIHttpProtocolHandler"); +XPCOMUtils.defineLazyServiceGetter(Utils, "clipboard", "@mozilla.org/widget/clipboard;1", "nsIClipboard"); +XPCOMUtils.defineLazyServiceGetter(Utils, "clipboardHelper", "@mozilla.org/widget/clipboardhelper;1", "nsIClipboardHelper"); +XPCOMUtils.defineLazyGetter(Utils, "crypto", function() +{ + try + { + let ctypes = Components.utils.import("resource://gre/modules/ctypes.jsm", null).ctypes; + + let nsslib; + try + { + nsslib = ctypes.open(ctypes.libraryName("nss3")); + } + catch (e) + { + // It seems that on Mac OS X the full path name needs to be specified + let file = Services.dirsvc.get("GreD", Ci.nsILocalFile); + file.append(ctypes.libraryName("nss3")); + nsslib = ctypes.open(file.path); + } + + let result = {}; + + // seccomon.h + result.siUTF8String = 14; + + // secoidt.h + result.SEC_OID_ISO_SHA_WITH_RSA_SIGNATURE = 15; + + // The following types are opaque to us + result.VFYContext = ctypes.void_t; + result.SECKEYPublicKey = ctypes.void_t; + result.CERTSubjectPublicKeyInfo = ctypes.void_t; + + /* + * seccomon.h + * struct SECItemStr { + * SECItemType type; + * unsigned char *data; + * unsigned int len; + * }; + */ + result.SECItem = ctypes.StructType("SECItem", [ + {type: ctypes.int}, + {data: ctypes.unsigned_char.ptr}, + {len: ctypes.int} + ]); + + /* + * cryptohi.h + * extern VFYContext *VFY_CreateContext(SECKEYPublicKey *key, SECItem *sig, + * SECOidTag sigAlg, void *wincx); + */ + result.VFY_CreateContext = nsslib.declare( + "VFY_CreateContext", + ctypes.default_abi, result.VFYContext.ptr, + result.SECKEYPublicKey.ptr, + result.SECItem.ptr, + ctypes.int, + ctypes.voidptr_t + ); + + /* + * cryptohi.h + * extern void VFY_DestroyContext(VFYContext *cx, PRBool freeit); + */ + result.VFY_DestroyContext = nsslib.declare( + "VFY_DestroyContext", + ctypes.default_abi, ctypes.void_t, + result.VFYContext.ptr, + ctypes.bool + ); + + /* + * cryptohi.h + * extern SECStatus VFY_Begin(VFYContext *cx); + */ + result.VFY_Begin = nsslib.declare("VFY_Begin", + ctypes.default_abi, ctypes.int, + result.VFYContext.ptr + ); + + /* + * cryptohi.h + * extern SECStatus VFY_Update(VFYContext *cx, const unsigned char *input, + * unsigned int inputLen); + */ + result.VFY_Update = nsslib.declare( + "VFY_Update", + ctypes.default_abi, ctypes.int, + result.VFYContext.ptr, + ctypes.unsigned_char.ptr, + ctypes.int + ); + + /* + * cryptohi.h + * extern SECStatus VFY_End(VFYContext *cx); + */ + result.VFY_End = nsslib.declare( + "VFY_End", + ctypes.default_abi, ctypes.int, + result.VFYContext.ptr + ); + + /* + * keyhi.h + * extern CERTSubjectPublicKeyInfo * + * SECKEY_DecodeDERSubjectPublicKeyInfo(SECItem *spkider); + */ + result.SECKEY_DecodeDERSubjectPublicKeyInfo = nsslib.declare( + "SECKEY_DecodeDERSubjectPublicKeyInfo", + ctypes.default_abi, result.CERTSubjectPublicKeyInfo.ptr, + result.SECItem.ptr + ); + + /* + * keyhi.h + * extern void SECKEY_DestroySubjectPublicKeyInfo(CERTSubjectPublicKeyInfo *spki); + */ + result.SECKEY_DestroySubjectPublicKeyInfo = nsslib.declare( + "SECKEY_DestroySubjectPublicKeyInfo", + ctypes.default_abi, ctypes.void_t, + result.CERTSubjectPublicKeyInfo.ptr + ); + + /* + * keyhi.h + * extern SECKEYPublicKey * + * SECKEY_ExtractPublicKey(CERTSubjectPublicKeyInfo *); + */ + result.SECKEY_ExtractPublicKey = nsslib.declare( + "SECKEY_ExtractPublicKey", + ctypes.default_abi, result.SECKEYPublicKey.ptr, + result.CERTSubjectPublicKeyInfo.ptr + ); + + /* + * keyhi.h + * extern void SECKEY_DestroyPublicKey(SECKEYPublicKey *key); + */ + result.SECKEY_DestroyPublicKey = nsslib.declare( + "SECKEY_DestroyPublicKey", + ctypes.default_abi, ctypes.void_t, + result.SECKEYPublicKey.ptr + ); + + // Convenience method + result.getSECItem = function(data) + { + var dataArray = new ctypes.ArrayType(ctypes.unsigned_char, data.length)(); + for (let i = 0; i < data.length; i++) + dataArray[i] = data.charCodeAt(i) % 256; + return new result.SECItem(result.siUTF8String, dataArray, dataArray.length); + }; + + return result; + } + catch (e) + { + Cu.reportError(e); + // Expected, ctypes isn't supported in Gecko 1.9.2 + return null; + } +}); + +if ("@mozilla.org/messenger/headerparser;1" in Cc) + XPCOMUtils.defineLazyServiceGetter(Utils, "headerParser", "@mozilla.org/messenger/headerparser;1", "nsIMsgHeaderParser"); +else + Utils.headerParser = null; diff --git a/data/extensions/spyblock@gnu.org/lib/windowObserver.js b/data/extensions/spyblock@gnu.org/lib/windowObserver.js new file mode 100644 index 0000000..eb0b13a --- /dev/null +++ b/data/extensions/spyblock@gnu.org/lib/windowObserver.js @@ -0,0 +1,112 @@ +/* + * This file is part of the Adblock Plus build tools, + * 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/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +exports.WindowObserver = WindowObserver; + +/** + * This class will call listener's method applyToWindow() for all new chrome + * windows being opened. It will also call listener's method removeFromWindow() + * for all windows still open when the extension is shut down. + * @param {Object} listener + * @param {String} [when] when to execute applyToWindow(). "start" means immediately + * when the window opens, "ready" when its contents are available + * and "end" (default) means to wait until the "load" event. + * @constructor + */ +function WindowObserver(listener, when) +{ + this._listener = listener; + this._when = when; + + let windows = []; + let e = Services.wm.getZOrderDOMWindowEnumerator(null, true); + while (e.hasMoreElements()) + windows.push(e.getNext()); + + // Check if there are any windows that we missed + let eAll = Services.ww.getWindowEnumerator(); + while (eAll.hasMoreElements()) + { + let element = eAll.getNext(); + if (windows.indexOf(element) < 0) + windows.push(element); + } + + for (let i = 0; i < windows.length; i++) + { + let window = windows[i].QueryInterface(Ci.nsIDOMWindow); + if (when == "start" || window.document.readyState == "complete") + this._listener.applyToWindow(window); + else + this.observe(window, "chrome-document-global-created", null); + } + + Services.obs.addObserver(this, "chrome-document-global-created", true); + + this._shutdownHandler = function() + { + let e = Services.ww.getWindowEnumerator(); + while (e.hasMoreElements()) + this._listener.removeFromWindow(e.getNext().QueryInterface(Ci.nsIDOMWindow)); + + Services.obs.removeObserver(this, "chrome-document-global-created"); + }.bind(this); + onShutdown.add(this._shutdownHandler); +} +WindowObserver.prototype = +{ + _listener: null, + _when: null, + _shutdownHandler: null, + + shutdown: function() + { + if (!this._shutdownHandler) + return; + + onShutdown.remove(this._shutdownHandler); + this._shutdownHandler(); + this._shutdownHandler = null; + }, + + observe: function(subject, topic, data) + { + if (topic == "chrome-document-global-created") + { + let window = subject.QueryInterface(Ci.nsIDOMWindow); + if (this._when == "start") + { + this._listener.applyToWindow(window); + return; + } + + let event = (this._when == "ready" ? "DOMContentLoaded" : "load"); + let listener = function() + { + window.removeEventListener(event, listener, false); + if (this._shutdownHandler) + this._listener.applyToWindow(window); + }.bind(this); + window.addEventListener(event, listener, false); + } + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsISupportsWeakReference, Ci.nsIObserver]) +}; |