summaryrefslogtreecommitdiff
path: root/data/extensions/spyblock@gnu.org/lib
diff options
context:
space:
mode:
Diffstat (limited to 'data/extensions/spyblock@gnu.org/lib')
-rw-r--r--data/extensions/spyblock@gnu.org/lib/Public.jsm202
-rw-r--r--data/extensions/spyblock@gnu.org/lib/antiadblockInit.js78
-rw-r--r--data/extensions/spyblock@gnu.org/lib/appSupport.js948
-rw-r--r--data/extensions/spyblock@gnu.org/lib/contentPolicy.js779
-rw-r--r--data/extensions/spyblock@gnu.org/lib/customizableUI.js320
-rw-r--r--data/extensions/spyblock@gnu.org/lib/downloader.js381
-rw-r--r--data/extensions/spyblock@gnu.org/lib/elemHide.js419
-rw-r--r--data/extensions/spyblock@gnu.org/lib/elemHideHitRegistration.js160
-rw-r--r--data/extensions/spyblock@gnu.org/lib/filterClasses.js906
-rw-r--r--data/extensions/spyblock@gnu.org/lib/filterListener.js282
-rw-r--r--data/extensions/spyblock@gnu.org/lib/filterNotifier.js73
-rw-r--r--data/extensions/spyblock@gnu.org/lib/filterStorage.js897
-rw-r--r--data/extensions/spyblock@gnu.org/lib/io.js365
-rw-r--r--data/extensions/spyblock@gnu.org/lib/keySelector.js228
-rw-r--r--data/extensions/spyblock@gnu.org/lib/main.js73
-rw-r--r--data/extensions/spyblock@gnu.org/lib/matcher.js446
-rw-r--r--data/extensions/spyblock@gnu.org/lib/notification.js339
-rw-r--r--data/extensions/spyblock@gnu.org/lib/objectTabs.js492
-rw-r--r--data/extensions/spyblock@gnu.org/lib/prefs.js203
-rw-r--r--data/extensions/spyblock@gnu.org/lib/requestNotifier.js378
-rw-r--r--data/extensions/spyblock@gnu.org/lib/subscriptionClasses.js597
-rw-r--r--data/extensions/spyblock@gnu.org/lib/sync.js459
-rw-r--r--data/extensions/spyblock@gnu.org/lib/synchronizer.js330
-rw-r--r--data/extensions/spyblock@gnu.org/lib/timeline.js155
-rw-r--r--data/extensions/spyblock@gnu.org/lib/ui.js1973
-rw-r--r--data/extensions/spyblock@gnu.org/lib/utils.js787
-rw-r--r--data/extensions/spyblock@gnu.org/lib/windowObserver.js112
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])
+};