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.jsm2
-rw-r--r--data/extensions/spyblock@gnu.org/lib/antiadblockInit.js59
-rw-r--r--data/extensions/spyblock@gnu.org/lib/appSupport.js121
-rw-r--r--data/extensions/spyblock@gnu.org/lib/child/bootstrap.js97
-rw-r--r--data/extensions/spyblock@gnu.org/lib/child/contentPolicy.js518
-rw-r--r--data/extensions/spyblock@gnu.org/lib/child/contextMenu.js137
-rw-r--r--data/extensions/spyblock@gnu.org/lib/child/dataCollector.js108
-rw-r--r--data/extensions/spyblock@gnu.org/lib/child/elemHide.js403
-rw-r--r--data/extensions/spyblock@gnu.org/lib/child/elemHideEmulation.js118
-rw-r--r--data/extensions/spyblock@gnu.org/lib/child/flasher.js99
-rw-r--r--data/extensions/spyblock@gnu.org/lib/child/main.js31
-rw-r--r--data/extensions/spyblock@gnu.org/lib/child/objectTabs.js405
-rw-r--r--data/extensions/spyblock@gnu.org/lib/child/requestNotifier.js444
-rw-r--r--data/extensions/spyblock@gnu.org/lib/child/subscribeLinks.js118
-rw-r--r--data/extensions/spyblock@gnu.org/lib/child/utils.js141
-rw-r--r--data/extensions/spyblock@gnu.org/lib/common.js53
-rw-r--r--data/extensions/spyblock@gnu.org/lib/contentPolicy.js772
-rw-r--r--data/extensions/spyblock@gnu.org/lib/coreUtils.js36
-rw-r--r--data/extensions/spyblock@gnu.org/lib/customizableUI.js4
-rw-r--r--data/extensions/spyblock@gnu.org/lib/downloader.js165
-rw-r--r--data/extensions/spyblock@gnu.org/lib/elemHide.js472
-rw-r--r--data/extensions/spyblock@gnu.org/lib/elemHideEmulation.js81
-rw-r--r--data/extensions/spyblock@gnu.org/lib/elemHideFF.js106
-rw-r--r--data/extensions/spyblock@gnu.org/lib/elemHideHitRegistration.js164
-rw-r--r--data/extensions/spyblock@gnu.org/lib/events.js106
-rw-r--r--data/extensions/spyblock@gnu.org/lib/ext_background.js57
-rw-r--r--data/extensions/spyblock@gnu.org/lib/ext_common.js187
-rw-r--r--data/extensions/spyblock@gnu.org/lib/filterClasses.js573
-rw-r--r--data/extensions/spyblock@gnu.org/lib/filterListener.js306
-rw-r--r--data/extensions/spyblock@gnu.org/lib/filterNotifier.js55
-rw-r--r--data/extensions/spyblock@gnu.org/lib/filterStorage.js734
-rw-r--r--data/extensions/spyblock@gnu.org/lib/io.js465
-rw-r--r--data/extensions/spyblock@gnu.org/lib/keySelector.js40
-rw-r--r--data/extensions/spyblock@gnu.org/lib/legacyIO.js335
-rw-r--r--data/extensions/spyblock@gnu.org/lib/main.js23
-rw-r--r--data/extensions/spyblock@gnu.org/lib/matcher.js173
-rw-r--r--data/extensions/spyblock@gnu.org/lib/messageResponder.js515
-rw-r--r--data/extensions/spyblock@gnu.org/lib/messaging.js316
-rw-r--r--data/extensions/spyblock@gnu.org/lib/notification.js294
-rw-r--r--data/extensions/spyblock@gnu.org/lib/objectTabs.js524
-rw-r--r--data/extensions/spyblock@gnu.org/lib/prefs.js115
-rw-r--r--data/extensions/spyblock@gnu.org/lib/requestNotifier.js400
-rw-r--r--data/extensions/spyblock@gnu.org/lib/subscriptionClasses.js265
-rw-r--r--data/extensions/spyblock@gnu.org/lib/sync.js2
-rw-r--r--data/extensions/spyblock@gnu.org/lib/synchronizer.js168
-rw-r--r--data/extensions/spyblock@gnu.org/lib/ui.js494
-rw-r--r--data/extensions/spyblock@gnu.org/lib/utils.js59
-rw-r--r--data/extensions/spyblock@gnu.org/lib/whitelisting.js46
48 files changed, 6887 insertions, 4019 deletions
diff --git a/data/extensions/spyblock@gnu.org/lib/Public.jsm b/data/extensions/spyblock@gnu.org/lib/Public.jsm
index ddd2389..ecd7e95 100644
--- a/data/extensions/spyblock@gnu.org/lib/Public.jsm
+++ b/data/extensions/spyblock@gnu.org/lib/Public.jsm
@@ -1,6 +1,6 @@
/*
* This file is part of Adblock Plus <https://adblockplus.org/>,
- * Copyright (C) 2006-2015 Eyeo GmbH
+ * Copyright (C) 2006-2017 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
diff --git a/data/extensions/spyblock@gnu.org/lib/antiadblockInit.js b/data/extensions/spyblock@gnu.org/lib/antiadblockInit.js
index d4ef326..c5b845f 100644
--- a/data/extensions/spyblock@gnu.org/lib/antiadblockInit.js
+++ b/data/extensions/spyblock@gnu.org/lib/antiadblockInit.js
@@ -1,6 +1,6 @@
/*
* This file is part of Adblock Plus <https://adblockplus.org/>,
- * Copyright (C) 2006-2015 Eyeo GmbH
+ * Copyright (C) 2006-2017 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
@@ -15,23 +15,28 @@
* along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>.
*/
-Cu.import("resource://gre/modules/Services.jsm");
+"use strict";
-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");
+const {Prefs} = require("prefs");
+const {ActiveFilter} = require("filterClasses");
+const {FilterStorage} = require("filterStorage");
+const {FilterNotifier} = require("filterNotifier");
+const {Subscription} = require("subscriptionClasses");
+const {Notification} = require("notification");
+
+let ext;
+if (typeof window != "undefined" && window.ext)
+ ({ext} = window);
+else
+ ext = require("ext_background");
exports.initAntiAdblockNotification = function initAntiAdblockNotification()
{
let notification = {
id: "antiadblock",
type: "question",
- title: Utils.getString("notification_antiadblock_title"),
- message: Utils.getString("notification_antiadblock_message"),
+ title: ext.i18n.getMessage("notification_antiadblock_title"),
+ message: ext.i18n.getMessage("notification_antiadblock_message"),
urlFilters: []
};
@@ -51,8 +56,9 @@ exports.initAntiAdblockNotification = function initAntiAdblockNotification()
{
for (let domain in filter.domains)
{
- let urlFilter = "||" + domain + "^";
- if (domain && filter.domains[domain] && urlFilters.indexOf(urlFilter) == -1)
+ let urlFilter = "||" + domain + "^$document";
+ if (domain && filter.domains[domain] &&
+ urlFilters.indexOf(urlFilter) == -1)
urlFilters.push(urlFilter);
}
}
@@ -68,18 +74,25 @@ exports.initAntiAdblockNotification = function initAntiAdblockNotification()
Notification.removeQuestionListener(notification.id, notificationListener);
}
- let subscription = Subscription.fromURL(Prefs.subscriptions_antiadblockurl);
- if (subscription.lastDownload && subscription.disabled)
- addAntiAdblockNotification(subscription);
+ let antiAdblockSubscription = Subscription.fromURL(
+ Prefs.subscriptions_antiadblockurl
+ );
+ if (antiAdblockSubscription.lastDownload && antiAdblockSubscription.disabled)
+ addAntiAdblockNotification(antiAdblockSubscription);
- FilterNotifier.addListener(function(action, value, newItem, oldItem)
+ function onSubscriptionChange(subscription)
{
- if (!/^subscription\.(updated|removed|disabled)$/.test(action) || value.url != Prefs.subscriptions_antiadblockurl)
+ let url = Prefs.subscriptions_antiadblockurl;
+ if (url != subscription.url)
return;
- if (action == "subscription.updated")
- addAntiAdblockNotification(value);
- else if (action == "subscription.removed" || (action == "subscription.disabled" && !value.disabled))
+ if (url in FilterStorage.knownSubscriptions && subscription.disabled)
+ addAntiAdblockNotification(subscription);
+ else
removeAntiAdblockNotification();
- });
-}
+ }
+
+ FilterNotifier.on("subscription.updated", onSubscriptionChange);
+ FilterNotifier.on("subscription.removed", onSubscriptionChange);
+ FilterNotifier.on("subscription.disabled", onSubscriptionChange);
+};
diff --git a/data/extensions/spyblock@gnu.org/lib/appSupport.js b/data/extensions/spyblock@gnu.org/lib/appSupport.js
index 992d568..ba8fdd1 100644
--- a/data/extensions/spyblock@gnu.org/lib/appSupport.js
+++ b/data/extensions/spyblock@gnu.org/lib/appSupport.js
@@ -1,6 +1,6 @@
/*
* This file is part of Adblock Plus <https://adblockplus.org/>,
- * Copyright (C) 2006-2015 Eyeo GmbH
+ * Copyright (C) 2006-2017 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
@@ -212,48 +212,6 @@ exports.removeBrowserLocationListeners = function removeBrowserLocationListeners
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)
{
@@ -269,7 +227,7 @@ switch (application)
exports.addTab = function ff_addTab(window, url, event)
{
if (event)
- window.openNewTabWith(url, exports.getBrowser(window).contentDocument, null, event, false);
+ window.openNewTabWith(url, null, null, event, false);
else
window.gBrowser.loadOneTab(url, {inBackground: false});
};
@@ -317,7 +275,7 @@ switch (application)
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);
+ window.openNewTabWith(url, null, null, event, false);
else
window.gBrowser.loadOneTab(url, {inBackground: false});
};
@@ -359,7 +317,7 @@ switch (application)
return (browser ? browser.currentURI : null);
}
};
-
+
// for Seamonkey we have to ignore same document flag because of
// bug #1035171 (https://bugzilla.mozilla.org/show_bug.cgi?id=1035171)
let origAddBrowserLocationListener = exports.addBrowserLocationListener;
@@ -617,40 +575,6 @@ switch (application)
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({
@@ -690,6 +614,7 @@ switch (application)
}
case "fennec2":
+ case "adblockbrowser":
{
exports.isKnownWindow = (window) => window.document.documentElement.id == "main-window";
@@ -805,40 +730,6 @@ switch (application)
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");
@@ -876,7 +767,7 @@ switch (application)
onShutdown.add(function()
{
let window = null;
- for (window in UI.applicationWindows)
+ for (window of UI.applicationWindows)
break;
if (window && menuItem)
diff --git a/data/extensions/spyblock@gnu.org/lib/child/bootstrap.js b/data/extensions/spyblock@gnu.org/lib/child/bootstrap.js
new file mode 100644
index 0000000..477ca44
--- /dev/null
+++ b/data/extensions/spyblock@gnu.org/lib/child/bootstrap.js
@@ -0,0 +1,97 @@
+/*
+ * This file is part of Adblock Plus <https://adblockplus.org/>,
+ * Copyright (C) 2006-2017 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/>.
+ */
+
+(function()
+{
+ const Cc = Components.classes;
+ const Ci = Components.interfaces;
+ const Cr = Components.results;
+ const Cu = Components.utils;
+
+ let {Loader, main, unload} = Cu.import("resource://gre/modules/commonjs/toolkit/loader.js", {});
+ let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
+
+ Cu.importGlobalProperties(["atob", "btoa", "File", "URL", "URLSearchParams",
+ "TextDecoder", "TextEncoder"]);
+
+ let shutdownHandlers = [];
+ let onShutdown =
+ {
+ done: false,
+ add: function(handler)
+ {
+ if (shutdownHandlers.indexOf(handler) < 0)
+ shutdownHandlers.push(handler);
+ },
+ remove: function(handler)
+ {
+ let index = shutdownHandlers.indexOf(handler);
+ if (index >= 0)
+ shutdownHandlers.splice(index, 1);
+ }
+ };
+
+ function init()
+ {
+ let url = new URL(Components.stack.filename);
+ let params = new URLSearchParams(url.search.substr(1));
+ let info = JSON.parse(params.get("info"));
+
+ let loader = Loader({
+ paths: {
+ "": info.addonRoot + "lib/"
+ },
+ globals: {
+ Components, Cc, Ci, Cu, Cr, atob, btoa, File, URL, URLSearchParams,
+ TextDecoder, TextEncoder, onShutdown
+ },
+ modules: {"info": info, "messageManager": this},
+ id: info.addonID
+ });
+ onShutdown.add(() => unload(loader, "disable"))
+
+ main(loader, "child/main");
+ }
+
+ function shutdown(message)
+ {
+ if (message.data == Components.stack.filename)
+ {
+ onShutdown.done = true;
+ for (let i = shutdownHandlers.length - 1; i >= 0; i --)
+ {
+ try
+ {
+ shutdownHandlers[i]();
+ }
+ catch (e)
+ {
+ Cu.reportError(e);
+ }
+ }
+ shutdownHandlers = null;
+ }
+ }
+
+ addMessageListener("AdblockPlus:Shutdown", shutdown);
+ onShutdown.add(() =>
+ {
+ removeMessageListener("AdblockPlus:Shutdown", shutdown);
+ });
+
+ init();
+})();
diff --git a/data/extensions/spyblock@gnu.org/lib/child/contentPolicy.js b/data/extensions/spyblock@gnu.org/lib/child/contentPolicy.js
new file mode 100644
index 0000000..97ea7b1
--- /dev/null
+++ b/data/extensions/spyblock@gnu.org/lib/child/contentPolicy.js
@@ -0,0 +1,518 @@
+/*
+ * This file is part of Adblock Plus <https://adblockplus.org/>,
+ * Copyright (C) 2006-2017 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.
+ */
+
+"use strict";
+
+try
+{
+ // Hack: SDK loader masks our Components object with a getter.
+ let proto = Object.getPrototypeOf(this);
+ let property = Object.getOwnPropertyDescriptor(proto, "Components");
+ if (property && property.get)
+ delete proto.Components;
+}
+catch (e)
+{
+ Cu.reportError(e);
+}
+
+let {XPCOMUtils} = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {});
+let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
+
+let {port} = require("messaging");
+let {Utils} = require("utils");
+let {getFrames, isPrivate, getRequestWindow} = require("child/utils");
+let {objectMouseEventHander} = require("child/objectTabs");
+let {RequestNotifier} = require("child/requestNotifier");
+
+/**
+ * Randomly generated class name, to be applied to collapsed nodes.
+ * @type Promise.<string>
+ */
+let collapsedClass = port.emitWithResponse("getCollapsedClass");
+
+/**
+ * Maps numerical content type IDs to strings.
+ * @type Map.<number,string>
+ */
+let types = new Map();
+
+/**
+ * Contains nodes stored by storeNodes() mapped by their IDs.
+ * @type Map.<string,DOMNode[]>
+ */
+let storedNodes = new Map();
+
+/**
+ * Process-dependent prefix to be used for unique nodes identifiers returned
+ * by storeNodes().
+ * @type string
+ */
+let nodesIDPrefix = Services.appinfo.processID + " ";
+
+/**
+ * Counter used to generate unique nodes identifiers in storeNodes().
+ * @type number
+ */
+let maxNodesID = 0;
+
+port.on("deleteNodes", onDeleteNodes);
+port.on("refilterNodes", onRefilterNodes);
+
+/**
+ * Processes parent's response to the ShouldAllow message.
+ * @param {nsIDOMWindow} window window that the request is associated with
+ * @param {nsIDOMElement} node DOM element that the request is associated with
+ * @param {Object|undefined} response object received as response
+ * @return {Boolean} false if the request should be blocked
+ */
+function processPolicyResponse(window, node, response)
+{
+ if (typeof response == "undefined")
+ return true;
+
+ let {allow, collapse, hits} = response;
+ let isObject = false;
+ for (let hit of hits)
+ {
+ if (hit.contentType == "OBJECT")
+ isObject = true;
+
+ let context = node;
+ if (typeof hit.frameIndex == "number")
+ {
+ context = window;
+ for (let i = 0; i < hit.frameIndex; i++)
+ context = context.parent;
+ context = context.document;
+ }
+ RequestNotifier.addNodeData(context, window.top, hit);
+ }
+
+ if (node.nodeType == Ci.nsIDOMNode.ELEMENT_NODE)
+ {
+ // Track mouse events for objects
+ if (allow && isObject)
+ {
+ node.addEventListener("mouseover", objectMouseEventHander, true);
+ node.addEventListener("mouseout", objectMouseEventHander, true);
+ }
+
+ if (collapse)
+ schedulePostProcess(node);
+ }
+ return allow;
+}
+
+/**
+ * Checks whether a request should be allowed, hides it if necessary
+ * @param {nsIDOMWindow} window
+ * @param {nsIDOMElement} node
+ * @param {String} contentType
+ * @param {String} location location of the request, filter key if contentType is ELEMHIDE
+ * @return {Boolean} false if the request should be blocked
+ */
+let shouldAllow = exports.shouldAllow = function(window, node, contentType, location)
+{
+ return processPolicyResponse(window, node, port.emitSync("shouldAllow", {
+ contentType,
+ location,
+ frames: getFrames(window),
+ isPrivate: isPrivate(window)
+ }));
+};
+
+/**
+ * Asynchronously checks whether a request should be allowed.
+ * @param {nsIDOMWindow} window
+ * @param {nsIDOMElement} node
+ * @param {String} contentType
+ * @param {String} location location of the request, filter key if contentType is ELEMHIDE
+ * @param {Function} callback callback to be called with a boolean value, if
+ * false the request should be blocked
+ */
+let shouldAllowAsync = exports.shouldAllowAsync = function(window, node, contentType, location, callback)
+{
+ port.emitWithResponse("shouldAllow", {
+ contentType,
+ location,
+ frames: getFrames(window),
+ isPrivate: isPrivate(window)
+ }).then(response =>
+ {
+ callback(processPolicyResponse(window, node, response));
+ });
+};
+
+/**
+ * Stores nodes and generates a unique ID for them that can be used for
+ * Policy.refilterNodes() later. It's important that Policy.deleteNodes() is
+ * called later, otherwise the nodes will be leaked.
+ * @param {DOMNode[]} nodes list of nodes to be stored
+ * @return {string} unique ID for the nodes
+ */
+let storeNodes = exports.storeNodes = function(nodes)
+{
+ let id = nodesIDPrefix + (++maxNodesID);
+ storedNodes.set(id, nodes);
+ return id;
+};
+
+/**
+ * Called via message whenever Policy.deleteNodes() is called in the parent.
+ */
+function onDeleteNodes(id, sender)
+{
+ storedNodes.delete(id);
+}
+
+/**
+ * Called via message whenever Policy.refilterNodes() is called in the parent.
+ */
+function onRefilterNodes({nodesID, entry}, sender)
+{
+ let nodes = storedNodes.get(nodesID);
+ if (nodes)
+ for (let node of nodes)
+ if (node.nodeType == Ci.nsIDOMNode.ELEMENT_NODE)
+ Utils.runAsync(refilterNode.bind(this, node, entry));
+}
+
+/**
+ * Re-checks filters on an element.
+ */
+function refilterNode(/**Node*/ node, /**Object*/ entry)
+{
+ let wnd = Utils.getWindow(node);
+ if (!wnd || wnd.closed)
+ return;
+
+ if (entry.type == "OBJECT")
+ {
+ node.removeEventListener("mouseover", objectMouseEventHander, true);
+ node.removeEventListener("mouseout", objectMouseEventHander, true);
+ }
+
+ shouldAllow(wnd, node, entry.type, entry.location, (allow) => {
+ // Force node to be collapsed
+ if (!allow)
+ schedulePostProcess(node)
+ });
+}
+
+/**
+ * Actual nsIContentPolicy and nsIChannelEventSink implementation
+ * @class
+ */
+var 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()
+ {
+ // Populate types map
+ let iface = Ci.nsIContentPolicy;
+ for (let name in iface)
+ if (name.indexOf("TYPE_") == 0 && name != "TYPE_DATAREQUEST")
+ types.set(iface[name], name.substr(5));
+
+ let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+ registrar.registerFactory(this.classID, this.classDescription, this.contractID, this);
+
+ let catMan = Utils.categoryManager;
+ for (let category of this.xpcom_categories)
+ catMan.addCategoryEntry(category, this.contractID, this.contractID, false, true);
+
+ Services.obs.addObserver(this, "document-element-inserted", true);
+
+ onShutdown.add(() =>
+ {
+ Services.obs.removeObserver(this, "document-element-inserted");
+
+ for (let category of this.xpcom_categories)
+ catMan.deleteCategoryEntry(category, this.contractID, false);
+
+ registrar.unregisterFactory(this.classID, 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 == Ci.nsIContentPolicy.TYPE_DOCUMENT)
+ return Ci.nsIContentPolicy.ACCEPT;
+
+ // Bail out early for chrome: an resource: URLs, this is a work-around for
+ // https://bugzil.la/1127744 and https://bugzil.la/1247640
+ let location = Utils.unwrapURL(contentLocation);
+ if (location.schemeIs("chrome") || location.schemeIs("resource"))
+ return Ci.nsIContentPolicy.ACCEPT;
+
+ // Ignore standalone objects
+ if (contentType == Ci.nsIContentPolicy.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;
+
+ // Data loaded by plugins should be associated with the document
+ if (contentType == Ci.nsIContentPolicy.TYPE_OBJECT_SUBREQUEST && node instanceof Ci.nsIDOMElement)
+ node = node.ownerDocument;
+
+ // Fix type for objects misrepresented as frames or images
+ if (contentType != Ci.nsIContentPolicy.TYPE_OBJECT && (node instanceof Ci.nsIDOMHTMLObjectElement || node instanceof Ci.nsIDOMHTMLEmbedElement))
+ contentType = Ci.nsIContentPolicy.TYPE_OBJECT;
+
+ let result = shouldAllow(wnd, node, types.get(contentType), location.spec);
+ return (result ? Ci.nsIContentPolicy.ACCEPT : Ci.nsIContentPolicy.REJECT_REQUEST);
+ },
+
+ shouldProcess: function(contentType, contentLocation, requestOrigin, insecNode, mimeType, extra)
+ {
+ return Ci.nsIContentPolicy.ACCEPT;
+ },
+
+ //
+ // nsIObserver interface implementation
+ //
+ _openers: new WeakMap(),
+ _alreadyLoaded: Symbol(),
+
+ observe: function(subject, topic, data, uri)
+ {
+ switch (topic)
+ {
+ case "document-element-inserted":
+ {
+ let window = subject.defaultView;
+ if (!window)
+ return;
+
+ let type = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShellTreeItem)
+ .itemType;
+ if (type != Ci.nsIDocShellTreeItem.typeContent)
+ return;
+
+ let opener = this._openers.get(window);
+ if (opener == this._alreadyLoaded)
+ {
+ // This window has loaded already, ignore it regardless of whether
+ // window.opener is still set.
+ return;
+ }
+
+ if (opener && Cu.isDeadWrapper(opener))
+ opener = null;
+
+ if (!opener)
+ {
+ // We don't know the opener for this window yet, try to find it
+ opener = window.opener;
+ if (!opener)
+ return;
+
+ // The opener might be an intermediate window, get the real one
+ while (opener.location == "about:blank" && opener.opener)
+ opener = opener.opener;
+
+ this._openers.set(window, opener);
+
+ let forgetPopup = event =>
+ {
+ subject.removeEventListener("DOMContentLoaded", forgetPopup);
+ this._openers.set(window, this._alreadyLoaded);
+ };
+ subject.addEventListener("DOMContentLoaded", forgetPopup);
+ }
+
+ if (!uri)
+ uri = window.location.href;
+ if (!shouldAllow(opener, opener.document, "POPUP", uri))
+ {
+ window.stop();
+ Utils.runAsync(() => window.close());
+ }
+ else if (uri == "about:blank")
+ {
+ // An about:blank pop-up most likely means that a load will be
+ // initiated asynchronously. Wait for that.
+ Utils.runAsync(() =>
+ {
+ let channel = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDocShell)
+ .QueryInterface(Ci.nsIDocumentLoader)
+ .documentChannel;
+ if (channel)
+ this.observe(subject, topic, data, channel.URI.spec);
+ });
+ }
+ break;
+ }
+ }
+ },
+
+ //
+ // nsIChannelEventSink interface implementation
+ //
+
+ asyncOnChannelRedirect: function(oldChannel, newChannel, flags, callback)
+ {
+ let async = false;
+ try
+ {
+ // nsILoadInfo.contentPolicyType was introduced in Gecko 35, then
+ // renamed to nsILoadInfo.externalContentPolicyType in Gecko 44.
+ let loadInfo = oldChannel.loadInfo;
+ let contentType = ("externalContentPolicyType" in loadInfo ?
+ loadInfo.externalContentPolicyType : loadInfo.contentPolicyType);
+ if (!contentType)
+ return;
+
+ let wnd = getRequestWindow(newChannel);
+ if (!wnd)
+ return;
+
+ if (contentType == Ci.nsIContentPolicy.TYPE_DOCUMENT)
+ {
+ if (wnd.history.length <= 1 && wnd.opener)
+ {
+ // Special treatment for pop-up windows - this will close the window
+ // rather than preventing the redirect. Note that we might not have
+ // seen the original channel yet because the redirect happened before
+ // the async code in observe() had a chance to run.
+ this.observe(wnd.document, "document-element-inserted", null, oldChannel.URI.spec);
+ this.observe(wnd.document, "document-element-inserted", null, newChannel.URI.spec);
+ }
+ return;
+ }
+
+ shouldAllowAsync(wnd, wnd.document, types.get(contentType), newChannel.URI.spec, function(allow)
+ {
+ callback.onRedirectVerifyCallback(allow ? Cr.NS_OK : Cr.NS_BINDING_ABORTED);
+ });
+ async = true;
+ }
+ catch (e)
+ {
+ // We shouldn't throw exceptions here - this will prevent the redirect.
+ Cu.reportError(e);
+ }
+ finally
+ {
+ if (!async)
+ callback.onRedirectVerifyCallback(Cr.NS_OK);
+ }
+ },
+
+ //
+ // 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 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()
+{
+ collapsedClass.then(cls =>
+ {
+ let nodes = scheduledNodes;
+ scheduledNodes = null;
+
+ // Resolving class is async initially so the nodes might have already been
+ // processed in the meantime.
+ if (!nodes)
+ return;
+
+ 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(cls);
+ }
+ });
+}
diff --git a/data/extensions/spyblock@gnu.org/lib/child/contextMenu.js b/data/extensions/spyblock@gnu.org/lib/child/contextMenu.js
new file mode 100644
index 0000000..297ef3e
--- /dev/null
+++ b/data/extensions/spyblock@gnu.org/lib/child/contextMenu.js
@@ -0,0 +1,137 @@
+/*
+ * This file is part of Adblock Plus <https://adblockplus.org/>,
+ * Copyright (C) 2006-2017 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/>.
+ */
+
+"use strict";
+
+let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
+let {XPCOMUtils} = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {});
+
+let {Utils} = require("utils");
+let {RequestNotifier} = require("child/requestNotifier");
+let {storeNodes} = require("child/contentPolicy");
+
+/**
+ * Determines the context menu entries to be shown for a contextmenu event.
+ * @param {Event} event
+ * @return {Array}
+ */
+function getContextInfo(event)
+{
+ let items = [];
+ let target = event.target;
+ if (target.localName == "menupopup" && target.triggerNode)
+ {
+ // SeaMonkey gives us the context menu's popupshowing event
+ target = target.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 items;
+
+ let addMenuItem = function([node, nodeData])
+ {
+ let nodeID = null;
+ if (node && node.nodeType == Ci.nsIDOMNode.ELEMENT_NODE)
+ nodeID = storeNodes([node]);
+ items.push([nodeID, nodeData]);
+ }.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].type == "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, "IMAGE", bgImage);
+ if (data && !data[1].filter)
+ {
+ addMenuItem(data);
+ break;
+ }
+ }
+ }
+
+ node = node.parentNode;
+ }
+ }
+
+ return items;
+};
+
+let ContextMenuObserver =
+{
+ observe: function(subject, topic, data)
+ {
+ if (subject.wrappedJSObject)
+ subject = subject.wrappedJSObject;
+
+ if (subject.addonInfo)
+ subject.addonInfo.adblockplus = getContextInfo(subject.event);
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsISupportsWeakReference, Ci.nsIObserver])
+};
+
+Services.obs.addObserver(ContextMenuObserver, "content-contextmenu", true);
+Services.obs.addObserver(ContextMenuObserver, "AdblockPlus:content-contextmenu", true);
+onShutdown.add(() => {
+ Services.obs.removeObserver(ContextMenuObserver, "content-contextmenu");
+ Services.obs.removeObserver(ContextMenuObserver, "AdblockPlus:content-contextmenu");
+});
diff --git a/data/extensions/spyblock@gnu.org/lib/child/dataCollector.js b/data/extensions/spyblock@gnu.org/lib/child/dataCollector.js
new file mode 100644
index 0000000..09c334a
--- /dev/null
+++ b/data/extensions/spyblock@gnu.org/lib/child/dataCollector.js
@@ -0,0 +1,108 @@
+/*
+ * This file is part of Adblock Plus <https://adblockplus.org/>,
+ * Copyright (C) 2006-2017 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 Collects some data for a content window, to be attached to
+ * issue reports.
+ */
+
+"use strict";
+
+let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
+let {Task} = Cu.import("resource://gre/modules/Task.jsm", {});
+let {PrivateBrowsingUtils} = Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm", {});
+
+let {port} = require("messaging");
+let {Utils} = require("utils");
+
+port.on("collectData", onCollectData);
+
+function onCollectData({outerWindowID, screenshotWidth}, sender)
+{
+ let window = Services.wm.getOuterWindowWithId(outerWindowID);
+ if (window)
+ {
+ return Task.spawn(function*()
+ {
+ let data = {};
+ data.isPrivate = PrivateBrowsingUtils.isContentWindowPrivate(window);
+ data.opener = window.opener ? window.opener.location.href : null;
+ data.referrer = window.document.referrer;
+ data.frames = yield scanFrames(window);
+ data.screenshot = yield createScreenshot(window, screenshotWidth);
+ return data;
+ });
+ }
+}
+
+function scanFrames(window)
+{
+ let frames = [];
+ for (let i = 0; i < window.frames.length; i++)
+ {
+ let frame = window.frames[i];
+ frames.push({
+ url: frame.location.href,
+ frames: scanFrames(frame)
+ });
+ }
+ return frames;
+}
+
+function* createScreenshot(window, screenshotWidth)
+{
+ let canvas = window.document.createElement("canvas");
+ canvas.width = screenshotWidth;
+
+ let context = canvas.getContext("2d");
+ let wndWidth = window.document.documentElement.scrollWidth;
+ let wndHeight = window.document.documentElement.scrollHeight;
+
+ // Copy scaled screenshot of the webpage, according to the specified width.
+
+ // Gecko doesn't like sizes more than 64k, restrict to 30k to be on the safe side.
+ // Also, make sure height is at most five times the width to keep image size down.
+ let copyWidth = Math.min(wndWidth, 30000);
+ let copyHeight = Math.min(wndHeight, 30000, copyWidth * 5);
+ let copyX = Math.max(Math.min(window.scrollX - copyWidth / 2, wndWidth - copyWidth), 0);
+ let copyY = Math.max(Math.min(window.scrollY - copyHeight / 2, wndHeight - copyHeight), 0);
+
+ let scalingFactor = screenshotWidth / copyWidth;
+ canvas.height = copyHeight * scalingFactor;
+
+ context.save();
+ context.scale(scalingFactor, scalingFactor);
+ context.drawWindow(window, copyX, copyY, copyWidth, copyHeight, "rgb(255,255,255)");
+ context.restore();
+
+ // Reduce colors
+ let pixelData = context.getImageData(0, 0, canvas.width, canvas.height);
+ let data = pixelData.data;
+ let mapping = [0x00, 0x55, 0xAA, 0xFF];
+ for (let i = 0; i < data.length; i++)
+ {
+ data[i] = mapping[data[i] >> 6];
+
+ if (i % 5000 == 0)
+ {
+ // Take a break every 5000 bytes to prevent browser hangs
+ yield new Promise((resolve, reject) => Utils.runAsync(resolve));
+ }
+ }
+
+ return pixelData;
+}
diff --git a/data/extensions/spyblock@gnu.org/lib/child/elemHide.js b/data/extensions/spyblock@gnu.org/lib/child/elemHide.js
new file mode 100644
index 0000000..988adee
--- /dev/null
+++ b/data/extensions/spyblock@gnu.org/lib/child/elemHide.js
@@ -0,0 +1,403 @@
+/*
+ * This file is part of Adblock Plus <https://adblockplus.org/>,
+ * Copyright (C) 2006-2017 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 Serves CSS for element hiding and processes hits.
+ */
+
+try
+{
+ // Hack: SDK loader masks our Components object with a getter.
+ let proto = Object.getPrototypeOf(this);
+ let property = Object.getOwnPropertyDescriptor(proto, "Components");
+ if (property && property.get)
+ delete proto.Components;
+}
+catch (e)
+{
+ Cu.reportError(e);
+}
+
+let {XPCOMUtils} = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {});
+let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
+
+let {shouldAllowAsync} = require("child/contentPolicy");
+let {getFrames, isPrivate, getRequestWindow} = require("child/utils");
+let {RequestNotifier} = require("child/requestNotifier");
+let {port} = require("messaging");
+let {Utils} = require("utils");
+
+const notImplemented = () => Cr.NS_ERROR_NOT_IMPLEMENTED;
+
+/**
+ * about: URL module used to count hits.
+ * @class
+ */
+let AboutHandler =
+{
+ classID: Components.ID("{55fb7be0-1dd2-11b2-98e6-9e97caf8ba67}"),
+ classDescription: "Element hiding hit registration protocol handler",
+ aboutPrefix: "abp-elemhide",
+
+ /**
+ * 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 Ci.nsIAboutModule.HIDE_FROM_ABOUTABOUT;
+ },
+
+ newChannel: function(uri, loadInfo)
+ {
+ let match = /\?hit(\d+)$/.exec(uri.path);
+ if (match)
+ return new HitRegistrationChannel(uri, loadInfo, match[1]);
+
+ match = /\?css(?:=(.*?))?(&specificonly)?$/.exec(uri.path);
+ if (match)
+ {
+ return new StyleDataChannel(uri, loadInfo,
+ match[1] ? decodeURIComponent(match[1]) : null, !!match[2]);
+ }
+
+ throw Cr.NS_ERROR_FAILURE;
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIFactory, Ci.nsIAboutModule])
+};
+AboutHandler.init();
+
+/**
+ * Base class for channel implementations, subclasses usually only need to
+ * override BaseChannel._getResponse() method.
+ * @constructor
+ */
+function BaseChannel(uri, loadInfo)
+{
+ this.URI = this.originalURI = uri;
+ this.loadInfo = loadInfo;
+}
+BaseChannel.prototype = {
+ URI: null,
+ originalURI: null,
+ contentCharset: "utf-8",
+ contentLength: 0,
+ contentType: null,
+ owner: Utils.systemPrincipal,
+ securityInfo: null,
+ notificationCallbacks: null,
+ loadFlags: 0,
+ loadGroup: null,
+ name: null,
+ status: Cr.NS_OK,
+
+ _getResponse: notImplemented,
+
+ _checkSecurity: function()
+ {
+ if (!this.loadInfo.triggeringPrincipal.equals(Utils.systemPrincipal))
+ throw Cr.NS_ERROR_FAILURE;
+ },
+
+ asyncOpen: function(listener, context)
+ {
+ Promise.resolve(this._getResponse()).then(data =>
+ {
+ let stream = Cc["@mozilla.org/io/string-input-stream;1"]
+ .createInstance(Ci.nsIStringInputStream);
+ stream.setData(data, data.length);
+
+ try
+ {
+ listener.onStartRequest(this, context);
+ }
+ catch(e)
+ {
+ // Listener failing isn't our problem
+ }
+
+ try
+ {
+ listener.onDataAvailable(this, context, stream, 0, stream.available());
+ }
+ catch(e)
+ {
+ // Listener failing isn't our problem
+ }
+
+ try
+ {
+ listener.onStopRequest(this, context, Cr.NS_OK);
+ }
+ catch(e)
+ {
+ // Listener failing isn't our problem
+ }
+ });
+ },
+
+ asyncOpen2: function(listener)
+ {
+ this._checkSecurity();
+ this.asyncOpen(listener, null);
+ },
+
+ open: function()
+ {
+ let data = this._getResponse();
+ if (typeof data.then == "function")
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+
+ let stream = Cc["@mozilla.org/io/string-input-stream;1"]
+ .createInstance(Ci.nsIStringInputStream);
+ stream.setData(data, data.length);
+ return stream;
+ },
+
+ open2: function()
+ {
+ this._checkSecurity();
+ return this.open();
+ },
+
+ isPending: () => false,
+ cancel: notImplemented,
+ suspend: notImplemented,
+ resume: notImplemented,
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIChannel, Ci.nsIRequest])
+};
+
+/**
+ * Channel returning CSS data for the global as well as site-specific stylesheet.
+ * @constructor
+ */
+function StyleDataChannel(uri, loadInfo, domain, specificOnly)
+{
+ BaseChannel.call(this, uri, loadInfo);
+ this._domain = domain;
+ this._specificOnly = specificOnly;
+}
+StyleDataChannel.prototype = {
+ __proto__: BaseChannel.prototype,
+ contentType: "text/css",
+ _domain: null,
+
+ _getResponse: function()
+ {
+ function escapeChar(match)
+ {
+ return "\\" + match.charCodeAt(0).toString(16) + " ";
+ }
+
+ // Would be great to avoid sync messaging here but nsIStyleSheetService
+ // insists on opening channels synchronously.
+ let [selectors, keys] = (this._domain ?
+ port.emitSync("getSelectorsForDomain", [this._domain, this._specificOnly]) :
+ port.emitSync("getUnconditionalSelectors"));
+
+ let cssPrefix = "{-moz-binding: url(about:abp-elemhide?hit";
+ let cssSuffix = "#dummy) !important;}\n";
+ let result = [];
+
+ for (let i = 0; i < selectors.length; i++)
+ {
+ let selector = selectors[i];
+ let key = keys[i];
+ result.push(selector.replace(/[^\x01-\x7F]/g, escapeChar),
+ cssPrefix, key, cssSuffix);
+ }
+
+ return result.join("");
+ }
+};
+
+/**
+ * Channel returning data for element hiding hits.
+ * @constructor
+ */
+function HitRegistrationChannel(uri, loadInfo, key)
+{
+ BaseChannel.call(this, uri, loadInfo);
+ this.key = key;
+}
+HitRegistrationChannel.prototype = {
+ __proto__: BaseChannel.prototype,
+ key: null,
+ contentType: "text/xml",
+
+ _getResponse: function()
+ {
+ let window = getRequestWindow(this);
+ port.emitWithResponse("registerElemHideHit", {
+ key: this.key,
+ frames: getFrames(window),
+ isPrivate: isPrivate(window)
+ }).then(hit =>
+ {
+ if (hit)
+ RequestNotifier.addNodeData(window.document, window.top, hit);
+ });
+ return "<bindings xmlns='http://www.mozilla.org/xbl'/>";
+ }
+};
+
+let observer = {
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsIObserver, Ci.nsISupportsWeakReference
+ ]),
+
+ topic: "document-element-inserted",
+ styleURL: Utils.makeURI("about:abp-elemhide?css"),
+ sheet: null,
+
+ init: function()
+ {
+ Services.obs.addObserver(this, this.topic, true);
+ onShutdown.add(() =>
+ {
+ Services.obs.removeObserver(this, this.topic);
+ });
+
+ port.on("elemhideupdate", () =>
+ {
+ this.sheet = null;
+ });
+ },
+
+ observe: function(subject, topic, data)
+ {
+ if (topic != this.topic)
+ return;
+
+ let window = subject.defaultView;
+ if (!window)
+ {
+ // This is typically XBL bindings and SVG images, but also real
+ // documents occasionally - probably due to speculative loading?
+ return;
+ }
+ let type = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShellTreeItem)
+ .itemType;
+ if (type != Ci.nsIDocShellTreeItem.typeContent)
+ return;
+
+ port.emitWithResponse("elemhideEnabled", {
+ frames: getFrames(window),
+ isPrivate: isPrivate(window)
+ }).then(({
+ enabled, contentType, docDomain, thirdParty, location, filter,
+ filterType
+ }) =>
+ {
+ if (Cu.isDeadWrapper(window))
+ {
+ // We are too late, the window is gone already.
+ return;
+ }
+
+ if (enabled)
+ {
+ let utils = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+
+ // If we have a filter hit at this point then it must be a $generichide
+ // filter - apply only specific element hiding filters.
+ let specificOnly = !!filter;
+ if (!specificOnly)
+ {
+ if (!this.sheet)
+ {
+ this.sheet = Utils.styleService.preloadSheet(this.styleURL,
+ Ci.nsIStyleSheetService.USER_SHEET);
+ }
+
+ try
+ {
+ utils.addSheet(this.sheet, Ci.nsIStyleSheetService.USER_SHEET);
+ }
+ catch (e)
+ {
+ // Ignore NS_ERROR_ILLEGAL_VALUE - it will be thrown if we try to add
+ // the stylesheet multiple times to the same document (the observer
+ // will be notified twice for some documents).
+ if (e.result != Cr.NS_ERROR_ILLEGAL_VALUE)
+ throw e;
+ }
+ }
+
+ let host = window.location.hostname;
+ if (host)
+ {
+ try
+ {
+ let suffix = "=" + encodeURIComponent(host);
+ if (specificOnly)
+ suffix += "&specificonly";
+ utils.loadSheetUsingURIString(this.styleURL.spec + suffix,
+ Ci.nsIStyleSheetService.USER_SHEET);
+ }
+ catch (e)
+ {
+ // Ignore NS_ERROR_ILLEGAL_VALUE - it will be thrown if we try to add
+ // the stylesheet multiple times to the same document (the observer
+ // will be notified twice for some documents).
+ if (e.result != Cr.NS_ERROR_ILLEGAL_VALUE)
+ throw e;
+ }
+ }
+ }
+
+ if (filter)
+ {
+ RequestNotifier.addNodeData(window.document, window.top, {
+ contentType, docDomain, thirdParty, location, filter, filterType
+ });
+ }
+ });
+ }
+};
+observer.init();
diff --git a/data/extensions/spyblock@gnu.org/lib/child/elemHideEmulation.js b/data/extensions/spyblock@gnu.org/lib/child/elemHideEmulation.js
new file mode 100644
index 0000000..7c4ee17
--- /dev/null
+++ b/data/extensions/spyblock@gnu.org/lib/child/elemHideEmulation.js
@@ -0,0 +1,118 @@
+/*
+ * This file is part of Adblock Plus <https://adblockplus.org/>,
+ * Copyright (C) 2006-2017 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/>.
+ */
+
+"use strict";
+
+(function()
+{
+ let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
+
+ let {port} = require("messaging");
+ let {getFrames, isPrivate} = require("child/utils");
+ let {RequestNotifier} = require("child/requestNotifier");
+
+ function getFilters(window, callback)
+ {
+ let message = {
+ frames: getFrames(window),
+ payload: {
+ type: "filters.get",
+ what: "elemhideemulation"
+ }
+ };
+ port.emitWithResponse("ext_message", message).then(callback);
+ }
+
+ function addUserCSS(window, cssCode)
+ {
+ let uri = Services.io.newURI("data:text/css," + encodeURIComponent(cssCode),
+ null, null);
+ let utils = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+ utils.loadSheet(uri, Ci.nsIDOMWindowUtils.USER_SHEET);
+ }
+
+ function initElemHideEmulation()
+ {
+ let scope = Object.assign({}, require("common"));
+ Services.scriptloader.loadSubScript(
+ "chrome://adblockplus/content/elemHideEmulation.js", scope);
+
+ let onContentWindow = (subject, topic, data) =>
+ {
+ if (!(subject instanceof Ci.nsIDOMWindow))
+ return;
+
+ let onReady = event =>
+ {
+ subject.removeEventListener("load", onReady);
+ let handler = new scope.ElemHideEmulation(
+ subject, getFilters.bind(null, subject), (selectors, filters) =>
+ {
+ if (selectors.length == 0)
+ return;
+
+ addUserCSS(subject, selectors.map(
+ selector => selector + "{display: none !important;}"
+ ).join("\n"));
+
+ if (!isPrivate(subject))
+ port.emit("addHits", filters);
+
+ let docDomain = null;
+ try
+ {
+ // We are calling getFrames() here because it will consider
+ // "inheritance" for about:blank and data: frames.
+ docDomain = new URL(getFrames(subject)[0].location).hostname;
+ }
+ catch (e)
+ {
+ // Invalid URL?
+ }
+
+ for (let i = 0; i < filters.length; i++)
+ {
+ RequestNotifier.addNodeData(subject.document, subject.top, {
+ contentType: "ELEMHIDE",
+ docDomain: docDomain,
+ thirdParty: false,
+ location: "##" + selectors[i],
+ filter: filters[i],
+ filterType: "elemhideemulation"
+ });
+ }
+ }
+ );
+
+ handler.apply();
+ };
+
+ subject.addEventListener("load", onReady);
+ };
+
+ Services.obs.addObserver(onContentWindow, "content-document-global-created",
+ false);
+ onShutdown.add(() =>
+ {
+ Services.obs.removeObserver(onContentWindow,
+ "content-document-global-created");
+ });
+ }
+
+ initElemHideEmulation();
+})();
diff --git a/data/extensions/spyblock@gnu.org/lib/child/flasher.js b/data/extensions/spyblock@gnu.org/lib/child/flasher.js
new file mode 100644
index 0000000..492f4e0
--- /dev/null
+++ b/data/extensions/spyblock@gnu.org/lib/child/flasher.js
@@ -0,0 +1,99 @@
+/*
+ * This file is part of Adblock Plus <https://adblockplus.org/>,
+ * Copyright (C) 2006-2017 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 Draws a blinking border for a list of matching elements.
+ */
+
+function Flasher(elements, scrollToItem)
+{
+ if (scrollToItem && elements[0].ownerDocument)
+ {
+ // Ensure that at least one element is visible when flashing
+ elements[0].scrollIntoView();
+ }
+
+ this.elements = elements;
+ this.count = 0;
+
+ this.doFlash();
+
+}
+Flasher.prototype =
+{
+ elements: null,
+ count: 0,
+ timer: null,
+
+ doFlash: function()
+ {
+ if (this.count >= 12)
+ {
+ this.stop();
+ return;
+ }
+
+ if (this.count % 2)
+ this.switchOff();
+ else
+ this.switchOn();
+
+ this.count++;
+
+ this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ this.timer.initWithCallback(() => this.doFlash(), 300, Ci.nsITimer.TYPE_ONE_SHOT);
+ },
+
+ stop: function()
+ {
+ if (this.timer)
+ {
+ this.timer.cancel();
+ this.timer = null;
+ }
+
+ if (this.elements)
+ {
+ this.switchOff();
+ this.elements = null;
+ }
+ },
+
+ setOutline: function(outline, offset)
+ {
+ for (let element of this.elements)
+ {
+ if (!Cu.isDeadWrapper(element) && "style" in element)
+ {
+ element.style.outline = outline;
+ element.style.outlineOffset = offset;
+ }
+ }
+ },
+
+ switchOn: function()
+ {
+ this.setOutline("#CC0000 dotted 2px", "-2px");
+ },
+
+ switchOff: function()
+ {
+ this.setOutline("", "");
+ }
+};
+
+exports.Flasher = Flasher;
diff --git a/data/extensions/spyblock@gnu.org/lib/child/main.js b/data/extensions/spyblock@gnu.org/lib/child/main.js
new file mode 100644
index 0000000..bc21e9a
--- /dev/null
+++ b/data/extensions/spyblock@gnu.org/lib/child/main.js
@@ -0,0 +1,31 @@
+/*
+ * This file is part of Adblock Plus <https://adblockplus.org/>,
+ * Copyright (C) 2006-2017 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/>.
+ */
+
+let {port} = require("messaging");
+
+// Only initialize after receiving a "response" to a dummy message - this makes
+// sure that on update the old version has enough time to receive and process
+// the shutdown message.
+port.emitWithResponse("ping").then(() =>
+{
+ require("child/elemHide");
+ require("child/contentPolicy");
+ require("child/contextMenu");
+ require("child/dataCollector");
+ require("child/elemHideEmulation");
+ require("child/subscribeLinks");
+}).catch(e => Cu.reportError(e));
diff --git a/data/extensions/spyblock@gnu.org/lib/child/objectTabs.js b/data/extensions/spyblock@gnu.org/lib/child/objectTabs.js
new file mode 100644
index 0000000..74e7387
--- /dev/null
+++ b/data/extensions/spyblock@gnu.org/lib/child/objectTabs.js
@@ -0,0 +1,405 @@
+/*
+ * This file is part of Adblock Plus <https://adblockplus.org/>,
+ * Copyright (C) 2006-2017 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.
+ */
+
+let {port} = require("messaging");
+
+/**
+ * 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,
+
+ /**
+ * 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 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,
+
+ /**
+ * Localized texts and class names to be used for the tab. This will be set
+ * when showTabFor is called for the first time.
+ * @type Object
+ */
+ texts: null,
+
+ /**
+ * 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" ||
+ application == "adblockbrowser")
+ return;
+
+ if (!this.texts)
+ this.texts = port.emitWithResponse("getObjectTabsTexts");
+ Promise.all([port.emitWithResponse("getObjectTabsStatus"), this.texts])
+ .then(([status, texts]) =>
+ {
+ this.texts = texts;
+ if (!status)
+ 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 {RequestNotifier} = require("child/requestNotifier");
+ let data = RequestNotifier.getDataForNode(element, true, "OBJECT");
+ if (data)
+ this._showTab(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.
+ * @param {Element} element
+ * @param {RequestEntry} data
+ */
+ _showTab: function(element, data)
+ {
+ let doc = element.ownerDocument.defaultView.top.document;
+
+ this.objtabElement = doc.createElementNS("http://www.w3.org/1999/xhtml", "a");
+ this.objtabElement.textContent = this.texts.label;
+ this.objtabElement.setAttribute("title", this.texts.tooltip);
+ this.objtabElement.setAttribute("href", data.location);
+ this.objtabElement.setAttribute("class", this.texts.classHidden);
+ 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()
+ {
+ 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.texts.classVisibleTop;
+ let left = objRect.right - this.objtabElement.offsetWidth;
+ let top = objRect.top - this.objtabElement.offsetHeight;
+ if (top < 0)
+ {
+ top = objRect.bottom;
+ className = this.texts.classVisibleBottom;
+ }
+
+ 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 {storeNodes} = require("child/contentPolicy");
+ let nodesID = storeNodes([this.currentElement]);
+ port.emit("blockItem", {
+ request: this.objtabElement.nodeData,
+ nodesID
+ });
+ },
+
+ /**
+ * Called whenever a timer fires.
+ * @param {nsISupport} subject
+ * @param {string} topic
+ * @param {string} data
+ */
+ observe: function(subject, topic, 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/child/requestNotifier.js b/data/extensions/spyblock@gnu.org/lib/child/requestNotifier.js
new file mode 100644
index 0000000..fc6d314
--- /dev/null
+++ b/data/extensions/spyblock@gnu.org/lib/child/requestNotifier.js
@@ -0,0 +1,444 @@
+/*
+ * This file is part of Adblock Plus <https://adblockplus.org/>,
+ * Copyright (C) 2006-2017 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.
+ */
+let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
+
+let {port} = require("messaging");
+let {Utils} = require("utils");
+let {Flasher} = require("child/flasher");
+
+let nodeData = new WeakMap();
+let windowStats = new WeakMap();
+let windowData = new WeakMap();
+let requestEntryMaxId = 0;
+
+/**
+ * Active RequestNotifier instances by their ID
+ * @type Map.<number,RequestNotifier>
+ */
+let notifiers = new Map();
+
+port.on("startWindowScan", onStartScan);
+port.on("shutdownNotifier", onNotifierShutdown);
+port.on("flashNodes", onFlashNodes);
+port.on("retrieveNodeSize", onRetrieveNodeSize);
+port.on("storeNodesForEntries", onStoreNodes);
+port.on("retrieveWindowStats", onRetrieveWindowStats);
+port.on("storeWindowData", onStoreWindowData);
+port.on("retrieveWindowData", onRetrieveWindowData);
+
+function onStartScan({notifierID, outerWindowID})
+{
+ let window = Services.wm.getOuterWindowWithId(outerWindowID);
+ if (window)
+ new RequestNotifier(window, notifierID);
+}
+
+function onNotifierShutdown(notifierID)
+{
+ let notifier = notifiers.get(notifierID);
+ if (notifier)
+ notifier.shutdown();
+}
+
+function onFlashNodes({notifierID, requests, scrollToItem})
+{
+ let notifier = notifiers.get(notifierID);
+ if (notifier)
+ notifier.flashNodes(requests, scrollToItem);
+}
+
+function onRetrieveNodeSize({notifierID, requests})
+{
+ let notifier = notifiers.get(notifierID);
+ if (notifier)
+ return notifier.retrieveNodeSize(requests);
+}
+
+function onStoreNodes({notifierID, requests})
+{
+ let notifier = notifiers.get(notifierID);
+ if (notifier)
+ return notifier.storeNodesForEntries(requests);
+}
+
+function onRetrieveWindowStats(outerWindowID)
+{
+ let window = Services.wm.getOuterWindowWithId(outerWindowID);
+ if (window)
+ return RequestNotifier.getWindowStatistics(window);
+}
+
+function onStoreWindowData({outerWindowID, data})
+{
+ let window = Services.wm.getOuterWindowWithId(outerWindowID);
+ if (window)
+ windowData.set(window.document, data);
+};
+
+function onRetrieveWindowData(outerWindowID)
+{
+ let window = Services.wm.getOuterWindowWithId(outerWindowID);
+ if (window)
+ return windowData.get(window.document) || null;
+};
+
+/**
+ * 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} window window to attach the notifier to
+ * @param {Integer} notifierID Parent notifier ID to be messaged
+ */
+function RequestNotifier(window, notifierID)
+{
+ this.window = window;
+ this.id = notifierID;
+ notifiers.set(this.id, this);
+ this.nodes = new Map();
+ this.startScan(window);
+}
+exports.RequestNotifier = RequestNotifier;
+
+RequestNotifier.prototype =
+{
+ /**
+ * Parent notifier ID to be messaged
+ * @type Integer
+ */
+ id: null,
+
+ /**
+ * The window this notifier is associated with.
+ * @type Window
+ */
+ window: null,
+
+ /**
+ * Nodes associated with a particular request ID.
+ * @type Map.<number,Node>
+ */
+ nodes: null,
+
+ /**
+ * 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.nodes;
+ this.stopFlashing();
+ notifiers.delete(this.id);
+ },
+
+ /**
+ * Notifies the parent about a new request.
+ * @param {Node} node DOM node that the request is associated with
+ * @param {Object} entry
+ */
+ notifyListener: function(node, entry)
+ {
+ if (this.nodes)
+ this.nodes.set(entry.id, node);
+ port.emit("foundNodeData", {
+ notifierID: this.id,
+ data: entry
+ });
+ },
+
+ onComplete: function()
+ {
+ port.emit("scanComplete", this.id);
+ },
+
+ /**
+ * 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()
+ {
+ // Don't do anything if the notifier was shut down already.
+ if (!this.window)
+ return;
+
+ let node = walker.currentNode;
+ let data = nodeData.get(node);
+ if (typeof data != "undefined")
+ for (let k in data)
+ this.notifyListener(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.onComplete();
+ }
+ }
+ }.bind(this);
+
+ // Process each node in a separate event to allow other events to process
+ this.eventsPosted++;
+ Utils.runAsync(process);
+ },
+
+ /**
+ * Makes the nodes associated with the given requests blink.
+ * @param {number[]} requests list of request IDs that were previously
+ * reported by this notifier.
+ * @param {boolean} scrollToItem if true, scroll to first node
+ */
+ flashNodes: function(requests, scrollToItem)
+ {
+ this.stopFlashing();
+
+ let nodes = [];
+ for (let id of requests)
+ {
+ if (!this.nodes.has(id))
+ continue;
+
+ let node = this.nodes.get(id);
+ if (Cu.isDeadWrapper(node))
+ this.nodes.delete(node);
+ else if (node.nodeType == Ci.nsIDOMNode.ELEMENT_NODE)
+ nodes.push(node);
+ }
+ if (nodes.length)
+ this.flasher = new Flasher(nodes, scrollToItem);
+ },
+
+ /**
+ * Stops flashing nodes after a previous flashNodes() call.
+ */
+ stopFlashing: function()
+ {
+ if (this.flasher)
+ this.flasher.stop();
+ this.flasher = null;
+ },
+
+ /**
+ * Attempts to calculate the size of the nodes associated with the requests.
+ * @param {number[]} requests list of request IDs that were previously
+ * reported by this notifier.
+ * @return {number[]|null} either an array containing width and height or
+ * null if the size could not be calculated.
+ */
+ retrieveNodeSize: function(requests)
+ {
+ function getNodeSize(node)
+ {
+ if (node instanceof Ci.nsIDOMHTMLImageElement && (node.naturalWidth || node.naturalHeight))
+ return [node.naturalWidth, node.naturalHeight];
+ else if (node instanceof Ci.nsIDOMHTMLElement && (node.offsetWidth || node.offsetHeight))
+ return [node.offsetWidth, node.offsetHeight];
+ else
+ return null;
+ }
+
+ let size = null;
+ for (let id of requests)
+ {
+ if (!this.nodes.has(id))
+ continue;
+
+ let node = this.nodes.get(id);
+ if (Cu.isDeadWrapper(node))
+ this.nodes.delete(node);
+ else
+ {
+ size = getNodeSize(node);
+ if (size)
+ break;
+ }
+ }
+ return size;
+ },
+
+ /**
+ * Stores the nodes associated with the requests and generates a unique ID
+ * for them that can be used with Policy.refilterNodes().
+ * @param {number[]} requests list of request IDs that were previously
+ * reported by this notifier.
+ * @return {string} unique identifiers associated with the nodes.
+ */
+ storeNodesForEntries: function(requests)
+ {
+ let nodes = [];
+ for (let id of requests)
+ {
+ if (!this.nodes.has(id))
+ continue;
+
+ let node = this.nodes.get(id);
+ if (Cu.isDeadWrapper(node))
+ this.nodes.delete(node);
+ else
+ nodes.push(node);
+ }
+
+ let {storeNodes} = require("child/contentPolicy");
+ return storeNodes(nodes);
+ }
+};
+
+/**
+ * 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 {Object} hitData
+ * @param {String} hitData.contentType request type, e.g. "IMAGE"
+ * @param {String} hitData.docDomain domain of the document that initiated the request
+ * @param {Boolean} hitData.thirdParty will be true if a third-party server has been requested
+ * @param {String} hitData.location the address that has been requested
+ * @param {String} hitData.filter filter applied to the request or null if none
+ * @param {String} hitData.filterType type of filter applied to the request
+ */
+RequestNotifier.addNodeData = function(node, topWnd, {contentType, docDomain, thirdParty, location, filter, filterType})
+{
+ let entry = {
+ id: ++requestEntryMaxId,
+ type: contentType,
+ docDomain, thirdParty, location, filter
+ };
+
+ let existingData = nodeData.get(node);
+ if (typeof existingData == "undefined")
+ {
+ existingData = {};
+ nodeData.set(node, existingData);
+ }
+
+ // Add this request to the node data
+ existingData[contentType + " " + location] = entry;
+
+ // Update window statistics
+ if (!windowStats.has(topWnd.document))
+ {
+ windowStats.set(topWnd.document, {
+ items: 0,
+ hidden: 0,
+ blocked: 0,
+ whitelisted: 0,
+ filters: {}
+ });
+ }
+
+ let stats = windowStats.get(topWnd.document);
+ if (filterType != "elemhide" && filterType != "elemhideexception" && filterType != "elemhideemulation")
+ stats.items++;
+ if (filter)
+ {
+ if (filterType == "blocking")
+ stats.blocked++;
+ else if (filterType == "whitelist" || filterType == "elemhideexception")
+ stats.whitelisted++;
+ else if (filterType == "elemhide" || filterType == "elemhideemulation")
+ stats.hidden++;
+
+ if (filter in stats.filters)
+ stats.filters[filter]++;
+ else
+ stats.filters[filter] = 1;
+ }
+
+ // Notify listeners
+ for (let notifier of notifiers.values())
+ if (!notifier.window || notifier.window == topWnd)
+ notifier.notifyListener(node, entry);
+}
+
+/**
+ * Retrieves the statistics for a window.
+ * @return {Object} Object with the properties items, blocked, whitelisted, hidden, filters containing statistics for the window (might be null)
+ */
+RequestNotifier.getWindowStatistics = function(/**Window*/ wnd)
+{
+ if (windowStats.has(wnd.document))
+ return windowStats.get(wnd.document);
+ else
+ return null;
+}
+
+/**
+ * Retrieves the request data 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, Object]}
+ * @static
+ */
+RequestNotifier.getDataForNode = function(node, noParent, type, location)
+{
+ while (node)
+ {
+ let data = nodeData.get(node);
+ if (typeof data != "undefined")
+ {
+ let entry = null;
+ // Look for matching entry
+ for (let k in data)
+ {
+ if ((!entry || entry.id < data[k].id) &&
+ (typeof type == "undefined" || data[k].type == type) &&
+ (typeof location == "undefined" || data[k].location == location))
+ {
+ entry = data[k];
+ }
+ }
+ if (entry)
+ 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;
+};
diff --git a/data/extensions/spyblock@gnu.org/lib/child/subscribeLinks.js b/data/extensions/spyblock@gnu.org/lib/child/subscribeLinks.js
new file mode 100644
index 0000000..a2e729d
--- /dev/null
+++ b/data/extensions/spyblock@gnu.org/lib/child/subscribeLinks.js
@@ -0,0 +1,118 @@
+/*
+ * This file is part of Adblock Plus <https://adblockplus.org/>,
+ * Copyright (C) 2006-2017 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/>.
+ */
+
+let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
+
+let {port} = require("messaging");
+
+Services.obs.addObserver(onContentWindow, "content-document-global-created",
+ false);
+onShutdown.add(() =>
+{
+ Services.obs.removeObserver(onContentWindow,
+ "content-document-global-created");
+});
+
+function onContentWindow(subject, topic, data)
+{
+ if (subject instanceof Ci.nsIDOMWindow && subject.top == subject)
+ {
+ let eventTarget = subject.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShell)
+ .chromeEventHandler;
+ if (eventTarget)
+ eventTarget.addEventListener("click", onClick, true);
+ }
+}
+
+function onClick(event)
+{
+ if (onShutdown.done)
+ return;
+
+ // Ignore right-clicks
+ if (event.button == 2)
+ return;
+
+ // Search the link associated with the click
+ let link = event.target;
+ while (!(link instanceof Ci.nsIDOMHTMLAnchorElement))
+ {
+ link = link.parentNode;
+
+ if (!link)
+ return;
+ }
+
+ let queryString = null;
+ if (link.protocol == "http:" || link.protocol == "https:")
+ {
+ if (link.host == "subscribe.adblockplus.org" && link.pathname == "/")
+ queryString = link.search.substr(1);
+ }
+ else
+ {
+ // Firefox doesn't populate the "search" property for links with
+ // non-standard URL schemes so we need to extract the query string
+ // manually
+ let match = /^abp:\/*subscribe\/*\?(.*)/i.exec(link.href);
+ if (match)
+ queryString = match[1];
+ }
+
+ if (!queryString)
+ return;
+
+ // This is our link - make sure the browser doesn't handle it
+ event.preventDefault();
+ event.stopPropagation();
+
+ // Decode URL parameters
+ let title = null;
+ let url = null;
+ let mainSubscriptionTitle = null;
+ let mainSubscriptionURL = null;
+ for (let param of queryString.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;
+ }
+ }
+
+ port.emit("subscribeLinkClick", {
+ title: title,
+ url: url,
+ mainSubscriptionTitle: mainSubscriptionTitle,
+ mainSubscriptionURL: mainSubscriptionURL
+ });
+}
diff --git a/data/extensions/spyblock@gnu.org/lib/child/utils.js b/data/extensions/spyblock@gnu.org/lib/child/utils.js
new file mode 100644
index 0000000..fde649f
--- /dev/null
+++ b/data/extensions/spyblock@gnu.org/lib/child/utils.js
@@ -0,0 +1,141 @@
+/*
+ * This file is part of Adblock Plus <https://adblockplus.org/>,
+ * Copyright (C) 2006-2017 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/>.
+ */
+
+"use strict";
+
+let {PrivateBrowsingUtils} = Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm", {});
+
+let {Utils} = require("utils");
+
+/**
+ * Retrieves the effective location of a window.
+ */
+let getWindowLocation = exports.getWindowLocation = function(/**Window*/ window) /**String*/
+{
+ let result = null;
+
+ // Crazy Thunderbird stuff
+ if ("name" in window && window.name == "messagepane")
+ {
+ try
+ {
+ let mailWnd = window.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)
+ {
+ result = 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)
+ result = 'mailto:' + emailAddress.replace(/^[\s"]+/, "").replace(/[\s"]+$/, "").replace(/\s/g, '%20');
+ }
+ } catch(e) {}
+ }
+
+ // Sane branch
+ if (!result)
+ result = window.location.href;
+
+ // Remove the anchor if any
+ let index = result.indexOf("#");
+ if (index >= 0)
+ result = result.substring(0, index);
+
+ return result;
+}
+
+/**
+ * Retrieves the frame hierarchy for a window. Returns an array containing
+ * the information for all frames, starting with the window itself up to its
+ * top-level window. Each entry has a location and a sitekey entry.
+ * @return {Array}
+ */
+let getFrames = exports.getFrames = function(/**Window*/ window)
+{
+ let frames = [];
+ while (window)
+ {
+ let frame = {
+ location: getWindowLocation(window),
+ sitekey: null
+ };
+
+ let documentElement = window.document && window.document.documentElement;
+ if (documentElement)
+ frame.sitekey = documentElement.getAttribute("data-adblockkey")
+
+ frames.push(frame);
+ window = (window != window.parent ? window.parent : null);
+ }
+
+ // URLs like about:blank inherit their security context from upper-level
+ // frames, resolve their URLs accordingly.
+ for (let i = frames.length - 2; i >= 0; i--)
+ {
+ let frame = frames[i];
+ if (frame.location == "about:blank" || frame.location == "moz-safe-about:blank" ||
+ frame.location == "about:srcdoc" ||
+ Utils.netUtils.URIChainHasFlags(Utils.makeURI(frame.location), Ci.nsIProtocolHandler.URI_INHERITS_SECURITY_CONTEXT))
+ {
+ frame.location = frames[i + 1].location;
+ }
+ }
+
+ return frames;
+};
+
+/**
+ * Checks whether Private Browsing mode is enabled for a content window.
+ * @return {Boolean}
+ */
+let isPrivate = exports.isPrivate = function(/**Window*/ window)
+{
+ return PrivateBrowsingUtils.isContentWindowPrivate(window);
+};
+
+/**
+ * Gets the DOM window associated with a particular request (if any).
+ */
+let getRequestWindow = exports.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;
+};
diff --git a/data/extensions/spyblock@gnu.org/lib/common.js b/data/extensions/spyblock@gnu.org/lib/common.js
new file mode 100644
index 0000000..e2c2db5
--- /dev/null
+++ b/data/extensions/spyblock@gnu.org/lib/common.js
@@ -0,0 +1,53 @@
+/*
+ * This file is part of Adblock Plus <https://adblockplus.org/>,
+ * Copyright (C) 2006-2017 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/>.
+ */
+
+// We are currently limited to ECMAScript 5 in this file, because it is being
+// used in the browser tests. See https://issues.adblockplus.org/ticket/4796
+
+/**
+ * Converts filter text into regular expression string
+ * @param {string} text as in Filter()
+ * @return {string} regular expression representation of filter text
+ */
+function filterToRegExp(text)
+{
+ return text
+ // remove multiple wildcards
+ .replace(/\*+/g, "*")
+ // remove anchors following separator placeholder
+ .replace(/\^\|$/, "^")
+ // escape special symbols
+ .replace(/\W/g, "\\$&")
+ // replace wildcards by .*
+ .replace(/\\\*/g, ".*")
+ // process separator placeholders (all ANSI characters but alphanumeric
+ // characters and _%.-)
+ .replace(/\\\^/g, "(?:[\\x00-\\x24\\x26-\\x2C\\x2F\\x3A-\\x40\\x5B-\\x5E\\x60\\x7B-\\x7F]|$)")
+ // process extended anchor at expression start
+ .replace(/^\\\|\\\|/, "^[\\w\\-]+:\\/+(?!\\/)(?:[^\\/]+\\.)?")
+ // process anchor at expression start
+ .replace(/^\\\|/, "^")
+ // process anchor at expression end
+ .replace(/\\\|$/, "$")
+ // remove leading wildcards
+ .replace(/^(\.\*)/, "")
+ // remove trailing wildcards
+ .replace(/(\.\*)$/, "");
+}
+
+if (typeof exports != "undefined")
+ exports.filterToRegExp = filterToRegExp;
diff --git a/data/extensions/spyblock@gnu.org/lib/contentPolicy.js b/data/extensions/spyblock@gnu.org/lib/contentPolicy.js
index 4f2247e..ad36655 100644
--- a/data/extensions/spyblock@gnu.org/lib/contentPolicy.js
+++ b/data/extensions/spyblock@gnu.org/lib/contentPolicy.js
@@ -1,6 +1,6 @@
/*
* This file is part of Adblock Plus <https://adblockplus.org/>,
- * Copyright (C) 2006-2015 Eyeo GmbH
+ * Copyright (C) 2006-2017 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
@@ -19,115 +19,90 @@
* @fileOverview Content policy implementation, responsible for blocking things.
*/
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
+"use strict";
+
+let {XPCOMUtils} = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {});
+let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
let {Utils} = require("utils");
+let {port} = require("messaging");
let {Prefs} = require("prefs");
let {FilterStorage} = require("filterStorage");
-let {BlockingFilter, WhitelistFilter} = require("filterClasses");
+let {BlockingFilter, WhitelistFilter, RegExpFilter} = 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 =
+var 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
+ * Map of content types reported by Firefox to the respecitve content types
+ * used by Adblock Plus. Other content types are simply mapped to OTHER.
+ * @type Map.<string,string>
*/
- localizedDescr: {},
+ contentTypes: new Map(function* ()
+ {
+ // Treat navigator.sendBeacon() the same as <a ping>,
+ // it's essentially the same concept - merely generalized.
+ yield ["BEACON", "PING"];
+
+ // Treat <img srcset> and <picture> the same as other images.
+ yield ["IMAGESET", "IMAGE"];
+
+ // Treat fetch() the same as XMLHttpRequest,
+ // it's essentially the same - merely a more modern API.
+ yield ["FETCH", "XMLHTTPREQUEST"];
+
+ // Everything else is mapped to itself
+ for (let contentType of ["OTHER", "SCRIPT", "IMAGE", "STYLESHEET", "OBJECT",
+ "SUBDOCUMENT", "DOCUMENT", "XMLHTTPREQUEST",
+ "OBJECT_SUBREQUEST", "FONT", "MEDIA", "PING",
+ "WEBSOCKET", "ELEMHIDE", "POPUP", "GENERICHIDE",
+ "GENERICBLOCK"])
+ yield [contentType, contentType];
+ }()),
/**
- * Lists the non-visual content types.
- * @type Object
+ * Set of content types that aren't associated with a visual document area
+ * @type Set.<string>
*/
- nonVisual: {},
+ nonVisualTypes: new Set([
+ "SCRIPT", "STYLESHEET", "XMLHTTPREQUEST", "OBJECT_SUBREQUEST", "FONT",
+ "PING", "WEBSOCKET", "ELEMHIDE", "POPUP", "GENERICHIDE", "GENERICBLOCK"
+ ]),
/**
* Map containing all schemes that should be ignored by content policy.
- * @type Object
+ * @type Set.<string>
*/
- whitelistSchemes: {},
+ whitelistSchemes: new Set(),
/**
* Called on module startup, initializes various exported properties.
*/
init: function()
{
- // 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;
+ this.whitelistSchemes.add(scheme);
- // Generate class identifier used to collapse node and register corresponding
- // stylesheet.
+ port.on("shouldAllow", (message, sender) => this.shouldAllow(message));
+
+ // Generate class identifier used to collapse nodes and register
+ // corresponding stylesheet.
+ let collapsedClass = "";
let offset = "a".charCodeAt(0);
for (let i = 0; i < 20; i++)
collapsedClass += String.fromCharCode(offset + Math.random() * 26);
+ port.on("getCollapsedClass", (message, sender) => collapsedClass);
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()
+ onShutdown.add(() =>
{
Utils.styleService.unregisterSheet(collapseStyle, Ci.nsIStyleSheetService.USER_SHEET);
});
@@ -135,525 +110,197 @@ let Policy = exports.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
+ * @param {Object} data request data
+ * @param {String} data.contentType
+ * @param {String} data.location location of the request
+ * @param {Object[]} data.frames
+ * @param {Boolean} data.isPrivate true if the request belongs to a private browsing window
+ * @return {Object} An object containing properties allow, collapse and hits
+ * indicating how this request should be handled.
*/
- processNode: function(wnd, node, contentType, location, collapse)
+ shouldAllow: function({contentType, location, frames, isPrivate})
{
- 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.isContentWindowPrivate(wnd))
- privatenode=true;
+ let hits = [];
- let originWindow = Utils.getOriginWindow(wnd);
- let wndLocation = originWindow.location.href;
- let docDomain = getHostname(wndLocation);
- let match = null;
- let [sitekey, sitekeyWnd] = getSitekey(wnd);
- if (!match && Prefs.enabled)
+ function addHit(frameIndex, contentType, docDomain, thirdParty, location, filter)
{
- let testWnd = wnd;
- let testSitekey = sitekey;
- let testSitekeyWnd = sitekeyWnd;
- let parentWndLocation = getWindowLocation(testWnd);
- while (true)
- {
- let testWndLocation = parentWndLocation;
- parentWndLocation = (testWnd == testWnd.parent ? testWndLocation : getWindowLocation(testWnd.parent));
- match = Policy.isWhitelisted(testWndLocation, parentWndLocation, testSitekey);
+ if (filter && !isPrivate)
+ FilterStorage.increaseHitCount(filter);
+ hits.push({
+ frameIndex, contentType, docDomain, thirdParty, location,
+ filter: filter ? filter.text : null,
+ filterType: filter ? filter.type : null
+ });
+ }
- 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;
-
- if (testWnd == testSitekeyWnd)
- [testSitekey, testSitekeyWnd] = getSitekey(testWnd.parent);
- testWnd = testWnd.parent;
- }
+ function response(allow, collapse)
+ {
+ return {allow, collapse, hits};
}
- // Data loaded by plugins should be attached to the document
- if (contentType == Policy.type.OBJECT_SUBREQUEST && node instanceof Ci.nsIDOMElement)
- node = node.ownerDocument;
+ // Ignore whitelisted schemes
+ if (contentType != "POPUP" && !this.isBlockableScheme(location))
+ return response(true, false);
- // 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;
+ // Interpret unknown types as "other"
+ contentType = this.contentTypes.get(contentType) || "OTHER";
- let locationText = location.spec;
- if (!match && contentType == Policy.type.ELEMHIDE)
+ let nogeneric = false;
+ if (Prefs.enabled)
{
- let testWnd = wnd;
- let parentWndLocation = getWindowLocation(testWnd);
- while (true)
+ let whitelistHit =
+ this.isFrameWhitelisted(frames, false);
+ if (whitelistHit)
{
- let testWndLocation = parentWndLocation;
- parentWndLocation = (testWnd == testWnd.parent ? testWndLocation : getWindowLocation(testWnd.parent));
- let parentDocDomain = getHostname(parentWndLocation);
- match = defaultMatcher.matchesAny(testWndLocation, "ELEMHIDE", parentDocDomain, false, sitekey);
- 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;
+ let [frameIndex, matchType, docDomain, thirdParty, location, filter] = whitelistHit;
+ addHit(frameIndex, matchType, docDomain, thirdParty, location, filter);
+ if (matchType == "DOCUMENT")
+ return response(true, false);
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, false, locationText, exception);
- return true;
+ nogeneric = true;
}
}
- let thirdParty = (contentType == Policy.type.ELEMHIDE ? false : isThirdParty(location, docDomain));
+ let match = null;
+ let wndLocation = frames[0].location;
+ let docDomain = getHostname(wndLocation);
+ let [sitekey, sitekeyFrame] = getSitekey(frames);
+
+ let thirdParty = isThirdParty(location, docDomain);
+ let collapse = false;
- if (!match && Prefs.enabled)
+ if (!match && Prefs.enabled && RegExpFilter.typeMap.hasOwnProperty(contentType))
{
- match = defaultMatcher.matchesAny(locationText, Policy.typeDescr[contentType] || "", docDomain, thirdParty, sitekey, 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);
- }
+ match = defaultMatcher.matchesAny(location, RegExpFilter.typeMap[contentType],
+ docDomain, thirdParty, sitekey, nogeneric, isPrivate);
+ if (match instanceof BlockingFilter && !this.nonVisualTypes.has(contentType))
+ collapse = (match.collapse != null ? match.collapse : !Prefs.fastcollapse);
}
+ addHit(null, contentType, docDomain, thirdParty, location, match);
- // Store node data
- RequestNotifier.addNodeData(node, topWnd, contentType, docDomain, thirdParty, locationText, match);
- if (match)
- FilterStorage.increaseHitCount(match, wnd);
-
- return !match || match instanceof WhitelistFilter;
+ return response(!match || match instanceof WhitelistFilter, collapse);
},
/**
* Checks whether the location's scheme is blockable.
- * @param location {nsIURI}
+ * @param location {nsIURI|String}
* @return {Boolean}
*/
isBlockableScheme: function(location)
{
- return !(location.scheme in Policy.whitelistSchemes);
+ let scheme;
+ if (typeof location == "string")
+ {
+ let match = /^([\w\-]+):/.exec(location);
+ scheme = match ? match[1] : null;
+ }
+ else
+ scheme = location.scheme;
+ return !this.whitelistSchemes.has(scheme);
},
/**
- * Checks whether a page is whitelisted.
+ * Checks whether a top-level window is whitelisted.
* @param {String} url
- * @param {String} [parentUrl] location of the parent page
- * @param {String} [sitekey] public key provided on the page
- * @return {Filter} filter that matched the URL or null if not whitelisted
+ * URL of the document loaded into the window
+ * @return {?WhitelistFilter}
+ * exception rule that matched the URL if any
*/
- isWhitelisted: function(url, parentUrl, sitekey)
+ isWhitelisted: function(url)
{
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)
+ if (!this.isBlockableScheme(url))
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, sitekey);
+ let result = defaultMatcher.matchesAny(url, RegExpFilter.typeMap.DOCUMENT,
+ getHostname(url), false, null);
return (result instanceof WhitelistFilter ? result : null);
},
/**
- * Checks whether the page loaded in a window is whitelisted for indication in the UI.
- * @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.
- * @param {Node[]} nodes
- * @param {RequestEntry} entry
- */
- refilterNodes: function(nodes, 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.
+ * Checks whether a frame is whitelisted.
+ * @param {Array} frames
+ * frame structure as returned by getFrames() in child/utils module.
+ * @param {boolean} isElemHide
+ * true if element hiding whitelisting should be considered
+ * @return {?Array}
+ * An array with the hit parameters: frameIndex, contentType, docDomain,
+ * thirdParty, location, filter. Note that the filter could be a
+ * genericblock/generichide exception rule. If nothing matched null is
+ * returned.
*/
- 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)
+ isFrameWhitelisted: function(frames, isElemHide)
{
- 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;
- }
- }
- },
+ let [sitekey, sitekeyFrame] = getSitekey(frames);
+ let nogenericHit = null;
- //
- // nsIChannelEventSink interface implementation
- //
+ let typeMap = RegExpFilter.typeMap.DOCUMENT;
+ if (isElemHide)
+ typeMap = typeMap | RegExpFilter.typeMap.ELEMHIDE;
+ let genericType = (isElemHide ? "GENERICHIDE" : "GENERICBLOCK");
- asyncOnChannelRedirect: function(oldChannel, newChannel, flags, callback)
- {
- let result = Cr.NS_OK;
- try
+ for (let i = 0; i < frames.length; i++)
{
- // 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 frame = frames[i];
+ let wndLocation = frame.location;
+ let parentWndLocation = frames[Math.min(i + 1, frames.length - 1)].location;
+ let parentDocDomain = getHostname(parentWndLocation);
- 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)
+ let match = defaultMatcher.matchesAny(wndLocation, typeMap, parentDocDomain, false, sitekey);
+ if (match instanceof WhitelistFilter)
{
- // 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;
+ let whitelistType = (match.contentType & RegExpFilter.typeMap.DOCUMENT) ? "DOCUMENT" : "ELEMHIDE";
+ return [i, whitelistType, parentDocDomain, false, wndLocation, match];
}
- if (contentType == Policy.type.POPUP && wnd.opener)
+ if (!nogenericHit)
{
- // Popups are initiated by their opener, not their own window.
- wnd = wnd.opener;
+ match = defaultMatcher.matchesAny(wndLocation,
+ RegExpFilter.typeMap[genericType], parentDocDomain, false, sitekey);
+ if (match instanceof WhitelistFilter)
+ nogenericHit = [i, genericType, parentDocDomain, false, wndLocation, match];
}
- if (!Policy.processNode(wnd, wnd.document, contentType, newLocation, false))
- result = Cr.NS_BINDING_ABORTED;
+ if (frame == sitekeyFrame)
+ [sitekey, sitekeyFrame] = getSitekey(frames.slice(i + 1));
}
- 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;
+ return nogenericHit;
+ },
-/**
- * Schedules a node for post-processing.
- */
-function schedulePostProcess(/**Element*/ node)
-{
- if (scheduledNodes)
- scheduledNodes.push(node);
- else
+ /**
+ * Deletes nodes that were previously stored with a
+ * RequestNotifier.storeNodesForEntries() call or similar.
+ * @param {string} id unique ID of the nodes
+ */
+ deleteNodes: function(id)
{
- scheduledNodes = [node];
- Utils.runAsync(postProcessNodes);
- }
-}
-
-/**
- * Processes nodes scheduled for post-processing (typically hides them).
- */
-function postProcessNodes()
-{
- let nodes = scheduledNodes;
- scheduledNodes = null;
+ port.emit("deleteNodes", id);
+ },
- for (let node of nodes)
+ /**
+ * Asynchronously re-checks filters for nodes given by an ID previously
+ * returned by a RequestNotifier.storeNodesForEntries() call or similar.
+ * @param {string} id unique ID of the nodes
+ * @param {RequestEntry} entry
+ */
+ refilterNodes: function(id, entry)
{
- // 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);
+ port.emit("refilterNodes", {
+ nodesID: id,
+ entry: entry
+ });
}
-}
+};
+Policy.init();
/**
* Extracts the hostname from a URL (might return null).
@@ -671,44 +318,33 @@ function getHostname(/**String*/ url) /**String*/
}
/**
- * Retrieves the sitekey of a window.
+ * Retrieves and validates the sitekey for a frame structure.
*/
-function getSitekey(wnd)
+function getSitekey(frames)
{
- let sitekey = null;
-
- while (true)
+ for (let frame of frames)
{
- if (wnd.document && wnd.document.documentElement)
+ if (frame.sitekey && frame.sitekey.indexOf("_") >= 0)
{
- let keydata = wnd.document.documentElement.getAttribute("data-adblockkey");
- if (keydata && keydata.indexOf("_") >= 0)
- {
- let [key, signature] = keydata.split("_", 2);
- key = key.replace(/=/g, "");
-
- // Website specifies a key but is the signature valid?
- let uri = Services.io.newURI(getWindowLocation(wnd), null, null);
- let host = uri.asciiHost;
- if (uri.port > 0)
- host += ":" + uri.port;
- let params = [
- uri.path.replace(/#.*/, ""), // REQUEST_URI
- host, // HTTP_HOST
- Utils.httpProtocol.userAgent // HTTP_USER_AGENT
- ];
- if (Utils.verifySignature(key, signature, params.join("\0")))
- return [key, wnd];
- }
+ let [key, signature] = frame.sitekey.split("_", 2);
+ key = key.replace(/=/g, "");
+
+ // Website specifies a key but is the signature valid?
+ let uri = Services.io.newURI(frame.location, null, null);
+ let host = uri.asciiHost;
+ if (uri.port > 0)
+ host += ":" + uri.port;
+ let params = [
+ uri.path.replace(/#.*/, ""), // REQUEST_URI
+ host, // HTTP_HOST
+ Utils.httpProtocol.userAgent // HTTP_USER_AGENT
+ ];
+ if (Utils.verifySignature(key, signature, params.join("\0")))
+ return [key, frame];
}
-
- if (wnd === wnd.parent)
- break;
-
- wnd = wnd.parent;
}
- return [sitekey, wnd];
+ return [null, null];
}
/**
@@ -756,14 +392,15 @@ function getWindowLocation(wnd)
/**
* Checks whether the location's origin is different from document's origin.
*/
-function isThirdParty(/**nsIURI*/location, /**String*/ docDomain) /**Boolean*/
+function isThirdParty(/**String*/location, /**String*/ docDomain) /**Boolean*/
{
if (!location || !docDomain)
return true;
+ let uri = Utils.makeURI(location);
try
{
- return Utils.effectiveTLD.getBaseDomain(location) != Utils.effectiveTLD.getBaseDomainFromHost(docDomain);
+ return Utils.effectiveTLD.getBaseDomain(uri) != Utils.effectiveTLD.getBaseDomainFromHost(docDomain);
}
catch (e)
{
@@ -771,25 +408,8 @@ function isThirdParty(/**nsIURI*/location, /**String*/ docDomain) /**Boolean*/
let host = "";
try
{
- host = location.host;
+ host = uri.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/coreUtils.js b/data/extensions/spyblock@gnu.org/lib/coreUtils.js
new file mode 100644
index 0000000..98a1331
--- /dev/null
+++ b/data/extensions/spyblock@gnu.org/lib/coreUtils.js
@@ -0,0 +1,36 @@
+/*
+ * This file is part of Adblock Plus <https://adblockplus.org/>,
+ * Copyright (C) 2006-2017 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/>.
+ */
+
+"use strict";
+
+function desc(properties)
+{
+ let descriptor = {};
+ let keys = Object.keys(properties);
+
+ for (let key of keys)
+ descriptor[key] = Object.getOwnPropertyDescriptor(properties, key);
+
+ return descriptor;
+}
+exports.desc = desc;
+
+function extend(cls, properties)
+{
+ return Object.create(cls.prototype, desc(properties));
+}
+exports.extend = extend;
diff --git a/data/extensions/spyblock@gnu.org/lib/customizableUI.js b/data/extensions/spyblock@gnu.org/lib/customizableUI.js
index ee250fa..3874256 100644
--- a/data/extensions/spyblock@gnu.org/lib/customizableUI.js
+++ b/data/extensions/spyblock@gnu.org/lib/customizableUI.js
@@ -1,6 +1,6 @@
/*
* This file is part of Adblock Plus <https://adblockplus.org/>,
- * Copyright (C) 2006-2015 Eyeo GmbH
+ * Copyright (C) 2006-2017 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
@@ -26,7 +26,7 @@ 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();
+let widgets = new Map();
function getToolbox(/**Window*/ window, /**Widget*/ widget) /**Element*/
{
diff --git a/data/extensions/spyblock@gnu.org/lib/downloader.js b/data/extensions/spyblock@gnu.org/lib/downloader.js
index 320a754..fd760a7 100644
--- a/data/extensions/spyblock@gnu.org/lib/downloader.js
+++ b/data/extensions/spyblock@gnu.org/lib/downloader.js
@@ -1,6 +1,6 @@
/*
* This file is part of Adblock Plus <https://adblockplus.org/>,
- * Copyright (C) 2006-2015 Eyeo GmbH
+ * Copyright (C) 2006-2017 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
@@ -15,40 +15,46 @@
* along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>.
*/
+"use strict";
+
/**
* @fileOverview Downloads a set of URLs in regular time intervals.
*/
-let {Utils} = require("utils");
+const {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;
+const MILLIS_IN_SECOND = exports.MILLIS_IN_SECOND = 1000;
+const MILLIS_IN_MINUTE = exports.MILLIS_IN_MINUTE = 60 * MILLIS_IN_SECOND;
+const MILLIS_IN_HOUR = exports.MILLIS_IN_HOUR = 60 * MILLIS_IN_MINUTE;
+const MILLIS_IN_DAY = exports.MILLIS_IN_DAY = 24 * MILLIS_IN_HOUR;
+let Downloader =
/**
* 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
+ * @param {Function} dataSource
+ * Function that will yield downloadable objects on each check
+ * @param {number} initialDelay
+ * Number of milliseconds to wait before the first check
+ * @param {number} checkInterval
+ * Interval between the checks
* @constructor
*/
-let Downloader = exports.Downloader = function Downloader(dataSource, initialDelay, checkInterval)
+exports.Downloader = function(dataSource, initialDelay, checkInterval)
{
this.dataSource = dataSource;
this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
- this._timer.initWithCallback(function()
+ this._timer.initWithCallback(() =>
{
this._timer.delay = checkInterval;
this._doCheck();
- }.bind(this), initialDelay, Ci.nsITimer.TYPE_REPEATING_SLACK);
+ }, initialDelay, Ci.nsITimer.TYPE_REPEATING_SLACK);
this._downloading = Object.create(null);
-}
+};
Downloader.prototype =
{
/**
* Timer triggering the downloads.
- * @type nsITimer
+ * @type {nsITimer}
*/
_timer: null,
@@ -59,74 +65,75 @@ Downloader.prototype =
/**
* Function that will yield downloadable objects on each check.
- * @type Function
+ * @type {Function}
*/
dataSource: null,
/**
* Maximal time interval that the checks can be left out until the soft
* expiration interval increases.
- * @type Integer
+ * @type {number}
*/
maxAbsenceInterval: 1 * MILLIS_IN_DAY,
/**
* Minimal time interval before retrying a download after an error.
- * @type Integer
+ * @type {number}
*/
minRetryInterval: 1 * MILLIS_IN_DAY,
/**
* Maximal allowed expiration interval, larger expiration intervals will be
* corrected.
- * @type Integer
+ * @type {number}
*/
maxExpirationInterval: 14 * MILLIS_IN_DAY,
/**
* Maximal number of redirects before the download is considered as failed.
- * @type Integer
+ * @type {number}
*/
maxRedirects: 5,
/**
* Called whenever expiration intervals for an object need to be adapted.
- * @type Function
+ * @type {Function}
*/
onExpirationChange: null,
/**
* Callback to be triggered whenever a download starts.
- * @type Function
+ * @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
+ * @type {Function}
*/
onDownloadSuccess: null,
/**
* Callback to be triggered whenever a download fails.
- * @type Function
+ * @type {Function}
*/
onDownloadError: null,
/**
* Checks whether anything needs downloading.
*/
- _doCheck: function()
+ _doCheck()
{
let now = Date.now();
for (let downloadable of this.dataSource())
{
- if (downloadable.lastCheck && now - downloadable.lastCheck > this.maxAbsenceInterval)
+ 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.
+ // 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;
@@ -143,12 +150,18 @@ Downloader.prototype =
this.onExpirationChange(downloadable);
// Does that object need downloading?
- if (downloadable.softExpiration > now && downloadable.hardExpiration > now)
+ if (downloadable.softExpiration > now &&
+ downloadable.hardExpiration > now)
+ {
continue;
+ }
// Do not retry downloads too often
- if (downloadable.lastError && now - downloadable.lastError < this.minRetryInterval)
+ if (downloadable.lastError &&
+ now - downloadable.lastError < this.minRetryInterval)
+ {
continue;
+ }
this._download(downloadable, 0);
}
@@ -157,23 +170,26 @@ Downloader.prototype =
/**
* Stops the periodic checks.
*/
- cancel: function()
+ cancel()
{
this._timer.cancel();
},
/**
* Checks whether an address is currently being downloaded.
+ * @param {string} url
+ * @return {boolean}
*/
- isDownloading: function(/**String*/ url) /**Boolean*/
+ isDownloading(url)
{
return url in this._downloading;
},
/**
* Starts downloading for an object.
+ * @param {Downloadable} downloadable
*/
- download: function(/**Downloadable*/ downloadable)
+ download(downloadable)
{
// Make sure to detach download from the current execution context
Utils.runAsync(this._download.bind(this, downloadable, 0));
@@ -182,17 +198,20 @@ Downloader.prototype =
/**
* Generates the real download URL for an object by appending various
* parameters.
+ * @param {Downloadable} downloadable
+ * @return {string}
*/
- getDownloadUrl: function(/**Downloadable*/ downloadable) /** String*/
+ getDownloadUrl(downloadable)
{
- let {addonName, addonVersion, application, applicationVersion, platform, platformVersion} = require("info");
+ const {addonName, addonVersion, application, applicationVersion,
+ platform, platformVersion} = require("info");
let url = downloadable.redirectURL || downloadable.url;
if (url.indexOf("?") >= 0)
url += "&";
else
url += "?";
// We limit the download count to 4+ to keep the request anonymized
- let downloadCount = downloadable.downloadCount;
+ let {downloadCount} = downloadable;
if (downloadCount > 4)
downloadCount = "4+";
url += "addonName=" + encodeURIComponent(addonName) +
@@ -206,7 +225,7 @@ Downloader.prototype =
return url;
},
- _download: function(downloadable, redirects)
+ _download(downloadable, redirects)
{
if (this.isDownloading(downloadable.url))
return;
@@ -220,11 +239,13 @@ Downloader.prototype =
try
{
channelStatus = request.channel.status;
- } catch (e) {}
+ }
+ catch (e) {}
let responseStatus = request.status;
- Cu.reportError("Adblock Plus: Downloading URL " + downloadable.url + " failed (" + error + ")\n" +
+ Cu.reportError("Adblock Plus: Downloading URL " + downloadable.url +
+ " failed (" + error + ")\n" +
"Download address: " + downloadUrl + "\n" +
"Channel status: " + channelStatus + "\n" +
"Server response: " + responseStatus);
@@ -235,14 +256,15 @@ Downloader.prototype =
let redirectCallback = null;
if (redirects <= this.maxRedirects)
{
- redirectCallback = function redirectCallback(url)
+ redirectCallback = url =>
{
downloadable.redirectURL = url;
this._download(downloadable, redirects + 1);
- }.bind(this);
+ };
}
- this.onDownloadError(downloadable, downloadUrl, error, channelStatus, responseStatus, redirectCallback);
+ this.onDownloadError(downloadable, downloadUrl, error, channelStatus,
+ responseStatus, redirectCallback);
}
}.bind(this);
@@ -258,7 +280,8 @@ Downloader.prototype =
return;
}
- try {
+ try
+ {
request.overrideMimeType("text/plain");
request.channel.loadFlags = request.channel.loadFlags |
request.channel.INHIBIT_CACHING |
@@ -270,19 +293,19 @@ Downloader.prototype =
}
catch (e)
{
- Cu.reportError(e)
+ Cu.reportError(e);
}
- request.addEventListener("error", function(event)
+ request.addEventListener("error", event =>
{
if (onShutdown.done)
return;
delete this._downloading[downloadable.url];
errorCallback("synchronize_connection_error");
- }.bind(this), false);
+ }, false);
- request.addEventListener("load", function(event)
+ request.addEventListener("load", event =>
{
if (onShutdown.done)
return;
@@ -298,17 +321,20 @@ Downloader.prototype =
downloadable.downloadCount++;
- this.onDownloadSuccess(downloadable, request.responseText, errorCallback, function redirectCallback(url)
- {
- if (redirects >= this.maxRedirects)
- errorCallback("synchronize_connection_error");
- else
+ this.onDownloadSuccess(
+ downloadable, request.responseText, errorCallback,
+ url =>
{
- downloadable.redirectURL = url;
- this._download(downloadable, redirects + 1);
+ 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);
@@ -320,9 +346,10 @@ Downloader.prototype =
/**
* Produces a soft and a hard expiration interval for a given supplied
* expiration interval.
+ * @param {number} interval
* @return {Array} soft and hard expiration interval
*/
- processExpirationInterval: function(/**Integer*/ interval)
+ processExpirationInterval(interval)
{
interval = Math.min(Math.max(interval, 0), this.maxExpirationInterval);
let soft = Math.round(interval * (Math.random() * 0.4 + 0.8));
@@ -334,61 +361,61 @@ Downloader.prototype =
/**
* An object that can be downloaded by the downloadable
- * @param {String} url URL that has to be requested for the object
+ * @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
+ * @type {string}
*/
url: null,
/**
* URL that the download was redirected to if any.
- * @type String
+ * @type {string}
*/
redirectURL: null,
/**
* Time of last download error or 0 if the last download was successful.
- * @type Integer
+ * @type {number}
*/
lastError: 0,
/**
* Time of last check whether the object needs downloading.
- * @type Integer
+ * @type {number}
*/
lastCheck: 0,
/**
* Object version corresponding to the last successful download.
- * @type Integer
+ * @type {number}
*/
lastVersion: 0,
/**
* Soft expiration interval, will increase if no checks are performed for a
* while.
- * @type Integer
+ * @type {number}
*/
softExpiration: 0,
/**
* Hard expiration interval, this is fixed.
- * @type Integer
+ * @type {number}
*/
hardExpiration: 0,
-
+
/**
* Number indicating how often the object was downloaded.
- * @type Integer
+ * @type {number}
*/
- downloadCount: 0,
+ downloadCount: 0
};
diff --git a/data/extensions/spyblock@gnu.org/lib/elemHide.js b/data/extensions/spyblock@gnu.org/lib/elemHide.js
index b762040..a91f1d4 100644
--- a/data/extensions/spyblock@gnu.org/lib/elemHide.js
+++ b/data/extensions/spyblock@gnu.org/lib/elemHide.js
@@ -1,6 +1,6 @@
/*
* This file is part of Adblock Plus <https://adblockplus.org/>,
- * Copyright (C) 2006-2015 Eyeo GmbH
+ * Copyright (C) 2006-2017 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
@@ -15,115 +15,135 @@
* along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>.
*/
+"use strict";
+
/**
* @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");
+const {ElemHideException} = require("filterClasses");
+const {FilterNotifier} = require("filterNotifier");
/**
* Lookup table, filters by their associated key
- * @type Object
+ * @type {Object}
*/
-let filterByKey = Object.create(null);
+let filterByKey = [];
/**
* Lookup table, keys of the filters by filter text
- * @type Object
+ * @type {Object}
*/
let keyByFilter = Object.create(null);
/**
- * Lookup table, keys are known element hiding exceptions
- * @type Object
+ * Nested lookup table, filter (or false if inactive) by filter key by domain.
+ * (Only contains filters that aren't unconditionally matched for all domains.)
+ * @type {Object}
*/
-let knownExceptions = Object.create(null);
+let filtersByDomain = Object.create(null);
/**
- * Lookup table, lists of element hiding exceptions by selector
- * @type Object
+ * Lookup table, filter key by selector. (Only used for selectors that are
+ * unconditionally matched for all domains.)
*/
-let exceptions = Object.create(null);
+let filterKeyBySelector = Object.create(null);
/**
- * Currently applied stylesheet URL
- * @type nsIURI
+ * This array caches the keys of filterKeyBySelector table (selectors which
+ * unconditionally apply on all domains). It will be null if the cache needs to
+ * be rebuilt.
*/
-let styleURL = null;
+let unconditionalSelectors = null;
/**
- * Element hiding component
- * @class
+ * This array caches the values of filterKeyBySelector table (filterIds for
+ * selectors which unconditionally apply on all domains). It will be null if the
+ * cache needs to be rebuilt.
*/
-let ElemHide = exports.ElemHide =
-{
- /**
- * Indicates whether filters have been added or removed since the last apply() call.
- * @type Boolean
- */
- isDirty: false,
+let unconditionalFilterKeys = null;
- /**
- * Inidicates whether the element hiding stylesheet is currently applied.
- * @type Boolean
- */
- applied: false,
+/**
+ * Object to be used instead when a filter has a blank domains property.
+ */
+let defaultDomains = Object.create(null);
+defaultDomains[""] = true;
- /**
- * Called on module startup.
- */
- init: function()
- {
- Prefs.addListener(function(name)
- {
- if (name == "enabled")
- ElemHide.apply();
- });
- onShutdown.add(function()
- {
- ElemHide.unapply();
- });
+/**
+ * Lookup table, keys are known element hiding exceptions
+ * @type {Object}
+ */
+let knownExceptions = Object.create(null);
- let styleFile = IO.resolveFilePath(Prefs.data_directory);
- styleFile.append("elemhide.css");
- styleURL = Services.io.newFileURI(styleFile).QueryInterface(Ci.nsIFileURL);
- },
+/**
+ * Lookup table, lists of element hiding exceptions by selector
+ * @type {Object}
+ */
+let exceptions = Object.create(null);
+/**
+ * Container for element hiding filters
+ * @class
+ */
+let ElemHide = exports.ElemHide = {
/**
* Removes all known filters
*/
- clear: function()
+ clear()
{
- filterByKey = Object.create(null);
+ filterByKey = [];
keyByFilter = Object.create(null);
+ filtersByDomain = Object.create(null);
+ filterKeyBySelector = Object.create(null);
+ unconditionalSelectors = unconditionalFilterKeys = null;
knownExceptions = Object.create(null);
exceptions = Object.create(null);
- ElemHide.isDirty = false;
- ElemHide.unapply();
+ FilterNotifier.emit("elemhideupdate");
+ },
+
+ _addToFiltersByDomain(key, filter)
+ {
+ let domains = filter.domains || defaultDomains;
+ for (let domain in domains)
+ {
+ let filters = filtersByDomain[domain];
+ if (!filters)
+ filters = filtersByDomain[domain] = Object.create(null);
+
+ if (domains[domain])
+ filters[key] = filter;
+ else
+ filters[key] = false;
+ }
},
/**
* Add a new element hiding filter
* @param {ElemHideFilter} filter
*/
- add: function(filter)
+ add(filter)
{
if (filter instanceof ElemHideException)
{
if (filter.text in knownExceptions)
return;
- let selector = filter.selector;
+ let {selector} = filter;
if (!(selector in exceptions))
exceptions[selector] = [];
exceptions[selector].push(filter);
+
+ // If this is the first exception for a previously unconditionally
+ // applied element hiding selector we need to take care to update the
+ // lookups.
+ let filterKey = filterKeyBySelector[selector];
+ if (typeof filterKey != "undefined")
+ {
+ this._addToFiltersByDomain(filterKey, filterByKey[filterKey]);
+ delete filterKeyBySelector[selector];
+ unconditionalSelectors = unconditionalFilterKeys = null;
+ }
+
knownExceptions[filter.text] = true;
}
else
@@ -131,14 +151,42 @@ let ElemHide = exports.ElemHide =
if (filter.text in keyByFilter)
return;
- let key;
- do {
- key = Math.random().toFixed(15).substr(5);
- } while (key in filterByKey);
-
- filterByKey[key] = filter;
+ let key = filterByKey.push(filter) - 1;
keyByFilter[filter.text] = key;
- ElemHide.isDirty = true;
+
+ if (!(filter.domains || filter.selector in exceptions))
+ {
+ // The new filter's selector is unconditionally applied to all domains
+ filterKeyBySelector[filter.selector] = key;
+ unconditionalSelectors = unconditionalFilterKeys = null;
+ }
+ else
+ {
+ // The new filter's selector only applies to some domains
+ this._addToFiltersByDomain(key, filter);
+ }
+ }
+
+ FilterNotifier.emit("elemhideupdate");
+ },
+
+ _removeFilterKey(key, filter)
+ {
+ if (filterKeyBySelector[filter.selector] == key)
+ {
+ delete filterKeyBySelector[filter.selector];
+ unconditionalSelectors = unconditionalFilterKeys = null;
+ return;
+ }
+
+ // We haven't found this filter in unconditional filters, look in
+ // filtersByDomain.
+ let domains = filter.domains || defaultDomains;
+ for (let domain in domains)
+ {
+ let filters = filtersByDomain[domain];
+ if (filters)
+ delete filters[key];
}
},
@@ -146,7 +194,7 @@ let ElemHide = exports.ElemHide =
* Removes an element hiding filter
* @param {ElemHideFilter} filter
*/
- remove: function(filter)
+ remove(filter)
{
if (filter instanceof ElemHideException)
{
@@ -167,226 +215,182 @@ let ElemHide = exports.ElemHide =
let key = keyByFilter[filter.text];
delete filterByKey[key];
delete keyByFilter[filter.text];
- ElemHide.isDirty = true;
+ this._removeFilterKey(key, filter);
}
+
+ FilterNotifier.emit("elemhideupdate");
},
/**
* Checks whether an exception rule is registered for a filter on a particular
* domain.
+ * @param {Filter} filter
+ * @param {string} docDomain
+ * @return {ElemHideException}
*/
- getException: function(/**Filter*/ filter, /**String*/ docDomain) /**ElemHideException*/
+ getException(filter, docDomain)
{
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
+ * Retrieves an element hiding filter by the corresponding protocol key
+ * @param {number} key
+ * @return {Filter}
*/
- apply: function()
+ getFilterByKey(key)
{
- if (this._applying)
- {
- this._needsApply = true;
- return;
- }
-
- 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);
- }
- }
- else if (!Prefs.enabled && ElemHide.applied)
- {
- ElemHide.unapply();
- }
-
- return;
- }
-
- IO.writeToFile(styleURL.file, this._generateCSSContent(), function(e)
- {
- 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();
-
- if (!noFilters)
- {
- try
- {
- Utils.styleService.loadAndRegisterSheet(styleURL, Ci.nsIStyleSheetService.USER_SHEET);
- ElemHide.applied = true;
- }
- catch (e)
- {
- Cu.reportError(e);
- }
- }
-
- FilterNotifier.triggerListeners("elemhideupdate");
- }
- }.bind(this));
-
- this._applying = true;
+ return (key in filterByKey ? filterByKey[key] : null);
},
- _generateCSSContent: function()
+ /**
+ * Returns a list of all selectors as a nested map. On first level, the keys
+ * are all values of `ElemHideBase.selectorDomain` (domains on which these
+ * selectors should apply, ignoring exceptions). The values are maps again,
+ * with the keys being selectors and values the corresponding filter keys.
+ * @returns {Map.<String,Map<String,String>>}
+ */
+ getSelectors()
{
- // Grouping selectors by domains
- let domains = Object.create(null);
- let hasFilters = false;
+ let domains = new Map();
for (let key in filterByKey)
{
let filter = filterByKey[key];
- let domain = filter.selectorDomain || "";
-
- let list;
- if (domain in domains)
- list = domains[domain];
- else
- {
- list = Object.create(null);
- domains[domain] = list;
- }
- list[filter.selector] = key;
- hasFilters = true;
- }
+ if (!filter.selector)
+ continue;
- if (!hasFilters)
- throw Cr.NS_ERROR_NOT_AVAILABLE;
+ let domain = filter.selectorDomain || "";
- function escapeChar(match)
- {
- return "\\" + match.charCodeAt(0).toString(16) + " ";
+ if (!domains.has(domain))
+ domains.set(domain, new Map());
+ domains.get(domain).set(filter.selector, key);
}
- // 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://"){';
- }
+ return domains;
+ },
- for (let selector in list)
- yield selector.replace(/[^\x01-\x7F]/g, escapeChar) + "{" + cssTemplate.replace("%ID%", list[selector]) + "}";
- yield '}';
- }
+ /**
+ * Returns a list of selectors that apply on each website unconditionally.
+ * @returns {string[]}
+ */
+ getUnconditionalSelectors()
+ {
+ if (!unconditionalSelectors)
+ unconditionalSelectors = Object.keys(filterKeyBySelector);
+ return unconditionalSelectors.slice();
},
/**
- * Unapplies current stylesheet URL
+ * Returns a list of filter keys for selectors which apply to all websites
+ * without exception.
+ * @returns {number[]}
*/
- unapply: function()
+ getUnconditionalFilterKeys()
{
- if (ElemHide.applied)
+ if (!unconditionalFilterKeys)
{
- try
- {
- Utils.styleService.unregisterSheet(styleURL, Ci.nsIStyleSheetService.USER_SHEET);
- }
- catch (e)
- {
- Cu.reportError(e);
- }
- ElemHide.applied = false;
+ let selectors = this.getUnconditionalSelectors();
+ unconditionalFilterKeys = [];
+ for (let selector of selectors)
+ unconditionalFilterKeys.push(filterKeyBySelector[selector]);
}
+ return unconditionalFilterKeys.slice();
},
+
/**
- * Retrieves the currently applied stylesheet URL
- * @type String
+ * Constant used by getSelectorsForDomain to return all selectors applying to
+ * a particular hostname.
*/
- get styleURL()
- {
- return ElemHide.applied ? styleURL.spec : null;
- },
+ ALL_MATCHING: 0,
/**
- * Retrieves an element hiding filter by the corresponding protocol key
+ * Constant used by getSelectorsForDomain to exclude selectors which apply to
+ * all websites without exception.
*/
- getFilterByKey: function(/**String*/ key) /**Filter*/
- {
- return (key in filterByKey ? filterByKey[key] : null);
- },
+ NO_UNCONDITIONAL: 1,
/**
- * Returns a list of all selectors active on a particular domain (currently
- * used only in Chrome, Opera and Safari).
+ * Constant used by getSelectorsForDomain to return only selectors for filters
+ * which specifically match the given host name.
*/
- getSelectorsForDomain: function(/**String*/ domain, /**Boolean*/ specificOnly)
+ SPECIFIC_ONLY: 2,
+
+ /**
+ * Determines from the current filter list which selectors should be applied
+ * on a particular host name. Optionally returns the corresponding filter
+ * keys.
+ * @param {string} domain
+ * @param {number} [criteria]
+ * One of the following: ElemHide.ALL_MATCHING, ElemHide.NO_UNCONDITIONAL or
+ * ElemHide.SPECIFIC_ONLY.
+ * @param {boolean} [provideFilterKeys]
+ * If true, the function will return a list of corresponding filter keys in
+ * addition to selectors.
+ * @returns {string[]|Array.<string[]>}
+ * List of selectors or an array with two elements (list of selectors and
+ * list of corresponding keys) if provideFilterKeys is true.
+ */
+ getSelectorsForDomain(domain, criteria, provideFilterKeys)
{
- let result = [];
- for (let key in filterByKey)
+ let filterKeys = [];
+ let selectors = [];
+
+ if (typeof criteria == "undefined")
+ criteria = ElemHide.ALL_MATCHING;
+ if (criteria < ElemHide.NO_UNCONDITIONAL)
{
- let filter = filterByKey[key];
- if (specificOnly && (!filter.domains || filter.domains[""]))
- continue;
+ selectors = this.getUnconditionalSelectors();
+ if (provideFilterKeys)
+ filterKeys = this.getUnconditionalFilterKeys();
+ }
+
+ let specificOnly = (criteria >= ElemHide.SPECIFIC_ONLY);
+ let seenFilters = Object.create(null);
+ let currentDomain = domain ? domain.toUpperCase() : "";
+ while (true)
+ {
+ if (specificOnly && currentDomain == "")
+ break;
- if (filter.isActiveOnDomain(domain) && !this.getException(filter, domain))
- result.push(filter.selector);
+ let filters = filtersByDomain[currentDomain];
+ if (filters)
+ {
+ for (let filterKey in filters)
+ {
+ if (filterKey in seenFilters)
+ continue;
+ seenFilters[filterKey] = true;
+
+ let filter = filters[filterKey];
+ if (filter && !this.getException(filter, domain))
+ {
+ selectors.push(filter.selector);
+ // It is faster to always push the key, even if not required.
+ filterKeys.push(filterKey);
+ }
+ }
+ }
+
+ if (currentDomain == "")
+ break;
+
+ let nextDot = currentDomain.indexOf(".");
+ currentDomain = nextDot == -1 ? "" : currentDomain.substr(nextDot + 1);
}
- return result;
+
+ if (provideFilterKeys)
+ return [selectors, filterKeys];
+ return selectors;
}
};
diff --git a/data/extensions/spyblock@gnu.org/lib/elemHideEmulation.js b/data/extensions/spyblock@gnu.org/lib/elemHideEmulation.js
new file mode 100644
index 0000000..edf2082
--- /dev/null
+++ b/data/extensions/spyblock@gnu.org/lib/elemHideEmulation.js
@@ -0,0 +1,81 @@
+/*
+ * This file is part of Adblock Plus <https://adblockplus.org/>,
+ * Copyright (C) 2006-2017 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/>.
+ */
+
+"use strict";
+
+/**
+ * @fileOverview Element hiding emulation implementation.
+ */
+
+const {ElemHide} = require("elemHide");
+const {Filter} = require("filterClasses");
+
+let filters = Object.create(null);
+
+/**
+ * Container for element hiding emulation filters
+ * @class
+ */
+let ElemHideEmulation = {
+ /**
+ * Removes all known filters
+ */
+ clear()
+ {
+ filters = Object.create(null);
+ },
+
+ /**
+ * Add a new element hiding emulation filter
+ * @param {ElemHideEmulationFilter} filter
+ */
+ add(filter)
+ {
+ filters[filter.text] = true;
+ },
+
+ /**
+ * Removes an element hiding emulation filter
+ * @param {ElemHideEmulationFilter} filter
+ */
+ remove(filter)
+ {
+ delete filters[filter.text];
+ },
+
+ /**
+ * Returns a list of all rules active on a particular domain
+ * @param {string} domain
+ * @return {ElemHideEmulationFilter[]}
+ */
+ getRulesForDomain(domain)
+ {
+ let result = [];
+ let keys = Object.getOwnPropertyNames(filters);
+ for (let key of keys)
+ {
+ let filter = Filter.fromText(key);
+ if (filter.isActiveOnDomain(domain) &&
+ !ElemHide.getException(filter, domain))
+ {
+ result.push(filter);
+ }
+ }
+ return result;
+ }
+};
+exports.ElemHideEmulation = ElemHideEmulation;
diff --git a/data/extensions/spyblock@gnu.org/lib/elemHideFF.js b/data/extensions/spyblock@gnu.org/lib/elemHideFF.js
new file mode 100644
index 0000000..fe42e11
--- /dev/null
+++ b/data/extensions/spyblock@gnu.org/lib/elemHideFF.js
@@ -0,0 +1,106 @@
+/*
+ * This file is part of Adblock Plus <https://adblockplus.org/>,
+ * Copyright (C) 2006-2017 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/>.
+ */
+
+"use strict";
+
+let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
+
+let {port} = require("messaging");
+let {ElemHide} = require("elemHide");
+let {FilterNotifier} = require("filterNotifier");
+let {FilterStorage} = require("filterStorage");
+let {Prefs} = require("prefs");
+let {Policy} = require("contentPolicy");
+let {Utils} = require("utils");
+
+let isDirty = false;
+FilterNotifier.on("elemhideupdate", () =>
+{
+ // Notify content process asynchronously, only one message per update batch.
+ if (!isDirty)
+ {
+ isDirty = true;
+ Utils.runAsync(() => {
+ isDirty = false;
+ port.emit("elemhideupdate")
+ });
+ }
+});
+
+port.on("getUnconditionalSelectors", () =>
+{
+ return [
+ ElemHide.getUnconditionalSelectors(),
+ ElemHide.getUnconditionalFilterKeys()
+ ];
+});
+
+port.on("getSelectorsForDomain", ([domain, specificOnly]) =>
+{
+ let type = specificOnly ? ElemHide.SPECIFIC_ONLY : ElemHide.NO_UNCONDITIONAL;
+ return ElemHide.getSelectorsForDomain(domain, type, true);
+});
+
+port.on("elemhideEnabled", ({frames, isPrivate}) =>
+{
+ if (!Prefs.enabled || !Policy.isBlockableScheme(frames[0].location))
+ return {enabled: false};
+
+ let hit = Policy.isFrameWhitelisted(frames, true);
+ if (hit)
+ {
+ let [frameIndex, contentType, docDomain, thirdParty, location, filter] = hit;
+ if (!isPrivate)
+ FilterStorage.increaseHitCount(filter);
+ return {
+ enabled: contentType == "GENERICHIDE",
+ contentType, docDomain, thirdParty, location,
+ filter: filter.text, filterType: filter.type
+ };
+ }
+
+ return {enabled: true};
+});
+
+port.on("registerElemHideHit", ({key, frames, isPrivate}) =>
+{
+ let filter = ElemHide.getFilterByKey(key);
+ if (!filter)
+ return null;
+
+ if (!isPrivate)
+ FilterStorage.increaseHitCount(filter);
+
+ let docDomain;
+ try
+ {
+ docDomain = Utils.unwrapURL(frames[0].location).host;
+ }
+ catch(e)
+ {
+ docDomain = null;
+ }
+
+ return {
+ contentType: "ELEMHIDE",
+ docDomain,
+ thirdParty: false,
+ location: filter.text.replace(/^.*?#/, '#'),
+ filter: filter.text,
+ filterType: filter.type
+ };
+});
diff --git a/data/extensions/spyblock@gnu.org/lib/elemHideHitRegistration.js b/data/extensions/spyblock@gnu.org/lib/elemHideHitRegistration.js
deleted file mode 100644
index 74661a9..0000000
--- a/data/extensions/spyblock@gnu.org/lib/elemHideHitRegistration.js
+++ /dev/null
@@ -1,164 +0,0 @@
-/*
- * This file is part of Adblock Plus <https://adblockplus.org/>,
- * Copyright (C) 2006-2015 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");
-
- // This dummy binding below won't have any effect on the element. For
- // elements that should be hidden however we don't return any binding at
- // all, this makes Gecko stop constructing the node - it cannot be shown.
- let data = "<bindings xmlns='http://www.mozilla.org/xbl'><binding id='dummy' bindToUntrustedContent='true'/></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/events.js b/data/extensions/spyblock@gnu.org/lib/events.js
new file mode 100644
index 0000000..8d11f7c
--- /dev/null
+++ b/data/extensions/spyblock@gnu.org/lib/events.js
@@ -0,0 +1,106 @@
+/*
+ * This file is part of Adblock Plus <https://adblockplus.org/>,
+ * Copyright (C) 2006-2017 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/>.
+ */
+
+"use strict";
+
+/**
+ * Registers and emits named events.
+ *
+ * @constructor
+ */
+exports.EventEmitter = function()
+{
+ this._listeners = Object.create(null);
+};
+
+exports.EventEmitter.prototype = {
+ /**
+ * Adds a listener for the specified event name.
+ *
+ * @param {string} name
+ * @param {function} listener
+ */
+ on(name, listener)
+ {
+ if (name in this._listeners)
+ this._listeners[name].push(listener);
+ else
+ this._listeners[name] = [listener];
+ },
+
+ /**
+ * Removes a listener for the specified event name.
+ *
+ * @param {string} name
+ * @param {function} listener
+ */
+ off(name, listener)
+ {
+ let listeners = this._listeners[name];
+ if (listeners)
+ {
+ let idx = listeners.indexOf(listener);
+ if (idx != -1)
+ listeners.splice(idx, 1);
+ }
+ },
+
+ /**
+ * Adds a one time listener and returns a promise that
+ * is resolved the next time the specified event is emitted.
+ * @param {string} name
+ * @return {Promise}
+ */
+ once(name)
+ {
+ return new Promise(resolve =>
+ {
+ let listener = () =>
+ {
+ this.off(name, listener);
+ resolve();
+ };
+
+ this.on(name, listener);
+ });
+ },
+
+ /**
+ * Returns a copy of the array of listeners for the specified event.
+ *
+ * @param {string} name
+ * @return {function[]}
+ */
+ listeners(name)
+ {
+ let listeners = this._listeners[name];
+ return listeners ? listeners.slice() : [];
+ },
+
+ /**
+ * Calls all previously added listeners for the given event name.
+ *
+ * @param {string} name
+ * @param {...*} [arg]
+ */
+ emit(name, ...args)
+ {
+ let listeners = this.listeners(name);
+ for (let listener of listeners)
+ listener(...args);
+ }
+};
diff --git a/data/extensions/spyblock@gnu.org/lib/ext_background.js b/data/extensions/spyblock@gnu.org/lib/ext_background.js
index 2a50142..b57f96c 100644
--- a/data/extensions/spyblock@gnu.org/lib/ext_background.js
+++ b/data/extensions/spyblock@gnu.org/lib/ext_background.js
@@ -1,6 +1,6 @@
/*
* This file is part of Adblock Plus <https://adblockplus.org/>,
- * Copyright (C) 2006-2015 Eyeo GmbH
+ * Copyright (C) 2006-2017 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
@@ -17,31 +17,21 @@
let {XPCOMUtils} = Cu.import("resource://gre/modules/XPCOMUtils.jsm", null);
let {Services} = Cu.import("resource://gre/modules/Services.jsm", null);
-let {
- _MessageProxy: MessageProxy,
- _EventTarget: EventTarget,
- _getSender: getSender
-} = require("ext_common");
-exports.onMessage = new EventTarget();
-let messageProxy = new MessageProxy(
- Cc["@mozilla.org/globalmessagemanager;1"]
- .getService(Ci.nsIMessageListenerManager),
- exports.onMessage);
-onShutdown.add(function()
-{
- messageProxy._disconnect();
-});
+let {_EventTarget: EventTarget, i18n} = require("ext_common");
+let {port} = require("messaging");
+
+exports.onMessage = new EventTarget(port);
+exports.i18n = i18n;
-function Page(sender)
+function Page(windowID)
{
- this._sender = sender;
+ this._windowID = windowID;
}
Page.prototype = {
- sendMessage: function(message)
+ sendMessage: function(payload)
{
- if (this._sender)
- this._sender.sendAsyncMessage("AdblockPlus:Message", {payload: message});
+ port.emit("ext_message", {targetID: this._windowID, payload});
}
};
exports.Page = Page;
@@ -50,48 +40,35 @@ function PageMap()
{
this._map = new Map();
- Services.obs.addObserver(this, "message-manager-disconnect", true);
- onShutdown.add(function()
- {
- Services.obs.removeObserver(this, "message-manager-disconnect");
- }.bind(this));
+ port.on("ext_disconnect", windowID => this._map.delete(windowID));
}
PageMap.prototype = {
- QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
-
- observe: function(subject, topic, data)
- {
- if (topic == "message-manager-disconnect")
- this._map.delete(subject);
- },
-
keys: function()
{
let result = [];
- for (let sender of this._map.keys())
- result.push(new Page(sender));
+ for (let windowID of this._map.keys())
+ result.push(new Page(windowID));
return result;
},
get: function(page)
{
- return this._map.get(page._sender);
+ return this._map.get(page._windowID);
},
set: function(page, value)
{
- if (page._sender)
- this._map.set(page._sender, value);
+ this._map.set(page._windowID, value);
},
has: function(page)
{
- return this._map.has(page._sender);
+ return this._map.has(page._windowID);
},
delete: function(page)
{
- this._map.delete(page._sender);
+ return this._map.delete(page._windowID);
}
};
exports.PageMap = PageMap;
diff --git a/data/extensions/spyblock@gnu.org/lib/ext_common.js b/data/extensions/spyblock@gnu.org/lib/ext_common.js
index 129f232..296c00f 100644
--- a/data/extensions/spyblock@gnu.org/lib/ext_common.js
+++ b/data/extensions/spyblock@gnu.org/lib/ext_common.js
@@ -1,6 +1,6 @@
/*
* This file is part of Adblock Plus <https://adblockplus.org/>,
- * Copyright (C) 2006-2015 Eyeo GmbH
+ * Copyright (C) 2006-2017 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
@@ -17,140 +17,105 @@
(function(global)
{
- const Ci = Components.interfaces;
+ const Cu = Components.utils;
+
+ let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
if (!global.ext)
global.ext = {};
- var holder = {
- get Page()
- {
- delete this.Page;
- this.Page = (typeof require == "function" ?
- require("ext_background").Page :
- function() {});
- return this.Page;
- }
- };
+ var wrapperSymbol = Symbol("ext-wrapper");
- var getSender = global.ext._getSender = function(origin)
+ function wrapFrames(frames)
{
- if (origin instanceof Ci.nsIDOMXULElement)
- return origin.messageManager;
- else if (origin instanceof Ci.nsIMessageSender)
- return origin;
- else
+ if (!frames.length)
return null;
- };
- var MessageProxy = global.ext._MessageProxy = function(messageManager, messageTarget)
- {
- this._messageManager = messageManager;
- this._messageTarget = messageTarget;
- this._callbacks = new Map();
- this._responseCallbackCounter = 0;
+ // We have frames as an array, non-Firefox code expects url and parent
+ // properties however.
+ Object.defineProperty(frames, "url", {
+ enumerable: true,
+ get: () => new URL(frames[0].location)
+ });
- this._handleRequest = this._handleRequest.bind(this);
- this._handleResponse = this._handleResponse.bind(this);
- this._messageManager.addMessageListener("AdblockPlus:Message", this._handleRequest);
- this._messageManager.addMessageListener("AdblockPlus:Response", this._handleResponse);
- };
- MessageProxy.prototype = {
- _disconnect: function()
- {
- this._messageManager.removeMessageListener("AdblockPlus:Message", this._handleRequest);
- this._messageManager.removeMessageListener("AdblockPlus:Response", this._handleResponse);
- },
+ Object.defineProperty(frames, "parent", {
+ enumerable: true,
+ get: () => wrapFrames(frames.slice(1))
+ });
- _sendResponse: function(sender, callbackId, message)
- {
- var response = {
- callbackId: callbackId
- };
- if (typeof response != "undefined")
- response.payload = message;
- sender.sendAsyncMessage("AdblockPlus:Response", response);
- },
+ return frames;
+ }
- _handleRequest: function(message)
+ var EventTarget = global.ext._EventTarget = function(port, windowID)
+ {
+ this._port = port;
+ this._windowID = windowID;
+ this.addListener((payload, sender, resolve) =>
{
- var sender = getSender(message.target);
- var request = message.data;
-
- var sent = false;
- var sendResponse;
- if (sender && "callbackId" in request)
+ if (payload.type)
{
- sendResponse = function(message)
- {
- this._sendResponse(sender, request.callbackId, message);
- sent = true;
- }.bind(this);
+ let result = this._port._dispatch(payload.type, payload, sender);
+ if (typeof result != "undefined")
+ resolve(result);
}
- else
- sendResponse = function() {};
-
- var results = this._messageTarget._dispatch(request.payload, {
- page: new holder.Page(sender)
- }, sendResponse);
- if (!sent && results.indexOf(true) == -1)
- sendResponse(undefined);
- },
-
- _handleResponse: function(message)
+ });
+ };
+ EventTarget.prototype = {
+ addListener: function(listener)
{
- var response = message.data;
- var callback = this._callbacks.get(response.callbackId);
- if (callback)
+ var wrapper = (message, sender) =>
{
- this._callbacks.delete(response.callbackId);
- if ("payload" in response)
- callback(response.payload);
- }
- },
+ if (this._windowID && this._windowID != message.targetID)
+ return undefined;
- sendMessage: function(message, responseCallback)
- {
- if (!(this._messageManager instanceof Ci.nsIMessageSender))
- throw new Error("Not implemented");
-
- var request = {
- payload: message
+ return new Promise((resolve, reject) =>
+ {
+ var sender = {};
+ if (message.senderID)
+ {
+ // We will only get here on the background side so we can access
+ // the Page object.
+ const Page = require("ext_background").Page;
+ sender.page = new Page(message.senderID);
+ }
+ if (message.frames)
+ sender.frame = wrapFrames(message.frames);
+ if (!listener(message.payload, sender, resolve))
+ resolve(undefined);
+ });
};
- if (responseCallback)
- {
- request.callbackId = ++this._responseCallbackCounter;
- this._callbacks.set(request.callbackId, responseCallback);
- }
+ listener[wrapperSymbol] = wrapper;
+ this._port.on("ext_message", wrapper);
+ },
- this._messageManager.sendAsyncMessage("AdblockPlus:Message", request);
+ removeListener: function(listener)
+ {
+ if (listener[wrapperSymbol])
+ this._port.off("ext_message", listener[wrapperSymbol]);
}
};
- var EventTarget = global.ext._EventTarget = function()
- {
- this._listeners = [];
- };
- EventTarget.prototype = {
- addListener: function(listener)
- {
- if (this._listeners.indexOf(listener) == -1)
- this._listeners.push(listener);
- },
- removeListener: function(listener)
- {
- var idx = this._listeners.indexOf(listener);
- if (idx != -1)
- this._listeners.splice(idx, 1);
- },
- _dispatch: function()
- {
- var results = [];
+ let pageName = "global";
+ if (typeof location !== "undefined")
+ pageName = location.pathname.replace(/.*\//, "").replace(/\..*?$/, "");
- for (var i = 0; i < this._listeners.length; i++)
- results.push(this._listeners[i].apply(null, arguments));
+ let stringBundle = Services.strings.createBundle(
+ "chrome://adblockplus/locale/" + pageName + ".properties?" + Math.random());
- return results;
+ global.ext.i18n = {
+ getMessage(key, args)
+ {
+ try {
+ return stringBundle.GetStringFromName(key);
+ }
+ catch(e)
+ {
+ // Don't report errors for special strings, these are expected to be
+ // missing.
+ if (key[0] != "@")
+ Cu.reportError(e);
+ return "";
+ }
}
};
diff --git a/data/extensions/spyblock@gnu.org/lib/filterClasses.js b/data/extensions/spyblock@gnu.org/lib/filterClasses.js
index a0dfaf0..3612728 100644
--- a/data/extensions/spyblock@gnu.org/lib/filterClasses.js
+++ b/data/extensions/spyblock@gnu.org/lib/filterClasses.js
@@ -1,6 +1,6 @@
/*
* This file is part of Adblock Plus <https://adblockplus.org/>,
- * Copyright (C) 2006-2015 Eyeo GmbH
+ * Copyright (C) 2006-2017 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
@@ -15,16 +15,20 @@
* along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>.
*/
+"use strict";
+
/**
* @fileOverview Definition of Filter class and its subclasses.
*/
-let {FilterNotifier} = require("filterNotifier");
+const {FilterNotifier} = require("filterNotifier");
+const {extend} = require("coreUtils");
+const {filterToRegExp} = require("common");
/**
* Abstract base class for filters
*
- * @param {String} text string representation of the filter
+ * @param {string} text string representation of the filter
* @constructor
*/
function Filter(text)
@@ -38,27 +42,36 @@ Filter.prototype =
{
/**
* String representation of the filter
- * @type String
+ * @type {string}
*/
text: null,
/**
* Filter subscriptions the filter belongs to
- * @type Array of Subscription
+ * @type {Subscription[]}
*/
subscriptions: null,
/**
+ * Filter type as a string, e.g. "blocking".
+ * @type {string}
+ */
+ get type()
+ {
+ throw new Error("Please define filter type in the subclass");
+ },
+
+ /**
* 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
+ * @param {string[]} buffer buffer to push the serialization results into
*/
- serialize: function(buffer)
+ serialize(buffer)
{
buffer.push("[Filter]");
buffer.push("text=" + this.text);
},
- toString: function()
+ toString()
{
return this.text;
}
@@ -66,31 +79,31 @@ Filter.prototype =
/**
* Cache for known filters, maps string representation to filter objects.
- * @type Object
+ * @type {Object}
*/
Filter.knownFilters = Object.create(null);
/**
* Regular expression that element hiding filters should match
- * @type RegExp
+ * @type {RegExp}
*/
-Filter.elemhideRegExp = /^([^\/\*\|\@"!]*?)#(\@)?(?:([\w\-]+|\*)((?:\([\w\-]+(?:[$^*]?=[^\(\)"]*)?\))*)|#([^{}]+))$/;
+Filter.elemhideRegExp = /^([^/*|@"!]*?)#(@)?(?:([\w-]+|\*)((?:\([\w-]+(?:[$^*]?=[^()"]*)?\))*)|#(.+))$/;
/**
* Regular expression that RegExp filters specified as RegExps should match
- * @type RegExp
+ * @type {RegExp}
*/
-Filter.regexpRegExp = /^(@@)?\/.*\/(?:\$~?[\w\-]+(?:=[^,\s]+)?(?:,~?[\w\-]+(?:=[^,\s]+)?)*)?$/;
+Filter.regexpRegExp = /^(@@)?\/.*\/(?:\$~?[\w-]+(?:=[^,\s]+)?(?:,~?[\w-]+(?:=[^,\s]+)?)*)?$/;
/**
* Regular expression that options on a RegExp filter should match
- * @type RegExp
+ * @type {RegExp}
*/
-Filter.optionsRegExp = /\$(~?[\w\-]+(?:=[^,\s]+)?(?:,~?[\w\-]+(?:=[^,\s]+)?)*)$/;
+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.
+ * 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()
+ * @param {string} text as in Filter()
* @return {Filter}
*/
Filter.fromText = function(text)
@@ -99,9 +112,13 @@ Filter.fromText = function(text)
return Filter.knownFilters[text];
let ret;
- let match = (text.indexOf("#") >= 0 ? Filter.elemhideRegExp.exec(text) : null);
+ let match = (text.includes("#") ? Filter.elemhideRegExp.exec(text) : null);
if (match)
- ret = ElemHideBase.fromText(text, match[1], match[2], match[3], match[4], match[5]);
+ {
+ ret = ElemHideBase.fromText(
+ text, match[1], !!match[2], match[3], match[4], match[5]
+ );
+ }
else if (text[0] == "!")
ret = new CommentFilter(text);
else
@@ -125,9 +142,9 @@ Filter.fromObject = function(obj)
if ("disabled" in obj)
ret._disabled = (obj.disabled == "true");
if ("hitCount" in obj)
- ret._hitCount = parseInt(obj.hitCount) || 0;
+ ret._hitCount = parseInt(obj.hitCount, 10) || 0;
if ("lastHit" in obj)
- ret._lastHit = parseInt(obj.lastHit) || 0;
+ ret._lastHit = parseInt(obj.lastHit, 10) || 0;
}
return ret;
};
@@ -135,8 +152,10 @@ Filter.fromObject = function(obj)
/**
* Removes unnecessary whitespaces from filter text, will only return null if
* the input parameter is null.
+ * @param {string} text
+ * @return {string}
*/
-Filter.normalize = function(/**String*/ text) /**String*/
+Filter.normalize = function(text)
{
if (!text)
return text;
@@ -151,18 +170,23 @@ Filter.normalize = function(/**String*/ text) /**String*/
}
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);
+ // 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.trim();
}
- else
- return text.replace(/\s/g, "");
+ return text.replace(/\s/g, "");
};
/**
+ * @see filterToRegExp
+ */
+Filter.toRegExp = filterToRegExp;
+
+/**
* Class for invalid filters
- * @param {String} text see Filter()
- * @param {String} reason Reason why this filter is invalid
+ * @param {string} text see Filter()
+ * @param {string} reason Reason why this filter is invalid
* @constructor
* @augments Filter
*/
@@ -174,25 +198,25 @@ function InvalidFilter(text, reason)
}
exports.InvalidFilter = InvalidFilter;
-InvalidFilter.prototype =
-{
- __proto__: Filter.prototype,
+InvalidFilter.prototype = extend(Filter, {
+ type: "invalid",
/**
* Reason why this filter is invalid
- * @type String
+ * @type {string}
*/
reason: null,
/**
* See Filter.serialize()
+ * @inheritdoc
*/
- serialize: function(buffer) {}
-};
+ serialize(buffer) {}
+});
/**
* Class for comments
- * @param {String} text see Filter()
+ * @param {string} text see Filter()
* @constructor
* @augments Filter
*/
@@ -202,20 +226,23 @@ function CommentFilter(text)
}
exports.CommentFilter = CommentFilter;
-CommentFilter.prototype =
-{
- __proto__: Filter.prototype,
+CommentFilter.prototype = extend(Filter, {
+ type: "comment",
/**
* See Filter.serialize()
+ * @inheritdoc
*/
- serialize: function(buffer) {}
-};
+ serialize(buffer) {}
+});
/**
* Abstract base class for filters that can get hits
- * @param {String} text see Filter()
- * @param {String} [domains] Domains that the filter is restricted to separated by domainSeparator e.g. "foo.com|bar.com|~baz.com"
+ * @param {string} text
+ * see Filter()
+ * @param {string} [domains]
+ * Domains that the filter is restricted to separated by domainSeparator
+ * e.g. "foo.com|bar.com|~baz.com"
* @constructor
* @augments Filter
*/
@@ -227,17 +254,14 @@ function ActiveFilter(text, domains)
}
exports.ActiveFilter = ActiveFilter;
-ActiveFilter.prototype =
-{
- __proto__: Filter.prototype,
-
+ActiveFilter.prototype = extend(Filter, {
_disabled: false,
_hitCount: 0,
_lastHit: 0,
/**
* Defines whether the filter is disabled
- * @type Boolean
+ * @type {boolean}
*/
get disabled()
{
@@ -256,7 +280,7 @@ ActiveFilter.prototype =
/**
* Number of hits on the filter since the last reset
- * @type Number
+ * @type {number}
*/
get hitCount()
{
@@ -274,8 +298,9 @@ ActiveFilter.prototype =
},
/**
- * Last time the filter had a hit (in milliseconds since the beginning of the epoch)
- * @type Number
+ * Last time the filter had a hit (in milliseconds since the beginning of the
+ * epoch)
+ * @type {number}
*/
get lastHit()
{
@@ -294,33 +319,35 @@ ActiveFilter.prototype =
/**
* String that the domains property should be generated from
- * @type String
+ * @type {string}
*/
domainSource: null,
/**
- * Separator character used in domainSource property, must be overridden by subclasses
- * @type String
+ * 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
+ * @type {boolean}
*/
ignoreTrailingDot: true,
/**
* Determines whether domainSource is already upper-case,
* can be overridden by subclasses.
- * @type Boolean
+ * @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
+ * 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()
{
@@ -335,7 +362,8 @@ ActiveFilter.prototype =
if (this.domainSource)
{
let source = this.domainSource;
- if (!this.domainSourceIsUpperCase) {
+ if (!this.domainSourceIsUpperCase)
+ {
// RegExpFilter already have uppercase domains
source = source.toUpperCase();
}
@@ -343,7 +371,8 @@ ActiveFilter.prototype =
if (list.length == 1 && list[0][0] != "~")
{
// Fast track for the common one-domain scenario
- domains = {__proto__: null, "": false};
+ domains = Object.create(null);
+ domains[""] = false;
if (this.ignoreTrailingDot)
list[0] = list[0].replace(/\.+$/, "");
domains[list[0]] = true;
@@ -376,7 +405,8 @@ ActiveFilter.prototype =
domains[domain] = include;
}
- domains[""] = !hasIncludes;
+ if (domains)
+ domains[""] = !hasIncludes;
}
this.domainSource = null;
@@ -388,28 +418,33 @@ ActiveFilter.prototype =
/**
* Array containing public keys of websites that this filter should apply to
- * @type Array of String
+ * @type {string[]}
*/
sitekeys: null,
/**
* Checks whether this filter is active on a domain.
- * @param {String} docDomain domain name of the document that loads the URL
- * @param {String} [sitekey] public key provided by the document
- * @return {Boolean} true in case of the filter being active
+ * @param {string} docDomain domain name of the document that loads the URL
+ * @param {string} [sitekey] public key provided by the document
+ * @return {boolean} true in case of the filter being active
*/
- isActiveOnDomain: function(docDomain, sitekey)
+ isActiveOnDomain(docDomain, sitekey)
{
- // Sitekeys are case-sensitive so we shouldn't convert them to upper-case to avoid false
- // positives here. Instead we need to change the way filter options are parsed.
- if (this.sitekeys && (!sitekey || this.sitekeys.indexOf(sitekey.toUpperCase()) < 0))
+ // Sitekeys are case-sensitive so we shouldn't convert them to
+ // upper-case to avoid false positives here. Instead we need to
+ // change the way filter options are parsed.
+ if (this.sitekeys &&
+ (!sitekey || this.sitekeys.indexOf(sitekey.toUpperCase()) < 0))
+ {
return false;
+ }
// 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 the document has no host name, match only if the filter
+ // isn't restricted to specific domains
if (!docDomain)
return this.domains[""];
@@ -432,8 +467,10 @@ ActiveFilter.prototype =
/**
* Checks whether this filter is active only on a domain and its subdomains.
+ * @param {string} docDomain
+ * @return {boolean}
*/
- isActiveOnlyOnDomain: function(/**String*/ docDomain) /**Boolean*/
+ isActiveOnlyOnDomain(docDomain)
{
if (!docDomain || !this.domains || this.domains[""])
return false;
@@ -443,16 +480,35 @@ ActiveFilter.prototype =
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;
+ {
+ if (this.domains[domain] && domain != docDomain)
+ {
+ if (domain.length <= docDomain.length)
+ return false;
+
+ if (!domain.endsWith("." + docDomain))
+ return false;
+ }
+ }
return true;
},
/**
+ * Checks whether this filter is generic or specific
+ * @return {boolean}
+ */
+ isGeneric()
+ {
+ return !(this.sitekeys && this.sitekeys.length) &&
+ (!this.domains || this.domains[""]);
+ },
+
+ /**
* See Filter.serialize()
+ * @inheritdoc
*/
- serialize: function(buffer)
+ serialize(buffer)
{
if (this._disabled || this._hitCount || this._lastHit)
{
@@ -465,21 +521,31 @@ ActiveFilter.prototype =
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] Content types the filter applies to, combination of values from RegExpFilter.typeMap
- * @param {Boolean} [matchCase] Defines whether the filter should distinguish between lower and upper case letters
- * @param {String} [domains] Domains that the filter is restricted to, e.g. "foo.com|bar.com|~baz.com"
- * @param {Boolean} [thirdParty] Defines whether the filter should apply to third-party or first-party content only
- * @param {String} [sitekeys] Public keys of websites that this filter should apply to
+ * @param {string} text see Filter()
+ * @param {string} regexpSource
+ * filter part that the regular expression should be build from
+ * @param {number} [contentType]
+ * Content types the filter applies to, combination of values from
+ * RegExpFilter.typeMap
+ * @param {boolean} [matchCase]
+ * Defines whether the filter should distinguish between lower and upper case
+ * letters
+ * @param {string} [domains]
+ * Domains that the filter is restricted to, e.g. "foo.com|bar.com|~baz.com"
+ * @param {boolean} [thirdParty]
+ * Defines whether the filter should apply to third-party or first-party
+ * content only
+ * @param {string} [sitekeys]
+ * Public keys of websites that this filter should apply to
* @constructor
* @augments ActiveFilter
*/
-function RegExpFilter(text, regexpSource, contentType, matchCase, domains, thirdParty, sitekeys)
+function RegExpFilter(text, regexpSource, contentType, matchCase, domains,
+ thirdParty, sitekeys)
{
ActiveFilter.call(this, text, domains, sitekeys);
@@ -492,10 +558,14 @@ function RegExpFilter(text, regexpSource, contentType, matchCase, domains, third
if (sitekeys != null)
this.sitekeySource = sitekeys;
- if (regexpSource.length >= 2 && regexpSource[0] == "/" && regexpSource[regexpSource.length - 1] == "/")
+ 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");
+ // 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");
Object.defineProperty(this, "regexp", {value: regexp});
}
else
@@ -506,18 +576,16 @@ function RegExpFilter(text, regexpSource, contentType, matchCase, domains, third
}
exports.RegExpFilter = RegExpFilter;
-RegExpFilter.prototype =
-{
- __proto__: ActiveFilter.prototype,
-
+RegExpFilter.prototype = extend(ActiveFilter, {
/**
* @see ActiveFilter.domainSourceIsUpperCase
*/
domainSourceIsUpperCase: true,
/**
- * Number of filters contained, will always be 1 (required to optimize Matcher).
- * @type Integer
+ * Number of filters contained, will always be 1 (required to
+ * optimize Matcher).
+ * @type {number}
*/
length: 1,
@@ -527,13 +595,14 @@ RegExpFilter.prototype =
domainSeparator: "|",
/**
- * Expression from which a regular expression should be generated - for delayed creation of the regexp property
- * @type String
+ * 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
+ * @type {RegExp}
*/
get regexp()
{
@@ -543,49 +612,39 @@ RegExpFilter.prototype =
if (prop)
return prop.value;
- // 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 source = Filter.toRegExp(this.regexpSource);
let regexp = new RegExp(source, this.matchCase ? "" : "i");
Object.defineProperty(this, "regexp", {value: regexp});
return regexp;
},
/**
- * Content types the filter applies to, combination of values from RegExpFilter.typeMap
- * @type Number
+ * 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
+ * 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
+ * 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,
/**
* String that the sitekey property should be generated from
- * @type String
+ * @type {string}
*/
sitekeySource: null,
/**
* Array containing public keys of websites that this filter should apply to
- * @type Array of String
+ * @type {string[]}
*/
get sitekeys()
{
@@ -603,47 +662,48 @@ RegExpFilter.prototype =
this.sitekeySource = null;
}
- Object.defineProperty(this, "sitekeys", {value: sitekeys, enumerable: true});
+ Object.defineProperty(
+ this, "sitekeys", {value: sitekeys, enumerable: true}
+ );
return this.sitekeys;
},
/**
* 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
- * @param {String} sitekey public key provided by the document
- * @return {Boolean} true in case of a match
- */
- matches: function(location, contentType, docDomain, thirdParty, sitekey, privatenode)
+ * @param {string} location URL to be tested
+ * @param {number} typeMask bitmask of content / request types to match
+ * @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
+ * @param {string} sitekey public key provided by the document
+ * @return {boolean} true in case of a match
+ */
+ matches(location, typeMask, docDomain, thirdParty, sitekey, privatenode)
{
-
if(this.subscriptions[0])
if (this.subscriptions[0].privateMode)
if (privatenode==false)
return false;
- if ((RegExpFilter.typeMap[contentType] & this.contentType) != 0 &&
+ if (this.contentType & typeMask &&
(this.thirdParty == null || this.thirdParty == thirdParty) &&
this.isActiveOnDomain(docDomain, sitekey) && this.regexp.test(location))
{
return true;
}
-
return false;
}
-};
+});
// Required to optimize Matcher, see also RegExpFilter.prototype.length
-Object.defineProperty(RegExpFilter.prototype, "0",
-{
- get: function() { return this; }
+Object.defineProperty(RegExpFilter.prototype, "0", {
+ get() { return this; }
});
/**
* Creates a RegExp filter from its text representation
- * @param {String} text same as in Filter()
+ * @param {string} text same as in Filter()
+ * @return {Filter}
*/
RegExpFilter.fromText = function(text)
{
@@ -686,7 +746,7 @@ RegExpFilter.fromText = function(text)
else if (option[0] == "~" && option.substr(1) in RegExpFilter.typeMap)
{
if (contentType == null)
- contentType = RegExpFilter.prototype.contentType;
+ ({contentType} = RegExpFilter.prototype);
contentType &= ~RegExpFilter.typeMap[option.substr(1)];
}
else if (option == "MATCH_CASE")
@@ -706,29 +766,23 @@ RegExpFilter.fromText = function(text)
else if (option == "SITEKEY" && typeof value != "undefined")
sitekeys = value;
else
- return new InvalidFilter(origText, "Unknown option " + option.toLowerCase());
+ return new InvalidFilter(origText, "filter_unknown_option");
}
}
- 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;
- }
-
try
{
if (blocking)
- return new BlockingFilter(origText, text, contentType, matchCase, domains, thirdParty, sitekeys, collapse);
- else
- return new WhitelistFilter(origText, text, contentType, matchCase, domains, thirdParty, sitekeys);
+ {
+ return new BlockingFilter(origText, text, contentType, matchCase, domains,
+ thirdParty, sitekeys, collapse);
+ }
+ return new WhitelistFilter(origText, text, contentType, matchCase, domains,
+ thirdParty, sitekeys);
}
catch (e)
{
- return new InvalidFilter(origText, e);
+ return new InvalidFilter(origText, "filter_invalid_regexp");
}
};
@@ -743,8 +797,10 @@ RegExpFilter.typeMap = {
OBJECT: 16,
SUBDOCUMENT: 32,
DOCUMENT: 64,
+ WEBSOCKET: 128,
+ WEBRTC: 256,
XBL: 1,
- PING: 1,
+ PING: 1024,
XMLHTTPREQUEST: 2048,
OBJECT_SUBREQUEST: 4096,
DTD: 1,
@@ -754,72 +810,85 @@ RegExpFilter.typeMap = {
BACKGROUND: 4, // Backwards compat, same as IMAGE
POPUP: 0x10000000,
- ELEMHIDE: 0x40000000
+ GENERICBLOCK: 0x20000000,
+ ELEMHIDE: 0x40000000,
+ GENERICHIDE: 0x80000000
};
-// ELEMHIDE, POPUP option shouldn't be there by default
-RegExpFilter.prototype.contentType &= ~(RegExpFilter.typeMap.ELEMHIDE | RegExpFilter.typeMap.POPUP);
+// DOCUMENT, ELEMHIDE, POPUP, GENERICHIDE and GENERICBLOCK options shouldn't
+// be there by default
+RegExpFilter.prototype.contentType &= ~(RegExpFilter.typeMap.DOCUMENT |
+ RegExpFilter.typeMap.ELEMHIDE |
+ RegExpFilter.typeMap.POPUP |
+ RegExpFilter.typeMap.GENERICHIDE |
+ RegExpFilter.typeMap.GENERICBLOCK);
/**
* 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 {String} sitekeys see RegExpFilter()
- * @param {Boolean} collapse defines whether the filter should collapse blocked content, can be null
+ * @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 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, sitekeys, collapse)
+function BlockingFilter(text, regexpSource, contentType, matchCase, domains,
+ thirdParty, sitekeys, collapse)
{
- RegExpFilter.call(this, text, regexpSource, contentType, matchCase, domains, thirdParty, sitekeys);
+ RegExpFilter.call(this, text, regexpSource, contentType, matchCase, domains,
+ thirdParty, sitekeys);
this.collapse = collapse;
}
exports.BlockingFilter = BlockingFilter;
-BlockingFilter.prototype =
-{
- __proto__: RegExpFilter.prototype,
+BlockingFilter.prototype = extend(RegExpFilter, {
+ type: "blocking",
/**
- * Defines whether the filter should collapse blocked content. Can be null (use the global preference).
- * @type Boolean
+ * 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 see RegExpFilter()
+ * @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 see RegExpFilter()
* @constructor
* @augments RegExpFilter
*/
-function WhitelistFilter(text, regexpSource, contentType, matchCase, domains, thirdParty, sitekeys)
+function WhitelistFilter(text, regexpSource, contentType, matchCase, domains,
+ thirdParty, sitekeys)
{
- RegExpFilter.call(this, text, regexpSource, contentType, matchCase, domains, thirdParty, sitekeys);
+ RegExpFilter.call(this, text, regexpSource, contentType, matchCase, domains,
+ thirdParty, sitekeys);
}
exports.WhitelistFilter = WhitelistFilter;
-WhitelistFilter.prototype =
-{
- __proto__: RegExpFilter.prototype
-};
+WhitelistFilter.prototype = extend(RegExpFilter, {
+ type: "whitelist"
+});
/**
* Base class for element hiding filters
- * @param {String} text see Filter()
- * @param {String} [domains] Host names or domains the filter should be restricted to
- * @param {String} selector CSS selector for the HTML elements that should be hidden
+ * @param {string} text see Filter()
+ * @param {string} [domains] 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
*/
@@ -828,15 +897,17 @@ function ElemHideBase(text, domains, selector)
ActiveFilter.call(this, text, domains || null);
if (domains)
- this.selectorDomain = domains.replace(/,~[^,]+/g, "").replace(/^~[^,]+,?/, "").toLowerCase();
- this.selector = selector;
+ {
+ this.selectorDomain = domains.replace(/,~[^,]+/g, "")
+ .replace(/^~[^,]+,?/, "").toLowerCase();
+ }
+
+ // Braces are being escaped to prevent CSS rule injection.
+ this.selector = selector.replace("{", "\\x7B ").replace("}", "\\x7D ");
}
exports.ElemHideBase = ElemHideBase;
-ElemHideBase.prototype =
-{
- __proto__: ActiveFilter.prototype,
-
+ElemHideBase.prototype = extend(ActiveFilter, {
/**
* @see ActiveFilter.domainSeparator
*/
@@ -848,28 +919,33 @@ ElemHideBase.prototype =
ignoreTrailingDot: false,
/**
- * Host name or domain the filter should be restricted to (can be null for no restriction)
- * @type String
+ * 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
+ * @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}
+ * @param {string} text same as in Filter()
+ * @param {string} domain
+ * domain part of the text representation (can be empty)
+ * @param {boolean} isException exception rule indicator
+ * @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|
+ * ElemHideEmulationFilter|InvalidFilter}
*/
-ElemHideBase.fromText = function(text, domain, isException, tagName, attrRules, selector)
+ElemHideBase.fromText = function(text, domain, isException, tagName, attrRules,
+ selector)
{
if (!selector)
{
@@ -878,48 +954,63 @@ ElemHideBase.fromText = function(text, domain, isException, tagName, attrRules,
let id = null;
let additional = "";
- if (attrRules) {
- attrRules = attrRules.match(/\([\w\-]+(?:[$^*]?=[^\(\)"]*)?\)/g);
- for (let rule of attrRules) {
+ 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) {
+ if (separatorPos > 0)
+ {
rule = rule.replace(/=/, '="') + '"';
additional += "[" + rule + "]";
}
- else {
+ else
+ {
if (id)
- {
- let {Utils} = require("utils");
- return new InvalidFilter(text, Utils.getString("filter_elemhide_duplicate_id"));
- }
- else
- id = rule;
+ return new InvalidFilter(text, "filter_elemhide_duplicate_id");
+
+ id = rule;
}
}
}
if (id)
- selector = tagName + "." + id + additional + "," + tagName + "#" + id + additional;
+ 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"));
- }
+ return new InvalidFilter(text, "filter_elemhide_nocriteria");
}
+
+ // We don't allow ElemHide filters which have any empty domains.
+ // Note: The ElemHide.prototype.domainSeparator is duplicated here, if that
+ // changes this must be changed too.
+ if (domain && /(^|,)~?(,|$)/.test(domain))
+ return new InvalidFilter(text, "filter_invalid_domain");
+
if (isException)
return new ElemHideException(text, domain, selector);
- else
- return new ElemHideFilter(text, domain, selector);
+
+ if (selector.indexOf("[-abp-properties=") != -1)
+ {
+ // Element hiding emulation filters are inefficient so we need to make sure
+ // that they're only applied if they specify active domains
+ if (!/,[^~][^,.]*\.[^,]/.test("," + domain))
+ return new InvalidFilter(text, "filter_elemhideemulation_nodomain");
+
+ return new ElemHideEmulationFilter(text, domain, selector);
+ }
+
+ 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()
+ * @param {string} text see Filter()
+ * @param {string} domains see ElemHideBase()
+ * @param {string} selector see ElemHideBase()
* @constructor
* @augments ElemHideBase
*/
@@ -929,16 +1020,15 @@ function ElemHideFilter(text, domains, selector)
}
exports.ElemHideFilter = ElemHideFilter;
-ElemHideFilter.prototype =
-{
- __proto__: ElemHideBase.prototype
-};
+ElemHideFilter.prototype = extend(ElemHideBase, {
+ type: "elemhide"
+});
/**
* Class for element hiding exceptions
- * @param {String} text see Filter()
- * @param {String} domains see ElemHideBase()
- * @param {String} selector see ElemHideBase()
+ * @param {string} text see Filter()
+ * @param {string} domains see ElemHideBase()
+ * @param {string} selector see ElemHideBase()
* @constructor
* @augments ElemHideBase
*/
@@ -948,7 +1038,24 @@ function ElemHideException(text, domains, selector)
}
exports.ElemHideException = ElemHideException;
-ElemHideException.prototype =
+ElemHideException.prototype = extend(ElemHideBase, {
+ type: "elemhideexception"
+});
+
+/**
+ * Class for element hiding emulation filters
+ * @param {string} text see Filter()
+ * @param {string} domains see ElemHideBase()
+ * @param {string} selector see ElemHideBase()
+ * @constructor
+ * @augments ElemHideBase
+ */
+function ElemHideEmulationFilter(text, domains, selector)
{
- __proto__: ElemHideBase.prototype
-};
+ ElemHideBase.call(this, text, domains, selector);
+}
+exports.ElemHideEmulationFilter = ElemHideEmulationFilter;
+
+ElemHideEmulationFilter.prototype = extend(ElemHideBase, {
+ type: "elemhideemulation"
+});
diff --git a/data/extensions/spyblock@gnu.org/lib/filterListener.js b/data/extensions/spyblock@gnu.org/lib/filterListener.js
index e993a44..8700602 100644
--- a/data/extensions/spyblock@gnu.org/lib/filterListener.js
+++ b/data/extensions/spyblock@gnu.org/lib/filterListener.js
@@ -1,6 +1,6 @@
/*
* This file is part of Adblock Plus <https://adblockplus.org/>,
- * Copyright (C) 2006-2015 Eyeo GmbH
+ * Copyright (C) 2006-2017 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
@@ -15,29 +15,28 @@
* along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>.
*/
+"use strict";
+
/**
- * @fileOverview Component synchronizing filter storage with Matcher instances and ElemHide.
+ * @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 {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");
+const {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
+const {XPCOMUtils} = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {});
-/**
- * Value of the FilterListener.batchMode property.
- * @type Boolean
- */
-let batchMode = false;
+const {FilterStorage} = require("filterStorage");
+const {FilterNotifier} = require("filterNotifier");
+const {ElemHide} = require("elemHide");
+const {ElemHideEmulation} = require("elemHideEmulation");
+const {defaultMatcher} = require("matcher");
+const {ActiveFilter, RegExpFilter,
+ ElemHideBase, ElemHideEmulationFilter} = require("filterClasses");
+const {Prefs} = require("prefs");
/**
* Increases on filter changes, filters will be saved if it exceeds 1.
- * @type Integer
+ * @type {number}
*/
let isDirty = 0;
@@ -45,36 +44,26 @@ 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();
- },
-
+let FilterListener = {
/**
- * 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.
+ * 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.
+ * @param {number} factor
*/
- setDirty: function(/**Integer*/ factor)
+ setDirty(factor)
{
if (factor == 0 && isDirty > 0)
isDirty = 1;
else
isDirty += factor;
if (isDirty >= 1)
+ {
+ isDirty = 0;
FilterStorage.saveToDisk();
+ }
}
};
@@ -82,11 +71,11 @@ let FilterListener =
* Observer listening to history purge actions.
* @class
*/
-let HistoryPurgeObserver =
-{
- observe: function(subject, topic, data)
+let HistoryPurgeObserver = {
+ observe(subject, topic, data)
{
- if (topic == "browser:purge-session-history" && Prefs.clearStatsOnHistoryPurge)
+ if (topic == "browser:purge-session-history" &&
+ Prefs.clearStatsOnHistoryPurge)
{
FilterStorage.resetHitCounts();
FilterListener.setDirty(0); // Force saving to disk
@@ -94,7 +83,9 @@ let HistoryPurgeObserver =
Prefs.recentReports = [];
}
},
- QueryInterface: XPCOMUtils.generateQI([Ci.nsISupportsWeakReference, Ci.nsIObserver])
+ QueryInterface: XPCOMUtils.generateQI(
+ [Ci.nsISupportsWeakReference, Ci.nsIObserver]
+ )
};
/**
@@ -102,41 +93,41 @@ let HistoryPurgeObserver =
*/
function init()
{
- 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);
- });
+ FilterNotifier.on("filter.hitCount", onFilterHitCount);
+ FilterNotifier.on("filter.lastHit", onFilterLastHit);
+ FilterNotifier.on("filter.added", onFilterAdded);
+ FilterNotifier.on("filter.removed", onFilterRemoved);
+ FilterNotifier.on("filter.disabled", onFilterDisabled);
+ FilterNotifier.on("filter.moved", onGenericChange);
+
+ FilterNotifier.on("subscription.added", onSubscriptionAdded);
+ FilterNotifier.on("subscription.removed", onSubscriptionRemoved);
+ FilterNotifier.on("subscription.disabled", onSubscriptionDisabled);
+ FilterNotifier.on("subscription.updated", onSubscriptionUpdated);
+ FilterNotifier.on("subscription.moved", onGenericChange);
+ FilterNotifier.on("subscription.title", onGenericChange);
+ FilterNotifier.on("subscription.fixedTitle", onGenericChange);
+ FilterNotifier.on("subscription.homepage", onGenericChange);
+ FilterNotifier.on("subscription.downloadStatus", onGenericChange);
+ FilterNotifier.on("subscription.lastCheck", onGenericChange);
+ FilterNotifier.on("subscription.errors", onGenericChange);
+
+ FilterNotifier.on("load", onLoad);
+ FilterNotifier.on("save", onSave);
- if ("nsIStyleSheetService" in Ci)
- ElemHide.init();
- else
- flushElemHide = function() {}; // No global stylesheet in Chrome & Co.
FilterStorage.loadFromDisk();
- Services.obs.addObserver(HistoryPurgeObserver, "browser:purge-session-history", true);
- onShutdown.add(function()
+ Services.obs.addObserver(HistoryPurgeObserver,
+ "browser:purge-session-history", true);
+ onShutdown.add(() =>
{
- Services.obs.removeObserver(HistoryPurgeObserver, "browser:purge-session-history");
+ Services.obs.removeObserver(HistoryPurgeObserver,
+ "browser:purge-session-history");
});
}
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
@@ -148,15 +139,22 @@ function addFilter(filter)
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);
+ {
+ if (filter instanceof ElemHideEmulationFilter)
+ ElemHideEmulation.add(filter);
+ else
+ ElemHide.add(filter);
+ }
}
/**
@@ -173,8 +171,10 @@ function removeFilter(filter)
{
let hasEnabled = false;
for (let i = 0; i < filter.subscriptions.length; i++)
+ {
if (!filter.subscriptions[i].disabled)
hasEnabled = true;
+ }
if (hasEnabled)
return;
}
@@ -182,93 +182,139 @@ function removeFilter(filter)
if (filter instanceof RegExpFilter)
defaultMatcher.remove(filter);
else if (filter instanceof ElemHideBase)
- ElemHide.remove(filter);
+ {
+ if (filter instanceof ElemHideEmulationFilter)
+ ElemHideEmulation.remove(filter);
+ else
+ ElemHide.remove(filter);
+ }
}
-/**
- * Subscription change listener
- */
-function onSubscriptionChange(action, subscription, newValue, oldValue)
-{
- FilterListener.setDirty(1);
+const primes = [101, 109, 131, 149, 163, 179, 193, 211, 229, 241];
- if (action != "added" && action != "removed" && action != "disabled" && action != "updated")
+function addFilters(filters)
+{
+ // We add filters using pseudo-random ordering. Reason is that ElemHide will
+ // assign consecutive filter IDs that might be visible to the website. The
+ // randomization makes sure that no conclusion can be made about the actual
+ // filters applying there. We have ten prime numbers to use as iteration step,
+ // any of those can be chosen as long as the array length isn't divisible by
+ // it.
+ let len = filters.length;
+ if (!len)
return;
- if (action != "removed" && !(subscription.url in FilterStorage.knownSubscriptions))
+ let current = (Math.random() * len) | 0;
+ let step;
+ do
{
- // Ignore updates for subscriptions not in the list
- return;
- }
+ step = primes[(Math.random() * primes.length) | 0];
+ } while (len % step == 0);
- if ((action == "added" || action == "removed" || action == "updated") && subscription.disabled)
- {
- // Ignore adding/removing/updating of disabled subscriptions
- return;
- }
+ for (let i = 0; i < len; i++, current = (current + step) % len)
+ addFilter(filters[current]);
+}
+
+function onSubscriptionAdded(subscription)
+{
+ FilterListener.setDirty(1);
+
+ if (!subscription.disabled)
+ addFilters(subscription.filters);
+}
+
+function onSubscriptionRemoved(subscription)
+{
+ FilterListener.setDirty(1);
+
+ if (!subscription.disabled)
+ subscription.filters.forEach(removeFilter);
+}
+
+function onSubscriptionDisabled(subscription, newValue)
+{
+ FilterListener.setDirty(1);
- if (action == "added" || action == "removed" || action == "disabled")
+ if (subscription.url in FilterStorage.knownSubscriptions)
{
- let method = (action == "added" || (action == "disabled" && newValue == false) ? addFilter : removeFilter);
- if (subscription.filters)
- subscription.filters.forEach(method);
+ if (newValue == false)
+ addFilters(subscription.filters);
+ else
+ subscription.filters.forEach(removeFilter);
}
- else if (action == "updated")
+}
+
+function onSubscriptionUpdated(subscription)
+{
+ FilterListener.setDirty(1);
+
+ if (subscription.url in FilterStorage.knownSubscriptions &&
+ !subscription.disabled)
{
subscription.oldFilters.forEach(removeFilter);
- subscription.filters.forEach(addFilter);
+ addFilters(subscription.filters);
}
-
- flushElemHide();
}
-/**
- * Filter change listener
- */
-function onFilterChange(action, filter, newValue, oldValue)
+function onFilterHitCount(filter, newValue)
{
- if (action == "hitCount" && newValue == 0)
- {
- // Filter hits are being reset, make sure these changes are saved.
+ if (newValue == 0)
FilterListener.setDirty(0);
- }
- else if (action == "hitCount" || action == "lastHit")
- FilterListener.setDirty(0.002);
else
- FilterListener.setDirty(1);
+ FilterListener.setDirty(0.002);
+}
- if (action != "added" && action != "removed" && action != "disabled")
- return;
+function onFilterLastHit()
+{
+ FilterListener.setDirty(0.002);
+}
- if ((action == "added" || action == "removed") && filter.disabled)
- {
- // Ignore adding/removing of disabled filters
- return;
- }
+function onFilterAdded(filter)
+{
+ FilterListener.setDirty(1);
+
+ if (!filter.disabled)
+ addFilter(filter);
+}
+
+function onFilterRemoved(filter)
+{
+ FilterListener.setDirty(1);
+
+ if (!filter.disabled)
+ removeFilter(filter);
+}
- if (action == "added" || (action == "disabled" && newValue == false))
+function onFilterDisabled(filter, newValue)
+{
+ FilterListener.setDirty(1);
+
+ if (newValue == false)
addFilter(filter);
else
removeFilter(filter);
- flushElemHide();
}
-/**
- * Generic notification listener
- */
-function onGenericChange(action)
+function onGenericChange()
{
- if (action == "load")
+ FilterListener.setDirty(1);
+}
+
+function onLoad()
+{
+ isDirty = 0;
+
+ defaultMatcher.clear();
+ ElemHide.clear();
+ ElemHideEmulation.clear();
+ for (let subscription of FilterStorage.subscriptions)
{
- isDirty = 0;
-
- defaultMatcher.clear();
- ElemHide.clear();
- for (let subscription of FilterStorage.subscriptions)
- if (!subscription.disabled)
- subscription.filters.forEach(addFilter);
- flushElemHide();
+ if (!subscription.disabled)
+ addFilters(subscription.filters);
}
- else if (action == "save")
- isDirty = 0;
+}
+
+function onSave()
+{
+ isDirty = 0;
}
diff --git a/data/extensions/spyblock@gnu.org/lib/filterNotifier.js b/data/extensions/spyblock@gnu.org/lib/filterNotifier.js
index 80d6769..66697cc 100644
--- a/data/extensions/spyblock@gnu.org/lib/filterNotifier.js
+++ b/data/extensions/spyblock@gnu.org/lib/filterNotifier.js
@@ -1,6 +1,6 @@
/*
* This file is part of Adblock Plus <https://adblockplus.org/>,
- * Copyright (C) 2006-2015 Eyeo GmbH
+ * Copyright (C) 2006-2017 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
@@ -15,47 +15,57 @@
* along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>.
*/
+"use strict";
+
/**
* @fileOverview This component manages listeners and calls them to distributes
* messages about filter changes.
*/
+const {EventEmitter} = require("events");
+const {desc} = require("coreUtils");
+
+const CATCH_ALL = "__all";
+
/**
- * List of registered listeners
- * @type Array of function(action, item, newValue, oldValue)
+ * @callback FilterNotifierCatchAllListener
+ * @param {string} action
+ * @param {Subscription|Filter} item
+ * @param {...*} additionalInfo
*/
-let listeners = [];
/**
* This class allows registering and triggering listeners for filter events.
* @class
*/
-let FilterNotifier = exports.FilterNotifier =
-{
+exports.FilterNotifier = Object.create(new EventEmitter(), desc({
/**
* Adds a listener
+ *
+ * @deprecated use FilterNotifier.on(action, callback)
+ * @param {FilterNotifierCatchAllListener} listener
*/
- addListener: function(/**function(action, item, newValue, oldValue)*/ listener)
+ addListener(listener)
{
- if (listeners.indexOf(listener) >= 0)
- return;
-
- listeners.push(listener);
+ let listeners = this._listeners[CATCH_ALL];
+ if (!listeners || listeners.indexOf(listener) == -1)
+ this.on(CATCH_ALL, listener);
},
/**
* Removes a listener that was previosly added via addListener
+ *
+ * @deprecated use FilterNotifier.off(action, callback)
+ * @param {FilterNotifierCatchAllListener} listener
*/
- removeListener: function(/**function(action, item, newValue, oldValue)*/ listener)
+ removeListener(listener)
{
- let index = listeners.indexOf(listener);
- if (index >= 0)
- listeners.splice(index, 1);
+ this.off(CATCH_ALL, listener);
},
/**
* Notifies listeners about an event
- * @param {String} action event code ("load", "save", "elemhideupdate",
+ * @param {string} action event code ("load", "save", "elemhideupdate",
* "subscription.added", "subscription.removed",
* "subscription.disabled", "subscription.title",
* "subscription.lastDownload", "subscription.downloadStatus",
@@ -63,11 +73,14 @@ let FilterNotifier = exports.FilterNotifier =
* "filter.added", "filter.removed", "filter.moved",
* "filter.disabled", "filter.hitCount", "filter.lastHit")
* @param {Subscription|Filter} item item that the change applies to
+ * @param {*} param1
+ * @param {*} param2
+ * @param {*} param3
+ * @deprecated use FilterNotifier.emit(action)
*/
- triggerListeners: function(action, item, param1, param2, param3)
+ triggerListeners(action, item, param1, param2, param3)
{
- let list = listeners.slice();
- for (let listener of list)
- listener(action, item, param1, param2, param3);
+ this.emit(action, item, param1, param2, param3);
+ this.emit(CATCH_ALL, 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
index dd8aea7..5bbf76c 100644
--- a/data/extensions/spyblock@gnu.org/lib/filterStorage.js
+++ b/data/extensions/spyblock@gnu.org/lib/filterStorage.js
@@ -1,6 +1,6 @@
/*
* This file is part of Adblock Plus <https://adblockplus.org/>,
- * Copyright (C) 2006-2015 Eyeo GmbH
+ * Copyright (C) 2006-2017 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
@@ -15,36 +15,42 @@
* along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>.
*/
+"use strict";
+
/**
- * @fileOverview FilterStorage class responsible for managing user's subscriptions and filters.
+ * @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");
+const {IO} = require("io");
+const {Prefs} = require("prefs");
+const {Filter, ActiveFilter} = require("filterClasses");
+const {Subscription, SpecialSubscription,
+ ExternalSubscription} = require("subscriptionClasses");
+const {FilterNotifier} = require("filterNotifier");
/**
* Version number of the filter storage file format.
- * @type Integer
+ * @type {number}
*/
-let formatVersion = 4;
+let formatVersion = 5;
/**
- * This class reads user's filters from disk, manages them in memory and writes them back.
+ * This class reads user's filters from disk, manages them in memory
+ * and writes them back.
* @class
*/
let FilterStorage = exports.FilterStorage =
{
/**
+ * Will be set to true after the initial loadFromDisk() call completes.
+ * @type {boolean}
+ */
+ initialized: false,
+
+ /**
* Version number of the patterns.ini format used.
- * @type Integer
+ * @type {number}
*/
get formatVersion()
{
@@ -52,46 +58,17 @@ let FilterStorage = exports.FilterStorage =
},
/**
- * File that the filter list has been loaded from and should be saved to
- * @type nsIFile
+ * File containing the filter list
+ * @type {string}
*/
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");
-
- // Property is configurable because of the test suite.
- Object.defineProperty(this, "sourceFile", {value: file, configurable: true});
- return file;
+ return "patterns.ini";
},
/**
* Will be set to true if no patterns.ini file exists.
- * @type Boolean
+ * @type {boolean}
*/
firstRun: false,
@@ -103,21 +80,23 @@ let FilterStorage = exports.FilterStorage =
/**
* List of filter subscriptions containing all filters
- * @type Array of Subscription
+ * @type {Subscription[]}
*/
subscriptions: [],
/**
* Map of subscriptions already on the list, by their URL/identifier
- * @type Object
+ * @type {Object}
*/
knownSubscriptions: Object.create(null),
/**
* Finds the filter group that a filter should be added to by default. Will
* return null if this group doesn't exist yet.
+ * @param {Filter} filter
+ * @return {?SpecialSubscription}
*/
- getGroupForFilter: function(/**Filter*/ filter) /**SpecialSubscription*/
+ getGroupForFilter(filter)
{
let generalSubscription = null;
for (let subscription of FilterStorage.subscriptions)
@@ -129,8 +108,11 @@ let FilterStorage = exports.FilterStorage =
return subscription;
// If this is a general subscription - store it as fallback
- if (!generalSubscription && (!subscription.defaults || !subscription.defaults.length))
+ if (!generalSubscription &&
+ (!subscription.defaults || !subscription.defaults.length))
+ {
generalSubscription = subscription;
+ }
}
}
return generalSubscription;
@@ -139,9 +121,8 @@ let FilterStorage = exports.FilterStorage =
/**
* 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)
+ addSubscription(subscription)
{
if (subscription.url in FilterStorage.knownSubscriptions)
return;
@@ -150,16 +131,14 @@ let FilterStorage = exports.FilterStorage =
FilterStorage.knownSubscriptions[subscription.url] = subscription;
addSubscriptionFilters(subscription);
- if (!silent)
- FilterNotifier.triggerListeners("subscription.added", subscription);
+ 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)
+ removeSubscription(subscription)
{
for (let i = 0; i < FilterStorage.subscriptions.length; i++)
{
@@ -169,8 +148,7 @@ let FilterStorage = exports.FilterStorage =
FilterStorage.subscriptions.splice(i--, 1);
delete FilterStorage.knownSubscriptions[subscription.url];
- if (!silent)
- FilterNotifier.triggerListeners("subscription.removed", subscription);
+ FilterNotifier.triggerListeners("subscription.removed", subscription);
return;
}
}
@@ -182,13 +160,16 @@ let FilterStorage = exports.FilterStorage =
* @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)
+ moveSubscription(subscription, insertBefore)
{
let currentPos = FilterStorage.subscriptions.indexOf(subscription);
if (currentPos < 0)
return;
- let newPos = insertBefore ? FilterStorage.subscriptions.indexOf(insertBefore) : -1;
+ let newPos = -1;
+ if (insertBefore)
+ newPos = FilterStorage.subscriptions.indexOf(insertBefore);
+
if (newPos < 0)
newPos = FilterStorage.subscriptions.length;
@@ -205,9 +186,9 @@ let FilterStorage = exports.FilterStorage =
/**
* 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
+ * @param {Filter[]} filters new filter list
*/
- updateSubscriptionFilters: function(subscription, filters)
+ updateSubscriptionFilters(subscription, filters)
{
removeSubscriptionFilters(subscription);
subscription.oldFilters = subscription.filters;
@@ -220,16 +201,20 @@ let FilterStorage = exports.FilterStorage =
/**
* 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)
+ * @param {SpecialSubscription} [subscription]
+ * particular group that the filter should be added to
+ * @param {number} [position]
+ * position within the subscription at which the filter should be added
*/
- addFilter: function(filter, subscription, position, silent)
+ addFilter(filter, subscription, position)
{
if (!subscription)
{
- if (filter.subscriptions.some(s => s instanceof SpecialSubscription && !s.disabled))
+ if (filter.subscriptions.some(s => s instanceof SpecialSubscription &&
+ !s.disabled))
+ {
return; // No need to add
+ }
subscription = FilterStorage.getGroupForFilter(filter);
}
if (!subscription)
@@ -246,25 +231,28 @@ let FilterStorage = exports.FilterStorage =
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);
+ 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
+ * the filter should be removed from (if ommited will be removed from all
+ * subscriptions)
+ * @param {number} [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)
+ removeFilter(filter, subscription, position)
{
- let subscriptions = (subscription ? [subscription] : filter.subscriptions.slice());
+ let subscriptions = (
+ subscription ? [subscription] : filter.subscriptions.slice()
+ );
for (let i = 0; i < subscriptions.length; i++)
{
- let subscription = subscriptions[i];
- if (subscription instanceof SpecialSubscription)
+ let currentSubscription = subscriptions[i];
+ if (currentSubscription instanceof SpecialSubscription)
{
let positions = [];
if (typeof position == "undefined")
@@ -272,7 +260,7 @@ let FilterStorage = exports.FilterStorage =
let index = -1;
do
{
- index = subscription.filters.indexOf(filter, index + 1);
+ index = currentSubscription.filters.indexOf(filter, index + 1);
if (index >= 0)
positions.push(index);
} while (index >= 0);
@@ -282,17 +270,19 @@ let FilterStorage = exports.FilterStorage =
for (let j = positions.length - 1; j >= 0; j--)
{
- let position = positions[j];
- if (subscription.filters[position] == filter)
+ let currentPosition = positions[j];
+ if (currentSubscription.filters[currentPosition] == filter)
{
- subscription.filters.splice(position, 1);
- if (subscription.filters.indexOf(filter) < 0)
+ currentSubscription.filters.splice(currentPosition, 1);
+ if (currentSubscription.filters.indexOf(filter) < 0)
{
- let index = filter.subscriptions.indexOf(subscription);
+ let index = filter.subscriptions.indexOf(currentSubscription);
if (index >= 0)
filter.subscriptions.splice(index, 1);
}
- FilterNotifier.triggerListeners("filter.removed", filter, subscription, position);
+ FilterNotifier.triggerListeners(
+ "filter.removed", filter, currentSubscription, currentPosition
+ );
}
}
}
@@ -302,37 +292,38 @@ let FilterStorage = exports.FilterStorage =
/**
* 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
+ * @param {SpecialSubscription} subscription filter group where the filter is
+ * located
+ * @param {number} oldPosition current position of the filter
+ * @param {number} newPosition new position of the filter
*/
- moveFilter: function(filter, subscription, oldPosition, newPosition)
+ moveFilter(filter, subscription, oldPosition, newPosition)
{
- if (!(subscription instanceof SpecialSubscription) || subscription.filters[oldPosition] != filter)
+ if (!(subscription instanceof SpecialSubscription) ||
+ subscription.filters[oldPosition] != filter)
+ {
return;
+ }
- newPosition = Math.min(Math.max(newPosition, 0), subscription.filters.length - 1);
+ 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);
+ 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)
+ increaseHitCount(filter)
{
- if (!Prefs.savestats || PrivateBrowsing.enabledForWindow(wnd) ||
- PrivateBrowsing.enabled || !(filter instanceof ActiveFilter))
- {
+ if (!Prefs.savestats || !(filter instanceof ActiveFilter))
return;
- }
filter.hitCount++;
filter.lastHit = Date.now();
@@ -340,9 +331,10 @@ let FilterStorage = exports.FilterStorage =
/**
* Resets hit count for some filters
- * @param {Array of Filter} filters filters to be reset, if null all filters will be reset
+ * @param {Filter[]} filters filters to be reset, if null all filters will
+ * be reset
*/
- resetHitCounts: function(filters)
+ resetHitCounts(filters)
{
if (!filters)
{
@@ -357,316 +349,316 @@ let FilterStorage = exports.FilterStorage =
}
},
- _loading: false,
+ /**
+ * @callback TextSink
+ * @param {string?} line
+ */
/**
- * Loads all subscriptions from the disk
- * @param {nsIFile} [sourceFile] File to read from
+ * Allows importing previously serialized filter data.
+ * @param {boolean} silent
+ * If true, no "load" notification will be sent out.
+ * @return {TextSink}
+ * Function to be called for each line of data. Calling it with null as
+ * parameter finalizes the import and replaces existing data. No changes
+ * will be applied before finalization, so import can be "aborted" by
+ * forgetting this callback.
*/
- loadFromDisk: function(sourceFile)
+ importData(silent)
{
- if (this._loading)
- return;
-
- this._loading = true;
-
- let readFile = function(sourceFile, backupIndex)
+ let parser = new INIParser();
+ return line =>
{
- let parser = new INIParser();
- IO.readFromFile(sourceFile, parser, function(e)
+ parser.process(line);
+ if (line === null)
{
- if (!e && parser.subscriptions.length == 0)
- {
- // No filter subscriptions in the file, this isn't right.
- e = new Error("No data in the file");
- }
+ let knownSubscriptions = Object.create(null);
+ for (let subscription of parser.subscriptions)
+ knownSubscriptions[subscription.url] = subscription;
- if (e)
- Cu.reportError(e);
+ this.fileProperties = parser.fileProperties;
+ this.subscriptions = parser.subscriptions;
+ this.knownSubscriptions = knownSubscriptions;
+ Filter.knownFilters = parser.knownFilters;
+ Subscription.knownSubscriptions = parser.knownSubscriptions;
- 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;
+ if (!silent)
+ FilterNotifier.triggerListeners("load");
+ }
+ };
+ },
- IO.statFile(sourceFile, function(e, statData)
- {
- if (!e && statData.exists)
- readFile(sourceFile, backupIndex);
- else
- doneReading(parser);
- });
- return;
- }
- }
- doneReading(parser);
- }.bind(this));
- }.bind(this);
+ /**
+ * Loads all subscriptions from the disk.
+ * @return {Promise} promise resolved or rejected when loading is complete
+ */
+ loadFromDisk()
+ {
+ let tryBackup = backupIndex =>
+ {
+ return this.restoreBackup(backupIndex, true).then(() =>
+ {
+ if (this.subscriptions.length == 0)
+ return tryBackup(backupIndex + 1);
+ }).catch(error =>
+ {
+ // Give up
+ });
+ };
- var doneReading = function(parser)
+ return IO.statFile(this.sourceFile).then(statData =>
{
- // 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 = Object.create(null);
- for (let i = 0; i < parser.subscriptions.length; i++)
+ if (!statData.exists)
{
- 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.firstRun = true;
+ return;
}
- this.fileProperties = parser.fileProperties;
- this.subscriptions = parser.subscriptions;
- this.knownSubscriptions = knownSubscriptions;
- Filter.knownFilters = parser.knownFilters;
- Subscription.knownSubscriptions = parser.knownSubscriptions;
-
- if (parser.userFilters)
+ let parser = this.importData(true);
+ return IO.readFromFile(this.sourceFile, parser).then(() =>
{
- for (let i = 0; i < parser.userFilters.length; i++)
+ parser(null);
+ if (this.subscriptions.length == 0)
{
- let filter = Filter.fromText(parser.userFilters[i]);
- this.addFilter(filter, null, undefined, true);
+ // No filter subscriptions in the file, this isn't right.
+ throw new Error("No data in the file");
}
- }
-
- this._loading = false;
- FilterNotifier.triggerListeners("load");
-
- if (sourceFile != this.sourceFile)
- this.saveToDisk();
-
- }.bind(this);
-
- let explicitFile;
- if (sourceFile)
+ });
+ }).catch(error =>
{
- explicitFile = true;
- readFile(sourceFile, 0);
- }
- else
+ Cu.reportError(error);
+ return tryBackup(1);
+ }).then(() =>
{
- explicitFile = false;
- sourceFile = FilterStorage.sourceFile;
+ this.initialized = true;
+ FilterNotifier.triggerListeners("load");
+ });
+ },
- let callback = function(e, statData)
- {
- if (e || !statData.exists)
- {
- this.firstRun = true;
- this._loading = false;
- FilterNotifier.triggerListeners("load");
- }
- else
- readFile(sourceFile, 0);
- }.bind(this);
+ /**
+ * Constructs the file name for a patterns.ini backup.
+ * @param {number} backupIndex
+ * number of the backup file (1 being the most recent)
+ * @return {string} backup file name
+ */
+ getBackupName(backupIndex)
+ {
+ let [name, extension] = this.sourceFile.split(".", 2);
+ return (name + "-backup" + backupIndex + "." + extension);
+ },
- if (sourceFile)
- IO.statFile(sourceFile, callback);
- else
- callback(true);
- }
+ /**
+ * Restores an automatically created backup.
+ * @param {number} backupIndex
+ * number of the backup to restore (1 being the most recent)
+ * @param {boolean} silent
+ * If true, no "load" notification will be sent out.
+ * @return {Promise} promise resolved or rejected when restoring is complete
+ */
+ restoreBackup(backupIndex, silent)
+ {
+ let backupFile = this.getBackupName(backupIndex);
+ let parser = this.importData(silent);
+ return IO.readFromFile(backupFile, parser).then(() =>
+ {
+ parser(null);
+ return this.saveToDisk();
+ });
},
- _generateFilterData: function(subscriptions)
+ /**
+ * Generator serializing filter data and yielding it line by line.
+ */
+ *exportData()
{
+ // Do not persist external subscriptions
+ let subscriptions = this.subscriptions.filter(
+ s => !(s instanceof ExternalSubscription)
+ );
+
yield "# Adblock Plus preferences";
yield "version=" + formatVersion;
- let saved = Object.create(null);
+ let saved = new Set();
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++)
+ for (let subscription of subscriptions)
{
- let subscription = subscriptions[i];
-
yield "";
subscription.serialize(buf);
if (subscription.filters.length)
{
- buf.push("", "[Subscription filters]")
+ buf.push("", "[Subscription filters]");
subscription.serializeFilters(buf);
}
- for (let k = 0; k < buf.length; k++)
- yield buf[k];
+ for (let line of buf)
+ yield line;
buf.splice(0);
}
+
+ // Save filter data
+ for (let subscription of subscriptions)
+ {
+ for (let filter of subscription.filters)
+ {
+ if (!saved.has(filter.text))
+ {
+ filter.serialize(buf);
+ saved.add(filter.text);
+ for (let line of buf)
+ yield line;
+ buf.splice(0);
+ }
+ }
+ }
},
/**
* Will be set to true if saveToDisk() is running (reentrance protection).
- * @type Boolean
+ * @type {boolean}
*/
_saving: false,
/**
* Will be set to true if a saveToDisk() call arrives while saveToDisk() is
* already running (delayed execution).
- * @type Boolean
+ * @type {boolean}
*/
_needsSave: false,
/**
* Saves all subscriptions back to disk
- * @param {nsIFile} [targetFile] File to be written
+ * @return {Promise} promise resolved or rejected when saving is complete
*/
- saveToDisk: function(targetFile)
+ saveToDisk()
{
- let explicitFile = true;
- if (!targetFile)
- {
- targetFile = FilterStorage.sourceFile;
- explicitFile = false;
- }
- if (!targetFile)
- return;
-
- if (!explicitFile && this._saving)
+ if (this._saving)
{
this._needsSave = true;
return;
}
- // Make sure the file's parent directory exists
- try {
- targetFile.parent.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
- } catch (e) {}
+ this._saving = true;
- let writeFilters = function()
+ return Promise.resolve().then(() =>
{
- IO.writeToFile(targetFile, this._generateFilterData(subscriptions), function(e)
- {
- if (!explicitFile)
- this._saving = false;
-
- if (e)
- Cu.reportError(e);
-
- if (!explicitFile && this._needsSave)
- {
- this._needsSave = false;
- this.saveToDisk();
- }
- else
- FilterNotifier.triggerListeners("save");
- }.bind(this));
- }.bind(this);
+ // First check whether we need to create a backup
+ if (Prefs.patternsbackups <= 0)
+ return false;
- let checkBackupRequired = function(callbackNotRequired, callbackRequired)
- {
- if (explicitFile || Prefs.patternsbackups <= 0)
- callbackNotRequired();
- else
+ return IO.statFile(this.sourceFile).then(statData =>
{
- IO.statFile(targetFile, function(e, statData)
+ if (!statData.exists)
+ return false;
+
+ return IO.statFile(this.getBackupName(1)).then(backupStatData =>
{
- if (e || !statData.exists)
- callbackNotRequired();
- else
+ if (backupStatData.exists &&
+ (Date.now() - backupStatData.lastModified) / 3600000 <
+ Prefs.patternsbackupinterval)
{
- 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();
- });
+ return false;
}
+ return true;
});
- }
- }.bind(this);
-
- let removeLastBackup = function(part1, part2)
+ });
+ }).then(backupRequired =>
{
- let file = targetFile.clone();
- file.leafName = part1 + "-backup" + Prefs.patternsbackups + part2;
- IO.removeFile(file, (e) => renameBackup(part1, part2, Prefs.patternsbackups - 1));
- }.bind(this);
+ if (!backupRequired)
+ return;
- let renameBackup = function(part1, part2, index)
- {
- if (index > 0)
+ let ignoreErrors = error =>
{
- let fromFile = targetFile.clone();
- fromFile.leafName = part1 + "-backup" + index + part2;
-
- let toName = part1 + "-backup" + (index + 1) + part2;
+ // Expected error, backup file doesn't exist.
+ };
- IO.renameFile(fromFile, toName, (e) => renameBackup(part1, part2, index - 1));
- }
- else
+ let renameBackup = index =>
{
- let toFile = targetFile.clone();
- toFile.leafName = part1 + "-backup" + (index + 1) + part2;
-
- IO.copyFile(targetFile, toFile, writeFilters);
- }
- }.bind(this);
+ if (index > 0)
+ {
+ return IO.renameFile(this.getBackupName(index),
+ this.getBackupName(index + 1))
+ .catch(ignoreErrors)
+ .then(() => renameBackup(index - 1));
+ }
- // Do not persist external subscriptions
- let subscriptions = this.subscriptions.filter((s) => !(s instanceof ExternalSubscription));
- if (!explicitFile)
- this._saving = true;
+ return IO.renameFile(this.sourceFile, this.getBackupName(1))
+ .catch(ignoreErrors);
+ };
- checkBackupRequired(writeFilters, removeLastBackup);
+ // Rename existing files
+ return renameBackup(Prefs.patternsbackups - 1);
+ }).catch(error =>
+ {
+ // Errors during backup creation shouldn't prevent writing filters.
+ Cu.reportError(error);
+ }).then(() =>
+ {
+ return IO.writeToFile(this.sourceFile, this.exportData());
+ }).then(() =>
+ {
+ FilterNotifier.triggerListeners("save");
+ }).catch(error =>
+ {
+ // If saving failed, report error but continue - we still have to process
+ // flags.
+ Cu.reportError(error);
+ }).then(() =>
+ {
+ this._saving = false;
+ if (this._needsSave)
+ {
+ this._needsSave = false;
+ this.saveToDisk();
+ }
+ });
},
/**
- * Returns the list of existing backup files.
+ * @typedef FileInfo
+ * @type {object}
+ * @property {nsIFile} file
+ * @property {number} lastModified
*/
- getBackupFiles: function() /**nsIFile[]*/
+
+ /**
+ * Returns a promise resolving in a list of existing backup files.
+ * @return {Promise.<FileInfo[]>}
+ */
+ getBackupFiles()
{
- // TODO: This method should be asynchronous
- let result = [];
+ let backups = [];
- let [, part1, part2] = /^(.*)(\.\w+)$/.exec(FilterStorage.sourceFile.leafName) || [null, FilterStorage.sourceFile.leafName, ""];
- for (let i = 1; ; i++)
+ let checkBackupFile = index =>
{
- let file = FilterStorage.sourceFile.clone();
- file.leafName = part1 + "-backup" + i + part2;
- if (file.exists())
- result.push(file);
- else
- break;
- }
- return result;
+ return IO.statFile(this.getBackupName(index)).then(statData =>
+ {
+ if (!statData.exists)
+ return backups;
+
+ backups.push({
+ index,
+ lastModified: statData.lastModified
+ });
+ return checkBackupFile(index + 1);
+ }).catch(error =>
+ {
+ // Something went wrong, return whatever data we got so far.
+ Cu.reportError(error);
+ return backups;
+ });
+ };
+
+ return checkBackupFile(1);
}
};
/**
* Joins subscription's filters to the subscription without any notifications.
- * @param {Subscription} subscription filter subscription that should be connected to its filters
+ * @param {Subscription} subscription
+ * filter subscription that should be connected to its filters
*/
function addSubscriptionFilters(subscription)
{
@@ -678,7 +670,8 @@ function addSubscriptionFilters(subscription)
}
/**
- * Removes subscription's filters from the subscription without any notifications.
+ * Removes subscription's filters from the subscription without any
+ * notifications.
* @param {Subscription} subscription filter subscription to be removed
*/
function removeSubscriptionFilters(subscription)
@@ -695,74 +688,7 @@ function removeSubscriptionFilters(subscription)
}
/**
- * 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.
+ * Listener returned by FilterStorage.importData(), parses filter data.
* @constructor
*/
function INIParser()
@@ -777,14 +703,13 @@ INIParser.prototype =
linesProcessed: 0,
subscriptions: null,
knownFilters: null,
- knownSubscriptions : null,
+ knownSubscriptions: null,
wantObj: true,
fileProperties: null,
curObj: null,
curSection: null,
- userFilters: null,
- process: function(val)
+ process(val)
{
let origKnownFilters = Filter.knownFilters;
Filter.knownFilters = this.knownFilters;
@@ -803,20 +728,21 @@ INIParser.prototype =
switch (this.curSection)
{
case "filter":
- case "pattern":
if ("text" in this.curObj)
Filter.fromObject(this.curObj);
break;
- case "subscription":
+ 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];
+ let subscription = this.subscriptions[
+ this.subscriptions.length - 1
+ ];
for (let text of this.curObj)
{
let filter = Filter.fromText(text);
@@ -825,9 +751,6 @@ INIParser.prototype =
}
}
break;
- case "user patterns":
- this.userFilters = this.curObj;
- break;
}
}
@@ -838,14 +761,11 @@ INIParser.prototype =
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;
@@ -862,11 +782,5 @@ INIParser.prototype =
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
index 5e60b54..0a22513 100644
--- a/data/extensions/spyblock@gnu.org/lib/io.js
+++ b/data/extensions/spyblock@gnu.org/lib/io.js
@@ -1,6 +1,6 @@
/*
* This file is part of Adblock Plus <https://adblockplus.org/>,
- * Copyright (C) 2006-2015 Eyeo GmbH
+ * Copyright (C) 2006-2017 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
@@ -15,318 +15,265 @@
* 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);
+"use strict";
-let {Prefs} = require("prefs");
+let {IO: LegacyIO} = require("legacyIO");
let {Utils} = require("utils");
-let firstRead = true;
-const BUFFER_SIZE = 0x80000; // 512kB
+let webextension = require("webextension");
+let messageID = 0;
+let messageCallbacks = new Map();
-let IO = exports.IO =
+webextension.then(port =>
{
- /**
- * Retrieves the platform-dependent line break string.
- */
- get lineBreak()
+ port.onMessage.addListener(message =>
{
- let lineBreak = (Services.appinfo.OS == "WINNT" ? "\r\n" : "\n");
- Object.defineProperty(this, "lineBreak", {value: lineBreak});
- return 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) {}
+ let {id} = message;
+ let callbacks = messageCallbacks.get(id);
+ if (callbacks)
+ {
+ messageCallbacks.delete(id);
- return null;
- },
+ if (message.success)
+ callbacks.resolve(message.result);
+ else
+ callbacks.reject(message.result);
+ }
+ });
+});
- /**
- * 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)
+function callWebExt(method, ...args)
+{
+ return webextension.then(port =>
{
- try
+ return new Promise((resolve, reject) =>
{
- let processing = false;
- let buffer = "";
- let loaded = false;
- let error = null;
+ let id = ++messageID;
+ messageCallbacks.set(id, {resolve, reject});
+ port.postMessage({id, method, args});
+ });
+ });
+}
- let onProgress = function(data)
- {
- 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);
+function callLegacy(method, ...args)
+{
+ return new Promise((resolve, reject) =>
+ {
+ LegacyIO[method](...args, (error, result) =>
+ {
+ if (error)
+ reject(error);
+ else
+ resolve(result);
+ });
+ });
+}
- if (loaded)
- {
- loaded = false;
- onSuccess();
- }
+function legacyFile(fileName)
+{
+ let file = LegacyIO.resolveFilePath("adblockplus");
+ file.append(fileName);
+ return file;
+}
- if (error)
- {
- let param = error;
- error = null;
- onError(param);
- }
- }
- }
- else
- buffer += data;
- };
+function ensureDirExists(file)
+{
+ if (!file.exists())
+ {
+ ensureDirExists(file.parent);
+ file.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+ }
+}
- let onSuccess = function()
+let fallback = {
+ readFromFile(fileName, listener)
+ {
+ let wrapper = {
+ process(line)
{
- if (processing)
- {
- // Still processing data, delay processing this event.
- loaded = true;
- return;
- }
-
- if (buffer !== "")
- listener.process(buffer);
- listener.process(null);
+ if (line !== null)
+ listener(line);
+ }
+ };
+ return callLegacy("readFromFile", legacyFile(fileName), wrapper);
+ },
- callback(null);
- };
+ writeToFile(fileName, data)
+ {
+ let file = legacyFile(fileName);
+ ensureDirExists(file.parent);
+ return callLegacy("writeToFile", file, data);
+ },
- let onError = function(e)
- {
- if (processing)
- {
- // Still processing data, delay processing this event.
- error = e;
- return;
- }
+ copyFile(fromFile, toFile)
+ {
+ return callLegacy("copyFile", legacyFile(fromFile), legacyFile(toFile));
+ },
- callback(e);
- };
+ renameFile(fromFile, newName)
+ {
+ return callLegacy("renameFile", legacyFile(fromFile), newName);
+ },
- let decoder = new TextDecoder();
- 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;
+ removeFile(fileName)
+ {
+ return callLegacy("removeFile", legacyFile(fileName));
+ },
- let f = yield OS.File.open(file.path, {read: true});
- while (true)
- {
- let array = yield f.read(BUFFER_SIZE);
- if (!array.length)
- break;
+ statFile(fileName)
+ {
+ return callLegacy("statFile", legacyFile(fileName));
+ }
+};
- let data = decoder.decode(array, {stream: true});
- onProgress(data);
- }
- yield f.close();
- }.bind(this)).then(onSuccess, onError);
- }
- catch (e)
- {
- callback(e);
- }
- },
+exports.IO =
+{
+ /**
+ * @callback TextSink
+ * @param {string} line
+ */
/**
- * Writes string data to a file in UTF-8 format asynchronously. The callback
- * will be called when the write operation is done.
+ * Reads text lines from a file.
+ * @param {string} fileName
+ * Name of the file to be read
+ * @param {TextSink} listener
+ * Function that will be called for each line in the file
+ * @return {Promise}
+ * Promise to be resolved or rejected once the operation is completed
*/
- writeToFile: function(/**nsIFile*/ file, /**Iterator*/ data, /**Function*/ callback)
+ readFromFile(fileName, listener)
{
- try
+ return callWebExt("readFromFile", fileName).then(contents =>
{
- let encoder = new TextEncoder();
-
- Task.spawn(function()
+ return new Promise((resolve, reject) =>
{
- // 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 lineIndex = 0;
- 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)
+ function processBatch()
{
- buf.push(line);
- bufLen += line.length;
- if (bufLen >= BUFFER_SIZE)
- yield writeChunk();
+ while (lineIndex < contents.length)
+ {
+ listener(contents[lineIndex++]);
+ if (lineIndex % 1000 == 0)
+ {
+ Utils.runAsync(processBatch);
+ return;
+ }
+ }
+ resolve();
}
- if (bufLen)
- yield writeChunk();
+ processBatch();
+ });
+ });
+ },
- // 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);
- }
+ /**
+ * Writes text lines to a file.
+ * @param {string} fileName
+ * Name of the file to be written
+ * @param {Iterable.<string>} data
+ * An array-like or iterable object containing the lines (without line
+ * endings)
+ * @return {Promise}
+ * Promise to be resolved or rejected once the operation is completed
+ */
+ writeToFile(fileName, data)
+ {
+ return callWebExt("writeToFile", fileName, Array.from(data));
},
/**
- * Copies a file asynchronously. The callback will be called when the copy
- * operation is done.
+ * Copies a file.
+ * @param {string} fromFile
+ * Name of the file to be copied
+ * @param {string} toFile
+ * Name of the file to be written, will be overwritten if exists
+ * @return {Promise}
+ * Promise to be resolved or rejected once the operation is completed
*/
- copyFile: function(/**nsIFile*/ fromFile, /**nsIFile*/ toFile, /**Function*/ callback)
+ copyFile(fromFile, toFile)
{
- try
- {
- let promise = OS.File.copy(fromFile.path, toFile.path);
- promise.then(callback.bind(null, null), callback);
- }
- catch (e)
- {
- callback(e);
- }
+ return callWebExt("copyFile", fromFile, toFile);
},
/**
- * Renames a file within the same directory, will call callback when done.
+ * Renames a file.
+ * @param {string} fromFile
+ * Name of the file to be renamed
+ * @param {string} newName
+ * New file name, will be overwritten if exists
+ * @return {Promise}
+ * Promise to be resolved or rejected once the operation is completed
*/
- renameFile: function(/**nsIFile*/ fromFile, /**String*/ newName, /**Function*/ callback)
+ renameFile(fromFile, newName)
{
- 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);
- }
+ return callWebExt("renameFile", fromFile, newName);
},
/**
- * Removes a file, will call callback when done.
+ * Removes a file.
+ * @param {string} fileName
+ * Name of the file to be removed
+ * @return {Promise}
+ * Promise to be resolved or rejected once the operation is completed
*/
- removeFile: function(/**nsIFile*/ file, /**Function*/ callback)
+ removeFile(fileName)
{
- try
- {
- let promise = OS.File.remove(file.path);
- promise.then(callback.bind(null, null), callback);
- }
- catch(e)
- {
- callback(e);
- }
+ return callWebExt("removeFile", fileName);
},
/**
- * Gets file information such as whether the file exists.
+ * @typedef StatData
+ * @type {object}
+ * @property {boolean} exists
+ * true if the file exists
+ * @property {number} lastModified
+ * file modification time in milliseconds
*/
- statFile: function(/**nsIFile*/ file, /**Function*/ callback)
+
+ /**
+ * Retrieves file metadata.
+ * @param {string} fileName
+ * Name of the file to be looked up
+ * @return {Promise.<StatData>}
+ * Promise to be resolved with file metadata once the operation is
+ * completed
+ */
+ statFile(fileName)
+ {
+ return callWebExt("statFile", fileName);
+ }
+};
+
+let {application} = require("info");
+if (application != "firefox" && application != "fennec2")
+{
+ // Currently, only Firefox has a working WebExtensions implementation, other
+ // applications should just use the fallback.
+ exports.IO = fallback;
+}
+else
+{
+ // Add fallbacks to IO methods - fall back to legacy I/O if file wasn't found.
+ for (let name of Object.getOwnPropertyNames(exports.IO))
{
- try
+ // No fallback for writeToFile method, new data should always be stored to
+ // new storage only.
+ if (name == "writeToFile")
+ continue;
+
+ let method = exports.IO[name];
+ let fallbackMethod = fallback[name];
+ exports.IO[name] = (...args) =>
{
- 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)
+ return method(...args).catch(error =>
{
- if (e.becauseNoSuchFile)
- {
- callback(null, {
- exists: false,
- isDirectory: false,
- isFile: false,
- lastModified: 0
- });
- }
- else
- callback(e);
+ if (error == "NoSuchFile")
+ return fallbackMethod(...args);
+ throw error;
});
- }
- catch(e)
- {
- callback(e);
- }
+ };
}
}
diff --git a/data/extensions/spyblock@gnu.org/lib/keySelector.js b/data/extensions/spyblock@gnu.org/lib/keySelector.js
index 01d9cd0..151f50d 100644
--- a/data/extensions/spyblock@gnu.org/lib/keySelector.js
+++ b/data/extensions/spyblock@gnu.org/lib/keySelector.js
@@ -4,16 +4,26 @@
Cu.import("resource://gre/modules/Services.jsm");
-let validModifiers =
+let validModifiers = Object.create(null);
+validModifiers.ACCEL = null;
+validModifiers.CTRL = "control";
+validModifiers.CONTROL = "control";
+validModifiers.SHIFT = "shift";
+validModifiers.ALT = "alt";
+validModifiers.META = "meta";
+
+let bindingsKeys = null;
+(function()
{
- ACCEL: null,
- CTRL: "control",
- CONTROL: "control",
- SHIFT: "shift",
- ALT: "alt",
- META: "meta",
- __proto__: null
-};
+ let request = new XMLHttpRequest();
+ request.open("GET", "chrome://global/content/platformHTMLBindings.xml");
+ request.addEventListener("load", () =>
+ {
+ bindingsKeys = request.responseXML.getElementsByTagName("handler");
+ });
+ request.send();
+})();
+
/**
* Sets the correct value of validModifiers.ACCEL.
@@ -66,9 +76,11 @@ KeySelector.prototype =
if (!validModifiers.ACCEL)
initAccelKey();
- this._existingShortcuts = {__proto__: null};
+ this._existingShortcuts = Object.create(null);
- let keys = window.document.getElementsByTagName("key");
+ let keys = Array.prototype.slice.apply(window.document.getElementsByTagName("key"));
+ if (bindingsKeys)
+ keys.push.apply(keys, bindingsKeys);
for (let i = 0; i < keys.length; i++)
{
let key = keys[i];
@@ -95,7 +107,7 @@ KeySelector.prototype =
let keyModifiers = key.getAttribute("modifiers");
if (keyModifiers)
- for each (let modifier in keyModifiers.toUpperCase().match(/\w+/g))
+ for (let modifier of keyModifiers.toUpperCase().match(/\w+/g))
if (modifier in validModifiers)
keyData[validModifiers[modifier]] = true;
@@ -110,7 +122,7 @@ KeySelector.prototype =
*/
selectKey: function(/**String*/ variants) /**Object*/
{
- for each (let variant in variants.split(/\s*,\s*/))
+ for (let variant of variants.split(/\s*,\s*/))
{
if (!variant)
continue;
@@ -125,7 +137,7 @@ KeySelector.prototype =
code: null,
codeName: null
};
- for each (let part in variant.toUpperCase().split(/\s+/))
+ for (let part of variant.toUpperCase().split(/\s+/))
{
if (part in validModifiers)
keyData[validModifiers[part]] = true;
diff --git a/data/extensions/spyblock@gnu.org/lib/legacyIO.js b/data/extensions/spyblock@gnu.org/lib/legacyIO.js
new file mode 100644
index 0000000..5549d96
--- /dev/null
+++ b/data/extensions/spyblock@gnu.org/lib/legacyIO.js
@@ -0,0 +1,335 @@
+/*
+ * This file is part of Adblock Plus <https://adblockplus.org/>,
+ * Copyright (C) 2006-2017 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 {Prefs} = require("prefs");
+let {Utils} = require("utils");
+
+let firstRead = true;
+const BUFFER_SIZE = 0x80000; // 512kB
+
+let IO = exports.IO =
+{
+ /**
+ * Retrieves the platform-dependent line break string.
+ */
+ get lineBreak()
+ {
+ let lineBreak = (Services.appinfo.OS == "WINNT" ? "\r\n" : "\n");
+ Object.defineProperty(this, "lineBreak", {value: lineBreak});
+ return 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)
+ {
+ try
+ {
+ let processing = false;
+ let buffer = "";
+ let loaded = false;
+ let error = null;
+
+ let onProgress = function*(data)
+ {
+ 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 = "";
+ yield* onProgress(data);
+
+ if (loaded)
+ {
+ loaded = false;
+ onSuccess();
+ }
+
+ if (error)
+ {
+ let param = error;
+ error = null;
+ onError(param);
+ }
+ }
+ }
+ else
+ buffer += data;
+ };
+
+ let onSuccess = function()
+ {
+ if (processing)
+ {
+ // Still processing data, delay processing this event.
+ loaded = true;
+ return;
+ }
+
+ // We are ignoring return value of listener.process() here because
+ // turning this callback into a generator would be complicated, and
+ // delaying isn't really necessary for the last two calls.
+ if (buffer !== "")
+ listener.process(buffer);
+ listener.process(null);
+
+ callback(null);
+ };
+
+ let onError = function(e)
+ {
+ if (processing)
+ {
+ // Still processing data, delay processing this event.
+ error = e;
+ return;
+ }
+
+ callback(e);
+ };
+
+ let decoder = new TextDecoder();
+ 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});
+ while (true)
+ {
+ let array = yield f.read(BUFFER_SIZE);
+ if (!array.length)
+ break;
+
+ let data = decoder.decode(array, {stream: true});
+ yield* onProgress(data);
+ }
+ 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)
+ {
+ 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 of 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
+ {
+ let 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/main.js b/data/extensions/spyblock@gnu.org/lib/main.js
index fa84d9e..c0d4733 100644
--- a/data/extensions/spyblock@gnu.org/lib/main.js
+++ b/data/extensions/spyblock@gnu.org/lib/main.js
@@ -1,6 +1,6 @@
/*
* This file is part of Adblock Plus <https://adblockplus.org/>,
- * Copyright (C) 2006-2015 Eyeo GmbH
+ * Copyright (C) 2006-2017 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
@@ -22,6 +22,7 @@
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
+bootstrapChildProcesses();
registerPublicAPI();
require("filterListener");
require("contentPolicy");
@@ -30,6 +31,26 @@ require("notification");
require("sync");
require("messageResponder");
require("ui");
+require("objectTabs");
+require("elemHideFF");
+require("elemHideEmulation");
+
+function bootstrapChildProcesses()
+{
+ let info = require("info");
+
+ let processScript = info.addonRoot + "lib/child/bootstrap.js?" +
+ Math.random() + "&info=" + encodeURIComponent(JSON.stringify(info));
+ let messageManager = Cc["@mozilla.org/parentprocessmessagemanager;1"]
+ .getService(Ci.nsIProcessScriptLoader)
+ .QueryInterface(Ci.nsIMessageBroadcaster);
+ messageManager.loadProcessScript(processScript, true);
+
+ onShutdown.add(() => {
+ messageManager.broadcastAsyncMessage("AdblockPlus:Shutdown", processScript);
+ messageManager.removeDelayedProcessScript(processScript);
+ });
+}
function registerPublicAPI()
{
diff --git a/data/extensions/spyblock@gnu.org/lib/matcher.js b/data/extensions/spyblock@gnu.org/lib/matcher.js
index 59ef1e7..02573bd 100644
--- a/data/extensions/spyblock@gnu.org/lib/matcher.js
+++ b/data/extensions/spyblock@gnu.org/lib/matcher.js
@@ -1,6 +1,6 @@
/*
* This file is part of Adblock Plus <https://adblockplus.org/>,
- * Copyright (C) 2006-2015 Eyeo GmbH
+ * Copyright (C) 2006-2017 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
@@ -15,11 +15,14 @@
* along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>.
*/
+"use strict";
+
/**
- * @fileOverview Matcher class implementing matching addresses against a list of filters.
+ * @fileOverview Matcher class implementing matching addresses against
+ * a list of filters.
*/
-let {Filter, RegExpFilter, WhitelistFilter} = require("filterClasses");
+const {Filter, WhitelistFilter} = require("filterClasses");
/**
* Blacklist/whitelist filter matching
@@ -34,20 +37,20 @@ exports.Matcher = Matcher;
Matcher.prototype = {
/**
* Lookup table for filters by their associated keyword
- * @type Object
+ * @type {Object}
*/
filterByKeyword: null,
/**
* Lookup table for keywords by the filter text
- * @type Object
+ * @type {Object}
*/
keywordByFilter: null,
/**
* Removes all known filters
*/
- clear: function()
+ clear()
{
this.filterByKeyword = Object.create(null);
this.keywordByFilter = Object.create(null);
@@ -57,7 +60,7 @@ Matcher.prototype = {
* Adds a filter to the matcher
* @param {RegExpFilter} filter
*/
- add: function(filter)
+ add(filter)
{
if (filter.text in this.keywordByFilter)
return;
@@ -78,7 +81,7 @@ Matcher.prototype = {
* Removes a filter from the matcher
* @param {RegExpFilter} filter
*/
- remove: function(filter)
+ remove(filter)
{
if (!(filter.text in this.keywordByFilter))
return;
@@ -103,13 +106,13 @@ Matcher.prototype = {
/**
* Chooses a keyword to be associated with the filter
- * @param {String} text text representation of the filter
- * @return {String} keyword (might be empty string)
+ * @param {Filter} filter
+ * @return {string} keyword or an empty string if no keyword could be found
*/
- findKeyword: function(filter)
+ findKeyword(filter)
{
let result = "";
- let text = filter.text;
+ let {text} = filter;
if (Filter.regexpRegExp.test(text))
return result;
@@ -122,7 +125,9 @@ Matcher.prototype = {
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);
+ let candidates = text.toLowerCase().match(
+ /[^a-z0-9%*][a-z0-9%]{3,}(?=[^a-z0-9%*])/g
+ );
if (!candidates)
return result;
@@ -133,7 +138,8 @@ Matcher.prototype = {
{
let candidate = candidates[i].substr(1);
let count = (candidate in hash ? hash[candidate].length : 0);
- if (count < resultCount || (count == resultCount && candidate.length > resultLength))
+ if (count < resultCount ||
+ (count == resultCount && candidate.length > resultLength))
{
result = candidate;
resultCount = count;
@@ -145,33 +151,50 @@ Matcher.prototype = {
/**
* Checks whether a particular filter is being matched against.
+ * @param {RegExpFilter} filter
+ * @return {boolean}
*/
- hasFilter: function(/**RegExpFilter*/ filter) /**Boolean*/
+ hasFilter(filter)
{
return (filter.text in this.keywordByFilter);
},
/**
* Returns the keyword used for a filter, null for unknown filters.
+ * @param {RegExpFilter} filter
+ * @return {string}
*/
- getKeywordForFilter: function(/**RegExpFilter*/ filter) /**String*/
+ getKeywordForFilter(filter)
{
if (filter.text in this.keywordByFilter)
return this.keywordByFilter[filter.text];
- else
- return null;
+ return null;
},
/**
* Checks whether the entries for a particular keyword match a URL
+ * @param {string} keyword
+ * @param {string} location
+ * @param {number} typeMask
+ * @param {string} docDomain
+ * @param {boolean} thirdParty
+ * @param {string} sitekey
+ * @param {boolean} specificOnly
+ * @return {?Filter}
*/
- _checkEntryMatch: function(keyword, location, contentType, docDomain, thirdParty, sitekey, privatenode)
+ _checkEntryMatch(keyword, location, typeMask, docDomain, thirdParty, sitekey,
+ specificOnly, 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, sitekey, privatenode))
+
+ if (specificOnly && filter.isGeneric() &&
+ !(filter instanceof WhitelistFilter))
+ continue;
+
+ if (filter.matches(location, typeMask, docDomain, thirdParty, sitekey, privatenode))
return filter;
}
return null;
@@ -179,14 +202,22 @@ Matcher.prototype = {
/**
* 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
- * @param {String} sitekey public key provided by the document
- * @return {RegExpFilter} matching filter or null
+ * @param {string} location
+ * URL to be tested
+ * @param {number} typeMask
+ * bitmask of content / request types to match
+ * @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
+ * @param {string} sitekey
+ * public key provided by the document
+ * @param {boolean} specificOnly
+ * should be true if generic matches should be ignored
+ * @return {?RegExpFilter}
+ * matching filter or null
*/
- matchesAny: function(location, contentType, docDomain, thirdParty, sitekey)
+ matchesAny(location, typeMask, docDomain, thirdParty, sitekey, specificOnly)
{
let candidates = location.toLowerCase().match(/[a-z0-9%]{3,}/g);
if (candidates === null)
@@ -197,7 +228,9 @@ Matcher.prototype = {
let substr = candidates[i];
if (substr in this.filterByKeyword)
{
- let result = this._checkEntryMatch(substr, location, contentType, docDomain, thirdParty, sitekey);
+ let result = this._checkEntryMatch(substr, location, typeMask,
+ docDomain, thirdParty, sitekey,
+ specificOnly);
if (result)
return result;
}
@@ -211,6 +244,7 @@ Matcher.prototype = {
* Combines a matcher for blocking and exception rules, automatically sorts
* rules into two Matcher instances.
* @constructor
+ * @augments Matcher
*/
function CombinedMatcher()
{
@@ -222,7 +256,7 @@ exports.CombinedMatcher = CombinedMatcher;
/**
* Maximal number of matching cache entries to be kept
- * @type Number
+ * @type {number}
*/
CombinedMatcher.maxCacheEntries = 1000;
@@ -230,32 +264,32 @@ CombinedMatcher.prototype =
{
/**
* Matcher for blocking rules.
- * @type Matcher
+ * @type {Matcher}
*/
blacklist: null,
/**
* Matcher for exception rules.
- * @type Matcher
+ * @type {Matcher}
*/
whitelist: null,
/**
* Lookup table of previous matchesAny results
- * @type Object
+ * @type {Object}
*/
resultCache: null,
/**
* Number of entries in resultCache
- * @type Number
+ * @type {number}
*/
cacheEntries: 0,
/**
* @see Matcher#clear
*/
- clear: function()
+ clear()
{
this.blacklist.clear();
this.whitelist.clear();
@@ -265,8 +299,9 @@ CombinedMatcher.prototype =
/**
* @see Matcher#add
+ * @param {Filter} filter
*/
- add: function(filter)
+ add(filter)
{
if (filter instanceof WhitelistFilter)
this.whitelist.add(filter);
@@ -282,8 +317,9 @@ CombinedMatcher.prototype =
/**
* @see Matcher#remove
+ * @param {Filter} filter
*/
- remove: function(filter)
+ remove(filter)
{
if (filter instanceof WhitelistFilter)
this.whitelist.remove(filter);
@@ -299,55 +335,63 @@ CombinedMatcher.prototype =
/**
* @see Matcher#findKeyword
+ * @param {Filter} filter
+ * @return {string} keyword
*/
- findKeyword: function(filter)
+ findKeyword(filter)
{
if (filter instanceof WhitelistFilter)
return this.whitelist.findKeyword(filter);
- else
- return this.blacklist.findKeyword(filter);
+ return this.blacklist.findKeyword(filter);
},
/**
* @see Matcher#hasFilter
+ * @param {Filter} filter
+ * @return {boolean}
*/
- hasFilter: function(filter)
+ hasFilter(filter)
{
if (filter instanceof WhitelistFilter)
return this.whitelist.hasFilter(filter);
- else
- return this.blacklist.hasFilter(filter);
+ return this.blacklist.hasFilter(filter);
},
/**
* @see Matcher#getKeywordForFilter
+ * @param {Filter} filter
+ * @return {string} keyword
*/
- getKeywordForFilter: function(filter)
+ getKeywordForFilter(filter)
{
if (filter instanceof WhitelistFilter)
return this.whitelist.getKeywordForFilter(filter);
- else
- return this.blacklist.getKeywordForFilter(filter);
+ return this.blacklist.getKeywordForFilter(filter);
},
/**
* Checks whether a particular filter is slow
+ * @param {RegExpFilter} filter
+ * @return {boolean}
*/
- isSlowFilter: function(/**RegExpFilter*/ filter) /**Boolean*/
+ isSlowFilter(filter)
{
- let matcher = (filter instanceof WhitelistFilter ? this.whitelist : this.blacklist);
+ let matcher = (
+ filter instanceof WhitelistFilter ? this.whitelist : this.blacklist
+ );
if (matcher.hasFilter(filter))
return !matcher.getKeywordForFilter(filter);
- else
- return !matcher.findKeyword(filter);
+ return !matcher.findKeyword(filter);
},
/**
* Optimized filter matching testing both whitelist and blacklist matchers
* simultaneously. For parameters see Matcher.matchesAny().
* @see Matcher#matchesAny
+ * @inheritdoc
*/
- matchesAnyInternal: function(location, contentType, docDomain, thirdParty, sitekey, privatenode)
+ matchesAnyInternal(location, typeMask, docDomain, thirdParty, sitekey,
+ specificOnly, privatenode)
{
let candidates = location.toLowerCase().match(/[a-z0-9%]{3,}/g);
if (candidates === null)
@@ -360,28 +404,37 @@ CombinedMatcher.prototype =
let substr = candidates[i];
if (substr in this.whitelist.filterByKeyword)
{
- let result = this.whitelist._checkEntryMatch(substr, location, contentType, docDomain, thirdParty, sitekey, privatenode);
+ let result = this.whitelist._checkEntryMatch(
+ substr, location, typeMask, docDomain, thirdParty, sitekey, privatenode
+ );
if (result)
return result;
}
if (substr in this.blacklist.filterByKeyword && blacklistHit === null)
- blacklistHit = this.blacklist._checkEntryMatch(substr, location, contentType, docDomain, thirdParty, sitekey, privatenode);
+ {
+ blacklistHit = this.blacklist._checkEntryMatch(
+ substr, location, typeMask, docDomain, thirdParty, sitekey,
+ specificOnly, privatenode
+ );
+ }
}
return blacklistHit;
},
/**
* @see Matcher#matchesAny
+ * @inheritdoc
*/
- matchesAny: function(location, contentType, docDomain, thirdParty, sitekey, privatenode)
+ matchesAny(location, typeMask, docDomain, thirdParty, sitekey, specificOnly, privatenode)
{
- let key = location + " " + contentType + " " + docDomain + " " + thirdParty + " " + sitekey;
- if (!privatenode){
+ let key = location + " " + typeMask + " " + docDomain + " " + thirdParty +
+ " " + sitekey + " " + specificOnly;
+ if (!privatenode)
if (key in this.resultCache)
return this.resultCache[key];
- }
- let result = this.matchesAnyInternal(location, contentType, docDomain, thirdParty, sitekey, privatenode);
+ let result = this.matchesAnyInternal(location, typeMask, docDomain,
+ thirdParty, sitekey, specificOnly, privatenode);
if (this.cacheEntries >= CombinedMatcher.maxCacheEntries)
{
@@ -396,10 +449,10 @@ CombinedMatcher.prototype =
return result;
}
-}
+};
/**
* Shared CombinedMatcher instance that should usually be used.
- * @type CombinedMatcher
+ * @type {CombinedMatcher}
*/
-let defaultMatcher = exports.defaultMatcher = new CombinedMatcher();
+exports.defaultMatcher = new CombinedMatcher();
diff --git a/data/extensions/spyblock@gnu.org/lib/messageResponder.js b/data/extensions/spyblock@gnu.org/lib/messageResponder.js
index 0f5ff0e..5dfc2e9 100644
--- a/data/extensions/spyblock@gnu.org/lib/messageResponder.js
+++ b/data/extensions/spyblock@gnu.org/lib/messageResponder.js
@@ -1,6 +1,6 @@
/*
* This file is part of Adblock Plus <https://adblockplus.org/>,
- * Copyright (C) 2006-2015 Eyeo GmbH
+ * Copyright (C) 2006-2017 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
@@ -15,166 +15,417 @@
* along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>.
*/
+/* globals require */
+
+"use strict";
+
(function(global)
{
- if (!global.ext)
- global.ext = require("ext_background");
-
- var Utils = require("utils").Utils;
- var FilterStorage = require("filterStorage").FilterStorage;
- var FilterNotifier = require("filterNotifier").FilterNotifier;
- var defaultMatcher = require("matcher").defaultMatcher;
- var BlockingFilter = require("filterClasses").BlockingFilter;
- var Synchronizer = require("synchronizer").Synchronizer;
-
- var subscriptionClasses = require("subscriptionClasses");
- var Subscription = subscriptionClasses.Subscription;
- var DownloadableSubscription = subscriptionClasses.DownloadableSubscription;
- var SpecialSubscription = subscriptionClasses.SpecialSubscription;
-
- var subscriptionKeys = ["disabled", "homepage", "lastSuccess", "title", "url", "downloadStatus"];
- function convertSubscription(subscription)
- {
- var result = {};
- for (var i = 0; i < subscriptionKeys.length; i++)
- result[subscriptionKeys[i]] = subscription[subscriptionKeys[i]]
- return result;
- }
+ let ext = global.ext || require("ext_background");
+
+ const {port} = require("messaging");
+ const {Prefs} = require("prefs");
+ const {Utils} = require("utils");
+ const {FilterStorage} = require("filterStorage");
+ const {FilterNotifier} = require("filterNotifier");
+ const {defaultMatcher} = require("matcher");
+ const {ElemHideEmulation} = require("elemHideEmulation");
+ const {Notification: NotificationStorage} = require("notification");
+
+ const {Filter, BlockingFilter, RegExpFilter} = require("filterClasses");
+ const {Synchronizer} = require("synchronizer");
- var changeListeners = null;
- var messageTypes = {
- "app": "app.listen",
- "filter": "filters.listen",
- "subscription": "subscriptions.listen"
- };
+ const info = require("info");
+ const {Subscription,
+ DownloadableSubscription,
+ SpecialSubscription} = require("subscriptionClasses");
- function onFilterChange(action)
+ // Some modules doesn't exist on Firefox. Moreover,
+ // require() throws an exception on Firefox in that case.
+ // However, try/catch causes the whole function to to be
+ // deoptimized on V8. So we wrap it into another function.
+ function tryRequire(module)
{
- var parts = action.split(".", 2);
- var type;
- if (parts.length == 1)
+ try
{
- type = "app";
- action = parts[0];
+ return require(module);
}
- else
+ catch (e)
+ {
+ return null;
+ }
+ }
+
+ function convertObject(keys, obj)
+ {
+ let result = {};
+ for (let key of keys)
{
- type = parts[0];
- action = parts[1];
+ if (key in obj)
+ result[key] = obj[key];
}
+ return result;
+ }
+
+ function convertSubscription(subscription)
+ {
+ let obj = convertObject(["disabled", "downloadStatus", "homepage",
+ "lastDownload", "title", "url"], subscription);
+ obj.isDownloading = Synchronizer.isExecuting(subscription.url);
+ return obj;
+ }
- if (!messageTypes.hasOwnProperty(type))
+ let convertFilter = convertObject.bind(null, ["text"]);
+
+ let changeListeners = new ext.PageMap();
+ let listenedPreferences = Object.create(null);
+ let listenedFilterChanges = Object.create(null);
+ let messageTypes = new Map([
+ ["app", "app.respond"],
+ ["filter", "filters.respond"],
+ ["pref", "prefs.respond"],
+ ["subscription", "subscriptions.respond"]
+ ]);
+
+ function sendMessage(type, action, ...args)
+ {
+ let pages = changeListeners.keys();
+ if (pages.length == 0)
return;
- var args = Array.prototype.slice.call(arguments, 1).map(function(arg)
+ let convertedArgs = [];
+ for (let arg of args)
{
if (arg instanceof Subscription)
- return convertSubscription(arg);
+ convertedArgs.push(convertSubscription(arg));
+ else if (arg instanceof Filter)
+ convertedArgs.push(convertFilter(arg));
else
- return arg;
- });
+ convertedArgs.push(arg);
+ }
- var pages = changeListeners.keys();
- for (var i = 0; i < pages.length; i++)
+ for (let page of pages)
{
- var filters = changeListeners.get(pages[i]);
- if (filters[type] && filters[type].indexOf(action) >= 0)
+ let filters = changeListeners.get(page);
+ let actions = filters[type];
+ if (actions && actions.indexOf(action) != -1)
{
- pages[i].sendMessage({
- type: messageTypes[type],
- action: action,
- args: args
+ page.sendMessage({
+ type: messageTypes.get(type),
+ action,
+ args: convertedArgs
});
}
}
- };
+ }
- global.ext.onMessage.addListener(function(message, sender, callback)
+ function addFilterListeners(type, actions)
{
- switch (message.type)
+ for (let action of actions)
{
- case "app.get":
- if (message.what == "issues")
- {
- var info = require("info");
- callback({
- seenDataCorruption: "seenDataCorruption" in global ? global.seenDataCorruption : false,
- filterlistsReinitialized: "filterlistsReinitialized" in global ? global.filterlistsReinitialized : false,
- legacySafariVersion: (info.platform == "safari" && (
- Services.vc.compare(info.platformVersion, "6.0") < 0 || // beforeload breaks websites in Safari 5
- Services.vc.compare(info.platformVersion, "6.1") == 0 || // extensions are broken in 6.1 and 7.0
- Services.vc.compare(info.platformVersion, "7.0") == 0))
- });
- }
- else if (message.what == "doclink")
- callback(Utils.getDocLink(message.link));
- else if (message.what == "localeInfo")
- {
- var bidiDir;
- if ("chromeRegistry" in Utils)
- bidiDir = Utils.chromeRegistry.isLocaleRTL("adblockplus") ? "rtl" : "ltr";
- else
- bidiDir = ext.i18n.getMessage("@@bidi_dir");
-
- callback({locale: Utils.appLocale, bidiDir: bidiDir});
- }
- else
- callback(null);
- break;
- case "app.open":
- if (message.what == "options")
- ext.showOptions();
- break;
- case "subscriptions.get":
- var subscriptions = FilterStorage.subscriptions.filter(function(s)
+ let name;
+ if (type == "filter" && action == "loaded")
+ name = "load";
+ else
+ name = type + "." + action;
+
+ if (!(name in listenedFilterChanges))
+ {
+ listenedFilterChanges[name] = null;
+ FilterNotifier.on(name, (...args) =>
{
- if (message.ignoreDisabled && s.disabled)
- return false;
- if (s instanceof DownloadableSubscription && message.downloadable)
- return true;
- if (s instanceof SpecialSubscription && message.special)
- return true;
- return false;
+ sendMessage(type, action, ...args);
});
- callback(subscriptions.map(convertSubscription));
- break;
- case "filters.blocked":
- var filter = defaultMatcher.matchesAny(message.url, message.requestType, message.docDomain, message.thirdParty);
- callback(filter instanceof BlockingFilter);
- break;
- case "subscriptions.toggle":
- var subscription = Subscription.fromURL(message.url);
- if (subscription.url in FilterStorage.knownSubscriptions && !subscription.disabled)
- FilterStorage.removeSubscription(subscription);
- else
- {
- subscription.disabled = false;
- subscription.title = message.title;
- subscription.homepage = message.homepage;
- FilterStorage.addSubscription(subscription);
- if (!subscription.lastDownload)
- Synchronizer.execute(subscription);
- }
- break;
- case "subscriptions.listen":
- if (!changeListeners)
+ }
+ }
+ }
+
+ function getListenerFilters(page)
+ {
+ let listenerFilters = changeListeners.get(page);
+ if (!listenerFilters)
+ {
+ listenerFilters = Object.create(null);
+ changeListeners.set(page, listenerFilters);
+ }
+ return listenerFilters;
+ }
+
+ port.on("app.get", (message, sender) =>
+ {
+ if (message.what == "issues")
+ {
+ let subscriptionInit = tryRequire("subscriptionInit");
+ let result = subscriptionInit ? subscriptionInit.reinitialized : false;
+ return {filterlistsReinitialized: result};
+ }
+
+ if (message.what == "doclink")
+ return Utils.getDocLink(message.link);
+
+ if (message.what == "localeInfo")
+ {
+ let bidiDir;
+ if ("chromeRegistry" in Utils)
+ {
+ let isRtl = Utils.chromeRegistry.isLocaleRTL("adblockplus");
+ bidiDir = isRtl ? "rtl" : "ltr";
+ }
+ else
+ bidiDir = ext.i18n.getMessage("@@bidi_dir");
+
+ return {locale: Utils.appLocale, bidiDir};
+ }
+
+ if (message.what == "features")
+ {
+ return {
+ devToolsPanel: info.platform == "chromium"
+ };
+ }
+
+ return info[message.what];
+ });
+
+ port.on("app.listen", (message, sender) =>
+ {
+ getListenerFilters(sender.page).app = message.filter;
+ });
+
+ port.on("app.open", (message, sender) =>
+ {
+ if (message.what == "options")
+ ext.showOptions();
+ });
+
+ port.on("filters.add", (message, sender) =>
+ {
+ let result = require("filterValidation").parseFilter(message.text);
+ let errors = [];
+ if (result.error)
+ errors.push(result.error.toString());
+ else if (result.filter)
+ FilterStorage.addFilter(result.filter);
+
+ return errors;
+ });
+
+ port.on("filters.blocked", (message, sender) =>
+ {
+ let filter = defaultMatcher.matchesAny(message.url,
+ RegExpFilter.typeMap[message.requestType], message.docDomain,
+ message.thirdParty);
+
+ return filter instanceof BlockingFilter;
+ });
+
+ port.on("filters.get", (message, sender) =>
+ {
+ if (message.what == "elemhideemulation")
+ {
+ let filters = [];
+ const {checkWhitelisted} = require("whitelisting");
+
+ if (Prefs.enabled && !checkWhitelisted(sender.page, sender.frame,
+ RegExpFilter.typeMap.DOCUMENT |
+ RegExpFilter.typeMap.ELEMHIDE))
+ {
+ let {hostname} = sender.frame.url;
+ filters = ElemHideEmulation.getRulesForDomain(hostname);
+ filters = filters.map((filter) =>
{
- changeListeners = new global.ext.PageMap();
- FilterNotifier.addListener(onFilterChange);
- }
+ return {
+ selector: filter.selector,
+ text: filter.text
+ };
+ });
+ }
+ return filters;
+ }
- var filters = changeListeners.get(sender.page);
- if (!filters)
+ let subscription = Subscription.fromURL(message.subscriptionUrl);
+ if (!subscription)
+ return [];
+
+ return subscription.filters.map(convertFilter);
+ });
+
+ port.on("filters.importRaw", (message, sender) =>
+ {
+ let result = require("filterValidation").parseFilters(message.text);
+ let errors = [];
+ for (let error of result.errors)
+ {
+ if (error.type != "unexpected-filter-list-header")
+ errors.push(error.toString());
+ }
+
+ if (errors.length > 0)
+ return errors;
+
+ let seenFilter = Object.create(null);
+ for (let filter of result.filters)
+ {
+ FilterStorage.addFilter(filter);
+ seenFilter[filter.text] = null;
+ }
+
+ if (!message.removeExisting)
+ return errors;
+
+ for (let subscription of FilterStorage.subscriptions)
+ {
+ if (!(subscription instanceof SpecialSubscription))
+ continue;
+
+ for (let j = subscription.filters.length - 1; j >= 0; j--)
+ {
+ let filter = subscription.filters[j];
+ if (/^@@\|\|([^/:]+)\^\$document$/.test(filter.text))
+ continue;
+
+ if (!(filter.text in seenFilter))
+ FilterStorage.removeFilter(filter);
+ }
+ }
+
+ return errors;
+ });
+
+ port.on("filters.listen", (message, sender) =>
+ {
+ getListenerFilters(sender.page).filter = message.filter;
+ addFilterListeners("filter", message.filter);
+ });
+
+ port.on("filters.remove", (message, sender) =>
+ {
+ let filter = Filter.fromText(message.text);
+ let subscription = null;
+ if (message.subscriptionUrl)
+ subscription = Subscription.fromURL(message.subscriptionUrl);
+
+ if (!subscription)
+ FilterStorage.removeFilter(filter);
+ else
+ FilterStorage.removeFilter(filter, subscription, message.index);
+ });
+
+ port.on("prefs.get", (message, sender) =>
+ {
+ return Prefs[message.key];
+ });
+
+ port.on("prefs.listen", (message, sender) =>
+ {
+ getListenerFilters(sender.page).pref = message.filter;
+ for (let preference of message.filter)
+ {
+ if (!(preference in listenedPreferences))
+ {
+ listenedPreferences[preference] = null;
+ Prefs.on(preference, () =>
{
- filters = Object.create(null);
- changeListeners.set(sender.page, filters);
- }
-
- if (message.filter)
- filters.subscription = message.filter;
- else
- delete filters.subscription;
- break;
+ sendMessage("pref", preference, Prefs[preference]);
+ });
+ }
+ }
+ });
+
+ port.on("prefs.toggle", (message, sender) =>
+ {
+ if (message.key == "notifications_ignoredcategories")
+ NotificationStorage.toggleIgnoreCategory("*");
+ else
+ Prefs[message.key] = !Prefs[message.key];
+ });
+
+ port.on("subscriptions.add", (message, sender) =>
+ {
+ let subscription = Subscription.fromURL(message.url);
+ if ("title" in message)
+ subscription.title = message.title;
+ if ("homepage" in message)
+ subscription.homepage = message.homepage;
+
+ if (message.confirm)
+ {
+ ext.showOptions(() =>
+ {
+ sendMessage("app", "addSubscription", subscription);
+ });
+ }
+ else
+ {
+ subscription.disabled = false;
+ FilterStorage.addSubscription(subscription);
+
+ if (subscription instanceof DownloadableSubscription &&
+ !subscription.lastDownload)
+ Synchronizer.execute(subscription);
+ }
+ });
+
+ port.on("subscriptions.get", (message, sender) =>
+ {
+ let subscriptions = FilterStorage.subscriptions.filter((s) =>
+ {
+ if (message.ignoreDisabled && s.disabled)
+ return false;
+ if (s instanceof DownloadableSubscription && message.downloadable)
+ return true;
+ if (s instanceof SpecialSubscription && message.special)
+ return true;
+ return false;
+ });
+
+ return subscriptions.map(convertSubscription);
+ });
+
+ port.on("subscriptions.listen", (message, sender) =>
+ {
+ getListenerFilters(sender.page).subscription = message.filter;
+ addFilterListeners("subscription", message.filter);
+ });
+
+ port.on("subscriptions.remove", (message, sender) =>
+ {
+ let subscription = Subscription.fromURL(message.url);
+ if (subscription.url in FilterStorage.knownSubscriptions)
+ FilterStorage.removeSubscription(subscription);
+ });
+
+ port.on("subscriptions.toggle", (message, sender) =>
+ {
+ let subscription = Subscription.fromURL(message.url);
+ if (subscription.url in FilterStorage.knownSubscriptions)
+ {
+ if (subscription.disabled || message.keepInstalled)
+ subscription.disabled = !subscription.disabled;
+ else
+ FilterStorage.removeSubscription(subscription);
+ }
+ else
+ {
+ subscription.disabled = false;
+ subscription.title = message.title;
+ subscription.homepage = message.homepage;
+ FilterStorage.addSubscription(subscription);
+ if (!subscription.lastDownload)
+ Synchronizer.execute(subscription);
+ }
+ });
+
+ port.on("subscriptions.update", (message, sender) =>
+ {
+ let {subscriptions} = FilterStorage;
+ if (message.url)
+ subscriptions = [Subscription.fromURL(message.url)];
+
+ for (let subscription of subscriptions)
+ {
+ if (subscription instanceof DownloadableSubscription)
+ Synchronizer.execute(subscription, true);
}
});
})(this);
diff --git a/data/extensions/spyblock@gnu.org/lib/messaging.js b/data/extensions/spyblock@gnu.org/lib/messaging.js
new file mode 100644
index 0000000..63d061e
--- /dev/null
+++ b/data/extensions/spyblock@gnu.org/lib/messaging.js
@@ -0,0 +1,316 @@
+/*
+ * This file is part of Adblock Plus <https://adblockplus.org/>,
+ * Copyright (C) 2006-2017 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/>.
+ */
+
+"use strict";
+
+let {EventEmitter} = require("events");
+
+const MESSAGE_NAME = "AdblockPlus:Message";
+const RESPONSE_NAME = "AdblockPlus:Response";
+
+function isPromise(value)
+{
+ // value instanceof Promise won't work - there can be different Promise
+ // classes (e.g. in different contexts) and there can also be promise-like
+ // classes (e.g. Task).
+ return (value && typeof value.then == "function");
+}
+
+function sendMessage(messageManager, messageName, payload, callbackID)
+{
+ let request = {messageName, payload, callbackID};
+ if (messageManager instanceof Ci.nsIMessageSender)
+ {
+ messageManager.sendAsyncMessage(MESSAGE_NAME, request);
+ return 1;
+ }
+ else if (messageManager instanceof Ci.nsIMessageBroadcaster)
+ {
+ messageManager.broadcastAsyncMessage(MESSAGE_NAME, request);
+ return messageManager.childCount;
+ }
+ else
+ {
+ Cu.reportError("Unexpected message manager, impossible to send message");
+ return 0;
+ }
+}
+
+function sendSyncMessage(messageManager, messageName, payload)
+{
+ let request = {messageName, payload};
+ let responses = messageManager.sendRpcMessage(MESSAGE_NAME, request);
+ let processor = new ResponseProcessor(messageName);
+ for (let response of responses)
+ processor.add(response);
+ return processor.value;
+}
+
+function ResponseProcessor(messageName)
+{
+ this.value = undefined;
+ this.add = function(response)
+ {
+ if (typeof response == "undefined")
+ return;
+
+ if (typeof this.value == "undefined")
+ this.value = response;
+ else
+ Cu.reportError("Got multiple responses to message '" + messageName + "', only first response was accepted.");
+ };
+}
+
+function getSender(origin)
+{
+ if (origin instanceof Ci.nsIDOMXULElement)
+ origin = origin.messageManager;
+
+ if (origin instanceof Ci.nsIMessageSender)
+ return new LightWeightPort(origin);
+ else
+ return null;
+}
+
+/**
+ * Lightweight communication port allowing only sending messages.
+ * @param {nsIMessageManager} messageManager
+ * @constructor
+ */
+function LightWeightPort(messageManager)
+{
+ this._messageManager = messageManager;
+}
+LightWeightPort.prototype =
+{
+ /**
+ * @see Port#emit
+ */
+ emit: function(messageName, payload)
+ {
+ sendMessage(this._messageManager, messageName, payload);
+ },
+
+ /**
+ * @see Port#emitSync
+ */
+ emitSync: function(messageName, payload)
+ {
+ return sendSyncMessage(this._messageManager, messageName, payload);
+ }
+};
+
+/**
+ * Communication port wrapping the message manager API to send and receive
+ * messages.
+ * @param {nsIMessageManager} messageManager
+ * @constructor
+ */
+function Port(messageManager)
+{
+ this._messageManager = messageManager;
+ this._eventEmitter = new EventEmitter();
+
+ this._responseCallbacks = new Map();
+ this._responseCallbackCounter = 0;
+
+ this._handleRequest = this._handleRequest.bind(this);
+ this._handleResponse = this._handleResponse.bind(this);
+ this._messageManager.addMessageListener(MESSAGE_NAME, this._handleRequest);
+ this._messageManager.addMessageListener(RESPONSE_NAME, this._handleResponse);
+}
+Port.prototype = {
+ /**
+ * Disables the port and makes it stop listening to incoming messages.
+ */
+ disconnect: function()
+ {
+ this._messageManager.removeMessageListener(MESSAGE_NAME, this._handleRequest);
+ this._messageManager.removeMessageListener(RESPONSE_NAME, this._handleResponse);
+ },
+
+ _sendResponse: function(sender, callbackID, payload)
+ {
+ if (!sender || typeof callbackID == "undefined")
+ return;
+
+ let response = {callbackID, payload};
+ sender._messageManager.sendAsyncMessage(RESPONSE_NAME, response);
+ },
+
+ _handleRequest: function(message)
+ {
+ let sender = getSender(message.target);
+ let {callbackID, messageName, payload} = message.data;
+
+ let result = this._dispatch(messageName, payload, sender);
+ if (isPromise(result))
+ {
+ // This is a promise - asynchronous response
+ if (message.sync)
+ {
+ Cu.reportError("Asynchronous response to the synchronous message '" + messageName + "' is not possible");
+ return undefined;
+ }
+
+ result.then(result =>
+ {
+ this._sendResponse(sender, callbackID, result)
+ }, e =>
+ {
+ Cu.reportError(e);
+ this._sendResponse(sender, callbackID, undefined);
+ });
+ }
+ else
+ this._sendResponse(sender, callbackID, result);
+
+ return result;
+ },
+
+ _handleResponse: function(message)
+ {
+ let {callbackID, payload} = message.data;
+ let callbackData = this._responseCallbacks.get(callbackID);
+ if (!callbackData)
+ return;
+
+ let [callback, processor, expectedResponses] = callbackData;
+
+ try
+ {
+ processor.add(payload);
+ }
+ catch (e)
+ {
+ Cu.reportError(e);
+ }
+
+ callbackData[2] = --expectedResponses;
+ if (expectedResponses <= 0)
+ {
+ this._responseCallbacks.delete(callbackID);
+ callback(processor.value);
+ }
+ },
+
+ _dispatch: function(messageName, payload, sender)
+ {
+ let callbacks = this._eventEmitter.listeners(messageName);
+ let processor = new ResponseProcessor(messageName);
+ for (let callback of callbacks)
+ {
+ try
+ {
+ processor.add(callback(payload, sender));
+ }
+ catch (e)
+ {
+ Cu.reportError(e);
+ }
+ }
+ return processor.value;
+ },
+
+ /**
+ * Function to be called when a particular message is received
+ * @callback Port~messageHandler
+ * @param payload data attached to the message if any
+ * @param {LightWeightPort} sender object that can be used to communicate with
+ * the sender of the message, could be null
+ * @return the handler can return undefined (no response), a value (response
+ * to be sent to sender immediately) or a promise (asynchronous
+ * response).
+ */
+
+ /**
+ * Adds a handler for the specified message.
+ * @param {string} messageName message that would trigger the callback
+ * @param {Port~messageHandler} callback
+ */
+ on: function(messageName, callback)
+ {
+ this._eventEmitter.on(messageName, callback);
+ },
+
+ /**
+ * Removes a handler for the specified message.
+ * @param {string} messageName message that would trigger the callback
+ * @param {Port~messageHandler} callback
+ */
+ off: function(messageName, callback)
+ {
+ this._eventEmitter.off(messageName, callback);
+ },
+
+ /**
+ * Sends a message.
+ * @param {string} messageName message identifier
+ * @param [payload] data to attach to the message
+ */
+ emit: function(messageName, payload)
+ {
+ sendMessage(this._messageManager, messageName, payload, undefined);
+ },
+
+ /**
+ * Sends a message and expects a response.
+ * @param {string} messageName message identifier
+ * @param [payload] data to attach to the message
+ * @return {Promise} promise that will be resolved with the response
+ */
+ emitWithResponse: function(messageName, payload)
+ {
+ let callbackID = ++this._responseCallbackCounter;
+ let expectedResponses = sendMessage(
+ this._messageManager, messageName, payload, callbackID);
+ return new Promise((resolve, reject) =>
+ {
+ this._responseCallbacks.set(callbackID,
+ [resolve, new ResponseProcessor(messageName), expectedResponses]);
+ });
+ },
+
+ /**
+ * Sends a synchonous message (DO NOT USE unless absolutely unavoidable).
+ * @param {string} messageName message identifier
+ * @param [payload] data to attach to the message
+ * @return response returned by the handler
+ */
+ emitSync: function(messageName, payload)
+ {
+ return sendSyncMessage(this._messageManager, messageName, payload);
+ }
+};
+exports.Port = Port;
+
+let messageManager;
+try
+{
+ // Child
+ messageManager = require("messageManager");
+}
+catch (e)
+{
+ // Parent
+ messageManager = Cc["@mozilla.org/parentprocessmessagemanager;1"]
+ .getService(Ci.nsIMessageListenerManager);
+}
+
+let port = new Port(messageManager);
+onShutdown.add(() => port.disconnect());
+exports.port = port;
diff --git a/data/extensions/spyblock@gnu.org/lib/notification.js b/data/extensions/spyblock@gnu.org/lib/notification.js
index 1fa8eed..311e4e8 100644
--- a/data/extensions/spyblock@gnu.org/lib/notification.js
+++ b/data/extensions/spyblock@gnu.org/lib/notification.js
@@ -1,6 +1,6 @@
/*
* This file is part of Adblock Plus <https://adblockplus.org/>,
- * Copyright (C) 2006-2015 Eyeo GmbH
+ * Copyright (C) 2006-2017 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
@@ -15,32 +15,39 @@
* along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>.
*/
+"use strict";
+
/**
* @fileOverview Handles notifications.
*/
-Cu.import("resource://gre/modules/Services.jsm");
+const {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
-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");
+const {Prefs} = require("prefs");
+const {Downloader, Downloadable,
+ MILLIS_IN_MINUTE, MILLIS_IN_HOUR, MILLIS_IN_DAY} = require("downloader");
+const {Utils} = require("utils");
+const {Matcher, defaultMatcher} = require("matcher");
+const {Filter, RegExpFilter, WhitelistFilter} = 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 = {
+const INITIAL_DELAY = 1 * MILLIS_IN_MINUTE;
+const CHECK_INTERVAL = 1 * MILLIS_IN_HOUR;
+const EXPIRATION_INTERVAL = 1 * MILLIS_IN_DAY;
+const TYPE = {
information: 0,
question: 1,
- critical: 2
+ relentless: 2,
+ critical: 3
};
-let listeners = {};
+let showListeners = [];
+let questionListeners = {};
function getNumericalSeverity(notification)
{
- return (notification.type in TYPE ? TYPE[notification.type] : TYPE.information);
+ if (notification.type in TYPE)
+ return TYPE[notification.type];
+ return TYPE.information;
}
function saveNotificationData()
@@ -64,7 +71,7 @@ function localize(translations, locale)
/**
* The object providing actual downloading functionality.
- * @type Downloader
+ * @type {Downloader}
*/
let downloader = null;
let localData = [];
@@ -78,31 +85,31 @@ let Notification = exports.Notification =
/**
* Called on module startup.
*/
- init: function()
+ init()
{
- downloader = new Downloader(this._getDownloadables.bind(this), INITIAL_DELAY, CHECK_INTERVAL);
- onShutdown.add(function()
- {
- downloader.cancel();
- });
-
+ downloader = new Downloader(this._getDownloadables.bind(this),
+ INITIAL_DELAY, CHECK_INTERVAL);
downloader.onExpirationChange = this._onExpirationChange.bind(this);
downloader.onDownloadSuccess = this._onDownloadSuccess.bind(this);
downloader.onDownloadError = this._onDownloadError.bind(this);
+ onShutdown.add(() => downloader.cancel());
},
/**
* Yields a Downloadable instances for the notifications download.
*/
- _getDownloadables: function()
+ *_getDownloadables()
{
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)
+ 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")
@@ -112,7 +119,7 @@ let Notification = exports.Notification =
yield downloadable;
},
- _onExpirationChange: function(downloadable)
+ _onExpirationChange(downloadable)
{
Prefs.notificationdata.lastCheck = downloadable.lastCheck;
Prefs.notificationdata.softExpiration = downloadable.softExpiration;
@@ -120,7 +127,8 @@ let Notification = exports.Notification =
saveNotificationData();
},
- _onDownloadSuccess: function(downloadable, responseText, errorCallback, redirectCallback)
+ _onDownloadSuccess(downloadable, responseText, errorCallback,
+ redirectCallback)
{
try
{
@@ -145,12 +153,18 @@ let Notification = exports.Notification =
Prefs.notificationdata.lastError = 0;
Prefs.notificationdata.downloadStatus = "synchronize_ok";
- [Prefs.notificationdata.softExpiration, Prefs.notificationdata.hardExpiration] = downloader.processExpirationInterval(EXPIRATION_INTERVAL);
+ [
+ Prefs.notificationdata.softExpiration,
+ Prefs.notificationdata.hardExpiration
+ ] = downloader.processExpirationInterval(EXPIRATION_INTERVAL);
Prefs.notificationdata.downloadCount = downloadable.downloadCount;
saveNotificationData();
+
+ Notification.showNext();
},
- _onDownloadError: function(downloadable, downloadURL, error, channelStatus, responseStatus, redirectCallback)
+ _onDownloadError(downloadable, downloadURL, error, channelStatus,
+ responseStatus, redirectCallback)
{
Prefs.notificationdata.lastError = Date.now();
Prefs.notificationdata.downloadStatus = error;
@@ -158,52 +172,115 @@ let Notification = exports.Notification =
},
/**
+ * Adds a listener for notifications to be shown.
+ * @param {Function} listener Listener to be invoked when a notification is
+ * to be shown
+ */
+ addShowListener(listener)
+ {
+ if (showListeners.indexOf(listener) == -1)
+ showListeners.push(listener);
+ },
+
+ /**
+ * Removes the supplied listener.
+ * @param {Function} listener Listener that was added via addShowListener()
+ */
+ removeShowListener(listener)
+ {
+ let index = showListeners.indexOf(listener);
+ if (index != -1)
+ showListeners.splice(index, 1);
+ },
+
+ /**
* Determines which notification is to be shown next.
- * @param {String} url URL to match notifications to (optional)
+ * @param {string} url URL to match notifications to (optional)
* @return {Object} notification to be shown, or null if there is none
*/
- getNextToShow: function(url)
+ _getNextToShow(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));
+ (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))
+ if (typeof Prefs.notificationdata.data == "object" &&
+ Prefs.notificationdata.data.notifications instanceof Array)
{
- Prefs.notificationdata.shown = [];
- saveNotificationData();
+ remoteData = Prefs.notificationdata.data.notifications;
}
let notifications = localData.concat(remoteData);
if (notifications.length === 0)
return null;
- let {addonName, addonVersion, application, applicationVersion, platform, platformVersion} = require("info");
+ const {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 notification.type === "undefined" ||
+ notification.type !== "critical")
+ {
+ let shown;
+ if (typeof Prefs.notificationdata.shown == "object")
+ shown = Prefs.notificationdata.shown[notification.id];
+
+ if (typeof shown != "undefined")
+ {
+ if (typeof notification.interval == "number")
+ {
+ if (shown + notification.interval > Date.now())
+ continue;
+ }
+ else if (shown)
+ continue;
+ }
+
+ if (notification.type !== "relentless" &&
+ Prefs.notifications_ignoredcategories.indexOf("*") != -1)
+ {
+ continue;
+ }
+ }
if (typeof url === "string" || notification.urlFilters instanceof Array)
{
- if (typeof url === "string" && notification.urlFilters instanceof Array)
+ if (Prefs.enabled && typeof url === "string" &&
+ notification.urlFilters instanceof Array)
{
+ let host;
+ try
+ {
+ host = new URL(url).hostname;
+ }
+ catch (e)
+ {
+ host = "";
+ }
+
+ let exception = defaultMatcher.matchesAny(
+ url, RegExpFilter.typeMap.DOCUMENT, host, false, null
+ );
+ if (exception instanceof WhitelistFilter)
+ continue;
+
let matcher = new Matcher();
for (let urlFilter of notification.urlFilters)
matcher.add(Filter.fromText(urlFilter));
- if (!matcher.matchesAny(url, "DOCUMENT", url))
+ if (!matcher.matchesAny(url, RegExpFilter.typeMap.DOCUMENT, host,
+ false, null))
+ {
continue;
+ }
}
else
continue;
@@ -215,7 +292,8 @@ let Notification = exports.Notification =
for (let target of notification.targets)
{
if (checkTarget(target, "extension", addonName, addonVersion) &&
- checkTarget(target, "application", application, applicationVersion) &&
+ checkTarget(target, "application", application,
+ applicationVersion) &&
checkTarget(target, "platform", platform, platformVersion))
{
match = true;
@@ -226,37 +304,63 @@ let Notification = exports.Notification =
continue;
}
- if (!notificationToShow
- || getNumericalSeverity(notification) > getNumericalSeverity(notificationToShow))
+ if (!notificationToShow ||
+ getNumericalSeverity(notification) >
+ getNumericalSeverity(notificationToShow))
notificationToShow = notification;
}
- if (notificationToShow && "id" in notificationToShow)
+ return notificationToShow;
+ },
+
+ /**
+ * Invokes the listeners added via addShowListener() with the next
+ * notification to be shown.
+ * @param {string} url URL to match notifications to (optional)
+ */
+ showNext(url)
+ {
+ let notification = Notification._getNextToShow(url);
+ if (notification)
{
- if (notificationToShow.type !== "question")
- this.markAsShown(notificationToShow.id);
+ for (let showListener of showListeners)
+ showListener(notification);
}
-
- return notificationToShow;
},
- markAsShown: function(id)
+ /**
+ * Marks a notification as shown.
+ * @param {string} id ID of the notification to be marked as shown
+ */
+ markAsShown(id)
{
- if (Prefs.notificationdata.shown.indexOf(id) > -1)
- return;
+ let now = Date.now();
+ let data = Prefs.notificationdata;
+
+ if (data.shown instanceof Array)
+ {
+ let newShown = {};
+ for (let oldId of data.shown)
+ newShown[oldId] = now;
+ data.shown = newShown;
+ }
+
+ if (typeof data.shown != "object")
+ data.shown = {};
+
+ data.shown[id] = now;
- 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
+ * @param {string} locale the target locale (optional, defaults to the
* application locale)
* @return {Object} the translated texts
*/
- getLocalizedTexts: function(notification, locale)
+ getLocalizedTexts(notification, locale)
{
locale = locale || Utils.appLocale;
let textKeys = ["title", "message"];
@@ -278,7 +382,7 @@ let Notification = exports.Notification =
* Adds a local notification.
* @param {Object} notification notification to add
*/
- addNotification: function(notification)
+ addNotification(notification)
{
if (localData.indexOf(notification) == -1)
localData.push(notification);
@@ -288,7 +392,7 @@ let Notification = exports.Notification =
* Removes an existing local notification.
* @param {Object} notification notification to remove
*/
- removeNotification: function(notification)
+ removeNotification(notification)
{
let index = localData.indexOf(notification);
if (index > -1)
@@ -296,42 +400,76 @@ let Notification = exports.Notification =
},
/**
+ * A callback function which listens to see if notifications were approved.
+ *
+ * @callback QuestionListener
+ * @param {boolean} approved
+ */
+
+ /**
* Adds a listener for question-type notifications
+ * @param {string} id
+ * @param {QuestionListener} listener
*/
- addQuestionListener: function(/**string*/ id, /**function(approved)*/ listener)
+ addQuestionListener(id, listener)
{
- if (!(id in listeners))
- listeners[id] = [];
- if (listeners[id].indexOf(listener) === -1)
- listeners[id].push(listener);
+ if (!(id in questionListeners))
+ questionListeners[id] = [];
+ if (questionListeners[id].indexOf(listener) === -1)
+ questionListeners[id].push(listener);
},
/**
* Removes a listener that was previously added via addQuestionListener
+ * @param {string} id
+ * @param {QuestionListener} listener
*/
- removeQuestionListener: function(/**string*/ id, /**function(approved)*/ listener)
+ removeQuestionListener(id, listener)
{
- if (!(id in listeners))
+ if (!(id in questionListeners))
return;
- let index = listeners[id].indexOf(listener);
+ let index = questionListeners[id].indexOf(listener);
if (index > -1)
- listeners[id].splice(index, 1);
- if (listeners[id].length === 0)
- delete listeners[id];
+ questionListeners[id].splice(index, 1);
+ if (questionListeners[id].length === 0)
+ delete questionListeners[id];
},
/**
- * Notifies listeners about interactions with a notification
- * @param {String} id notification ID
- * @param {Boolean} approved indicator whether notification has been approved or not
+ * Notifies question listeners about interactions with a notification
+ * @param {string} id notification ID
+ * @param {boolean} approved indicator whether notification has been approved
*/
- triggerQuestionListeners: function(id, approved)
+ triggerQuestionListeners(id, approved)
{
- if (!(id in listeners))
+ if (!(id in questionListeners))
return;
- let questionListeners = listeners[id];
- for (let listener of questionListeners)
+ let listeners = questionListeners[id];
+ for (let listener of listeners)
listener(approved);
+ },
+
+ /**
+ * Toggles whether notifications of a specific category should be ignored
+ * @param {string} category notification category identifier
+ * @param {boolean} [forceValue] force specified value
+ */
+ toggleIgnoreCategory(category, forceValue)
+ {
+ let categories = Prefs.notifications_ignoredcategories;
+ let index = categories.indexOf(category);
+ if (index == -1 && forceValue !== false)
+ {
+ categories.push(category);
+ Prefs.notifications_showui = true;
+ }
+ else if (index != -1 && forceValue !== true)
+ categories.splice(index, 1);
+
+ // HACK: JSON values aren't saved unless they are assigned a
+ // different object.
+ Prefs.notifications_ignoredcategories =
+ JSON.parse(JSON.stringify(categories));
}
};
Notification.init();
diff --git a/data/extensions/spyblock@gnu.org/lib/objectTabs.js b/data/extensions/spyblock@gnu.org/lib/objectTabs.js
index bcf4362..3ee92bc 100644
--- a/data/extensions/spyblock@gnu.org/lib/objectTabs.js
+++ b/data/extensions/spyblock@gnu.org/lib/objectTabs.js
@@ -1,6 +1,6 @@
/*
* This file is part of Adblock Plus <https://adblockplus.org/>,
- * Copyright (C) 2006-2015 Eyeo GmbH
+ * Copyright (C) 2006-2017 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
@@ -19,479 +19,95 @@
* @fileOverview Code responsible for showing and hiding object tabs.
*/
+let {Prefs} = require("prefs");
+let {Utils} = require("utils");
+let {port} = require("messaging");
+
/**
- * Class responsible for showing and hiding object tabs.
- * @class
+ * Random element class, to be used for object tabs displayed on top of the
+ * plugin content.
+ * @type string
*/
-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" || application == "icecatmobile")
- 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.
- * @param {Element} element
- * @param {RequestEntry} data
- */
- _showTab: function(element, 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);
- }
+let classVisibleTop = 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();
- },
+/**
+ * Random element class, to be used for object tabs displayed at the bottom of
+ * the plugin content.
+ * @type string
+ */
+let classVisibleBottom = null;
- /**
- * Hides the tab element.
- */
- _hideTab: function()
- {
- this.delayedShowParams = null;
+/**
+ * Random element class, to be used for object tabs that are hidden.
+ * @type string
+ */
+let classHidden = null;
- if (this.objtabElement)
- {
- // Prevent recursive calls via popuphidden handler
- let objtab = this.objtabElement;
- this.objtabElement = null;
- this.currentElement = null;
+port.on("getObjectTabsStatus", function(message, sender)
+{
+ let {UI} = require("ui");
- if (this.hideTimer)
- {
- this.hideTimer.cancel();
- this.hideTimer = null;
- }
+ return !!(Prefs.enabled && Prefs.frameobjects && UI.overlay && classHidden);
+});
- if (this.positionTimer)
- {
- this.positionTimer.cancel();
- this.positionTimer = null;
- }
+port.on("getObjectTabsTexts", function(message, sender)
+{
+ let {UI} = require("ui");
- try {
- objtab.parentNode.removeChild(objtab);
- } catch (e) {}
- objtab.removeEventListener("mouseover", objectTabEventHander, false);
- objtab.removeEventListener("mouseout", objectTabEventHander, false);
- objtab.nodeData = null;
+ return {
+ label: UI.overlay.attributes.objtabtext,
+ tooltip: UI.overlay.attributes.objtabtooltip,
+ classVisibleTop, classVisibleBottom, classHidden
+ };
+});
- for (let wnd of this.windowListeners)
- wnd.removeEventListener("MozAfterPaint", objectWindowEventHandler, false);
- this.windowListeners = null;
- }
- },
+port.on("blockItem", function({request, nodesID}, sender)
+{
+ let {UI} = require("ui");
+ UI.blockItem(UI.currentWindow, nodesID, request);
+});
- /**
- * Updates position of the tab element.
- */
- _positionTab: function()
+function init()
+{
+ function processCSSData(event)
{
- // 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();
+ if (onShutdown.done)
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");
+ let data = event.target.responseText;
- if (this.objtabElement.getAttribute("class") != className)
- this.objtabElement.setAttribute("class", className);
+ let rnd = [];
+ let offset = "a".charCodeAt(0);
+ for (let i = 0; i < 60; i++)
+ rnd.push(offset + Math.random() * 26);
- this.prevPositionUpdate = Date.now();
- },
+ classVisibleTop = String.fromCharCode.apply(String, rnd.slice(0, 20));
+ classVisibleBottom = String.fromCharCode.apply(String, rnd.slice(20, 40));
+ classHidden = String.fromCharCode.apply(String, rnd.slice(40, 60));
- /**
- * 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)
+ let url = Utils.makeURI("data:text/css," + encodeURIComponent(data.replace(/%%CLASSVISIBLETOP%%/g, classVisibleTop)
+ .replace(/%%CLASSVISIBLEBOTTOM%%/g, classVisibleBottom)
+ .replace(/%%CLASSHIDDEN%%/g, classHidden)));
+ Utils.styleService.loadAndRegisterSheet(url, Ci.nsIStyleSheetService.USER_SHEET);
+ onShutdown.add(function()
{
- // 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);
- },
+ Utils.styleService.unregisterSheet(url, Ci.nsIStyleSheetService.USER_SHEET);
+ });
+ }
- /**
- * Called whenever a timer fires.
- * @param {nsISupport} subject
- * @param {string} topic
- * @param {string} data
- */
- observe: function(subject, topic, data)
+ // Load CSS asynchronously
+ try
{
- 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");
- }
+ let request = new XMLHttpRequest();
+ request.mozBackgroundRequest = true;
+ request.open("GET", "chrome://adblockplus/content/objtabs.css");
+ request.overrideMimeType("text/plain");
+ request.addEventListener("load", processCSSData, false);
+ request.send(null);
}
-};
-
-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)
+ catch (e)
{
- event.preventDefault();
- event.stopPropagation();
-
- objTabs.doBlock();
+ Cu.reportError(e);
}
- else if (event.type == "mouseover")
- objTabs.showTabFor(objTabs.currentElement);
- else if (event.type == "mouseout")
- objTabs.hideTabFor(objTabs.currentElement);
}
-exports.objectMouseEventHander = objectMouseEventHander;
+init();
diff --git a/data/extensions/spyblock@gnu.org/lib/prefs.js b/data/extensions/spyblock@gnu.org/lib/prefs.js
index ab1cc5c..d1ebb95 100644
--- a/data/extensions/spyblock@gnu.org/lib/prefs.js
+++ b/data/extensions/spyblock@gnu.org/lib/prefs.js
@@ -2,41 +2,45 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
+let {XPCOMUtils} = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {});
let {addonRoot, addonName} = require("info");
let branchName = "extensions." + addonName + ".";
let branch = Services.prefs.getBranch(branchName);
+let preconfiguredBranch =
+ Services.prefs.getBranch(branchName + "preconfigured.");
let ignorePrefChanges = false;
function init()
{
// Load default preferences and set up properties for them
let defaultBranch = Services.prefs.getDefaultBranch(branchName);
- let scope =
+
+ let prefsData = require("prefs.json");
+ let defaults = prefsData.defaults;
+ let preconfigurable = new Set(prefsData.preconfigurable);
+ for (let pref in defaults)
{
- pref: function(pref, value)
+ let value = defaults[pref];
+ let [getter, setter] = typeMap[typeof value];
+ if (preconfigurable.has(pref))
{
- if (pref.substr(0, branchName.length) != branchName)
+ try
{
- Cu.reportError(new Error("Ignoring default preference " + pref + ", wrong branch."));
- return;
+ value = getter(preconfiguredBranch, pref);
}
- pref = pref.substr(branchName.length);
-
- let [getter, setter] = typeMap[typeof value];
- setter(defaultBranch, pref, value);
- defineProperty(pref, false, getter, setter);
+ catch (e) {}
}
- };
- Services.scriptloader.loadSubScript(addonRoot + "defaults/prefs.js", scope);
+ setter(defaultBranch, pref, value);
+ defineProperty(pref, false, getter, setter);
+ }
// Add preference change observer
try
{
branch.QueryInterface(Ci.nsIPrefBranch2).addObserver("", Prefs, true);
- onShutdown.add(function() branch.removeObserver("", Prefs));
+ onShutdown.add(() => branch.removeObserver("", Prefs));
}
catch (e)
{
@@ -50,7 +54,7 @@ function init()
function defineProperty(/**String*/ name, defaultValue, /**Function*/ readFunc, /**Function*/ writeFunc)
{
let value = defaultValue;
- Prefs["_update_" + name] = function()
+ Prefs["_update_" + name] = () =>
{
try
{
@@ -62,29 +66,32 @@ function defineProperty(/**String*/ name, defaultValue, /**Function*/ readFunc,
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
+ Object.defineProperty(Prefs, name, {
+ enumerable: true,
+ get: () => value,
+ set: (newValue) =>
{
- ignorePrefChanges = false;
+ 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;
}
- return value;
});
Prefs["_update_" + name]();
}
@@ -161,6 +168,23 @@ let Prefs = exports.Prefs =
QueryInterface: XPCOMUtils.generateQI([Ci.nsISupportsWeakReference, Ci.nsIObserver])
};
+let getIntPref = (branch, pref) => branch.getIntPref(pref);
+let setIntPref = (branch, pref, newValue) => branch.setIntPref(pref, newValue);
+
+let getBoolPref = (branch, pref) => branch.getBoolPref(pref);
+let setBoolPref = (branch, pref, newValue) => branch.setBoolPref(pref, newValue);
+
+let getCharPref = (branch, pref) => branch.getComplexValue(pref, Ci.nsISupportsString).data;
+let setCharPref = (branch, pref, newValue) =>
+{
+ let str = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString);
+ str.data = newValue;
+ branch.setComplexValue(pref, Ci.nsISupportsString, str);
+};
+
+let getJSONPref = (branch, pref) => JSON.parse(getCharPref(branch, pref));
+let setJSONPref = (branch, pref, newValue) => setCharPref(branch, pref, JSON.stringify(newValue));
+
// Getter/setter functions for difference preference types
let typeMap =
{
@@ -170,21 +194,4 @@ let typeMap =
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
index 8b9ca30..f42eaac 100644
--- a/data/extensions/spyblock@gnu.org/lib/requestNotifier.js
+++ b/data/extensions/spyblock@gnu.org/lib/requestNotifier.js
@@ -1,6 +1,6 @@
/*
* This file is part of Adblock Plus <https://adblockplus.org/>,
- * Copyright (C) 2006-2015 Eyeo GmbH
+ * Copyright (C) 2006-2017 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
@@ -19,86 +19,59 @@
* @fileOverview Stores Adblock Plus data to be attached to a window.
*/
-Cu.import("resource://gre/modules/Services.jsm");
+let {port} = require("messaging");
-let {Utils} = require("utils");
-let {BlockingFilter, WhitelistFilter, ElemHideBase, ElemHideFilter, ElemHideException} = require("filterClasses");
+let requestNotifierMaxId = 0;
-let nodeData = new WeakMap();
-let windowStats = new WeakMap();
-let windowSelection = new WeakMap();
-let requestEntryMaxId = 0;
+/**
+ * Active RequestNotifier instances by their ID
+ * @type Map.<number,RequestNotifier>
+ */
+let notifiers = new Map();
-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
+port.on("foundNodeData", ({notifierID, data}, sender) =>
{
- // 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;
-}
+ let notifier = notifiers.get(notifierID);
+ if (notifier)
+ notifier.notifyListener(data);
+});
-/**
- * List of notifiers in use - these notifiers need to receive notifications on
- * new requests.
- * @type RequestNotifier[]
- */
-let activeNotifiers = [];
+port.on("scanComplete", (notifierID, sender) =>
+{
+ let notifier = notifiers.get(notifierID);
+ if (notifier)
+ notifier.onComplete();
+});
/**
* 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 {Integer} outerWindowID ID of the 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)
+function RequestNotifier(outerWindowID, listener, listenerObj)
{
- this.window = wnd;
this.listener = listener;
this.listenerObj = listenerObj || null;
- activeNotifiers.push(this);
- if (wnd)
- this.startScan(wnd);
- else
- this.scanComplete = true;
+ this.id = ++requestNotifierMaxId;
+ notifiers.set(this.id, this);
+
+ port.emit("startWindowScan", {
+ notifierID: this.id,
+ outerWindowID: outerWindowID
+ });
}
exports.RequestNotifier = RequestNotifier;
RequestNotifier.prototype =
{
/**
- * The window this notifier is associated with.
- * @type Window
+ * The unique ID of this notifier.
+ * @type Integer
*/
- window: null,
+ id: null,
/**
* The listener to be called when a new request is found.
@@ -124,268 +97,113 @@ RequestNotifier.prototype =
*/
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);
+ notifiers.delete(this.id);
+ port.emit("shutdownNotifier", this.id);
},
/**
* Notifies listener about a new request.
- * @param {Window} wnd
- * @param {Node} node
- * @param {RequestEntry} entry
+ * @param {Object} entry
*/
- notifyListener: function(wnd, node, entry)
+ notifyListener: function(entry)
{
- this.listener.call(this.listenerObj, wnd, node, entry, this.scanComplete);
+ this.listener.call(this.listenerObj, entry, this.scanComplete);
},
- /**
- * Number of currently posted scan events (will be 0 when the scan finishes
- * running).
- */
- eventsPosted: 0,
+ onComplete: function()
+ {
+ this.scanComplete = true;
+ this.notifyListener(null);
+ },
/**
- * Starts the initial scan of the window (will recurse into frames).
- * @param {Window} wnd the window to be scanned
+ * Makes the nodes associated with the given requests blink.
+ * @param {number[]} requests list of request IDs that were previously
+ * reported by this notifier.
+ * @param {Boolean} scrollToItem if true, scroll to first node
*/
- startScan: function(wnd)
+ flashNodes: function(requests, scrollToItem)
{
- let doc = wnd.document;
- let walker = doc.createTreeWalker(doc, Ci.nsIDOMNodeFilter.SHOW_ELEMENT, null, false);
+ if (!requests)
+ requests = [];
- let process = function()
- {
- if (!this.listener)
- return;
+ port.emit("flashNodes", {
+ notifierID: this.id,
+ requests,
+ scrollToItem
+ });
+ },
- let node = walker.currentNode;
- let data = getEntry(nodeData, node);
- if (typeof data != "undefined")
- for (let k in data)
- this.notifyListener(wnd, node, data[k]);
+ /**
+ * Attempts to calculate the size of the nodes associated with the requests.
+ * @param {number[]} requests list of request IDs that were previously
+ * reported by this notifier.
+ * @param {Function} callback function to be called with two parameters (x,y)
+ */
+ retrieveNodeSize: function(requests, callback)
+ {
+ if (!requests)
+ requests = [];
- 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]);
+ port.emitWithResponse("retrieveNodeSize", {
+ notifierID: this.id,
+ requests
+ }).then(callback);
+ },
- this.eventsPosted--;
- if (!this.eventsPosted)
- {
- this.scanComplete = true;
- this.notifyListener(wnd, null, null);
- }
- }
- }.bind(this);
+ /**
+ * Stores the nodes associated with the requests and generates a unique ID
+ * for them that can be used with Policy.refilterNodes(). Note that
+ * Policy.deleteNodes() always has to be called to release the memory.
+ * @param {number[]} requests list of request IDs that were previously
+ * reported by this notifier.
+ * @param {Function} callback function to be called with the nodes ID.
+ */
+ storeNodesForEntries: function(requests, callback)
+ {
+ if (!requests)
+ requests = [];
- // Process each node in a separate event to allow other events to process
- this.eventsPosted++;
- Utils.runAsync(process);
+ port.emitWithResponse("storeNodesForEntries", {
+ notifierID: this.id,
+ requests
+ }).then(callback);
}
};
-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)
+ * Associates a piece of data with a particular window.
+ * @param {number} outerWindowID the ID of the window
+ * @static
*/
-RequestNotifier.getWindowStatistics = function(/**Window*/ wnd)
+RequestNotifier.storeWindowData = function(outerWindowID, data)
{
- if (hasEntry(windowStats, wnd.document))
- return getEntry(windowStats, wnd.document);
- else
- return null;
-}
+ port.emit("storeWindowData", {
+ outerWindowID,
+ data
+ });
+};
/**
- * 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]}
+ * Retrieves a piece of data previously associated with the window by calling
+ * storeWindowData.
+ * @param {number} outerWindowID the ID of the window
+ * @param {Function} callback function to be called with the data.
* @static
*/
-RequestNotifier.getDataForNode = function(node, noParent, type, location)
+RequestNotifier.retrieveWindowData = function(outerWindowID, callback)
{
- while (node)
- {
- let data = getEntry(nodeData, node);
- if (typeof data != "undefined")
- {
- let entry = null;
- // Look for matching entry
- for (let k in data)
- {
- if ((!entry || entry.id < data[k].id) &&
- (typeof type == "undefined" || data[k].type == type) &&
- (typeof location == "undefined" || data[k].location == location))
- {
- entry = data[k];
- }
- }
- if (entry)
- 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;
+ port.emitWithResponse("retrieveWindowData", outerWindowID).then(callback);
};
-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.id = ++requestEntryMaxId;
-
- 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 =
+/**
+ * Retrieves the statistics for a window.
+ * @param {number} outerWindowID the ID of the window
+ * @param {Function} callback the callback to be called with the resulting
+ * object (object properties will be items, blocked,
+ * whitelisted, hidden, filters) or null.
+ */
+RequestNotifier.getWindowStatistics = function(outerWindowID, callback)
{
- /**
- * id of request (used to determine last entry attached to a node)
- * @type integer
- */
- id: 0,
- /**
- * 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;
- }
+ port.emitWithResponse("retrieveWindowStats", outerWindowID).then(callback);
};
diff --git a/data/extensions/spyblock@gnu.org/lib/subscriptionClasses.js b/data/extensions/spyblock@gnu.org/lib/subscriptionClasses.js
index 6ba15f5..5fe1eb8 100644
--- a/data/extensions/spyblock@gnu.org/lib/subscriptionClasses.js
+++ b/data/extensions/spyblock@gnu.org/lib/subscriptionClasses.js
@@ -1,6 +1,6 @@
/*
* This file is part of Adblock Plus <https://adblockplus.org/>,
- * Copyright (C) 2006-2015 Eyeo GmbH
+ * Copyright (C) 2006-2017 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
@@ -15,20 +15,22 @@
* along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>.
*/
+"use strict";
+
/**
* @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");
+const {ActiveFilter, BlockingFilter,
+ WhitelistFilter, ElemHideBase} = require("filterClasses");
+const {FilterNotifier} = require("filterNotifier");
+const {desc, extend} = require("coreUtils");
/**
* Abstract base class for filter subscriptions
*
- * @param {String} url download location of the subscription
- * @param {String} [title] title of the filter subscription
+ * @param {string} url download location of the subscription
+ * @param {string} [title] title of the filter subscription
* @constructor
*/
function Subscription(url, title)
@@ -37,11 +39,6 @@ function Subscription(url, title)
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;
@@ -50,13 +47,13 @@ Subscription.prototype =
{
/**
* Download location of the subscription
- * @type String
+ * @type {string}
*/
url: null,
/**
* Filters contained in the filter subscription
- * @type Array of Filter
+ * @type {Filter[]}
*/
filters: null,
@@ -66,7 +63,7 @@ Subscription.prototype =
/**
* Title of the filter subscription
- * @type String
+ * @type {string}
*/
get title()
{
@@ -78,14 +75,15 @@ Subscription.prototype =
{
let oldValue = this._title;
this._title = value;
- FilterNotifier.triggerListeners("subscription.title", this, value, oldValue);
+ FilterNotifier.triggerListeners("subscription.title",
+ this, value, oldValue);
}
return this._title;
},
/**
* Determines whether the title should be editable
- * @type Boolean
+ * @type {boolean}
*/
get fixedTitle()
{
@@ -97,14 +95,15 @@ Subscription.prototype =
{
let oldValue = this._fixedTitle;
this._fixedTitle = value;
- FilterNotifier.triggerListeners("subscription.fixedTitle", this, value, oldValue);
+ FilterNotifier.triggerListeners("subscription.fixedTitle",
+ this, value, oldValue);
}
return this._fixedTitle;
},
/**
* Defines whether the filters in the subscription should be disabled
- * @type Boolean
+ * @type {boolean}
*/
get disabled()
{
@@ -116,33 +115,36 @@ Subscription.prototype =
{
let oldValue = this._disabled;
this._disabled = value;
- FilterNotifier.triggerListeners("subscription.disabled", this, value, oldValue);
+ FilterNotifier.triggerListeners("subscription.disabled",
+ this, value, oldValue);
}
return this._disabled;
},
/**
- * Serializes the subscription to an array of strings for writing out on the disk.
- * @param {Array of String} buffer buffer to push the serialization results into
+ * Serializes the subscription to an array of strings for writing
+ * out on the disk.
+ * @param {string[]} buffer buffer to push the serialization results into
*/
- serialize: function(buffer)
+ serialize(buffer)
{
buffer.push("[Subscription]");
buffer.push("url=" + this.url);
- buffer.push("title=" + this._title);
+ if (this._title)
+ buffer.push("title=" + this._title);
if (this._fixedTitle)
buffer.push("fixedTitle=true");
if (this._disabled)
buffer.push("disabled=true");
},
- serializeFilters: function(buffer)
+ serializeFilters(buffer)
{
for (let filter of this.filters)
buffer.push(filter.text.replace(/\[/g, "\\["));
},
- toString: function()
+ toString()
{
let buffer = [];
this.serialize(buffer);
@@ -152,45 +154,40 @@ Subscription.prototype =
/**
* Cache for known filter subscriptions, maps URL to subscription objects.
- * @type Object
+ * @type {Object}
*/
Subscription.knownSubscriptions = Object.create(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
+ * @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;
+ if (url[0] != "~")
return new DownloadableSubscription(url, null);
- }
- catch (e)
- {
- return new SpecialSubscription(url);
- }
+ 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
+ * @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
+ if (obj.url[0] != "~")
{
- 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)
@@ -208,12 +205,7 @@ Subscription.fromObject = function(obj)
if ("version" in obj)
result.version = parseInt(obj.version, 10) || 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)
@@ -221,25 +213,8 @@ Subscription.fromObject = function(obj)
if ("downloadCount" in obj)
result.downloadCount = parseInt(obj.downloadCount, 10) || 0;
}
- catch (e)
+ else
{
- // 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(" ");
@@ -256,8 +231,8 @@ Subscription.fromObject = function(obj)
/**
* Class for special filter subscriptions (user's filters)
- * @param {String} url see Subscription()
- * @param {String} [title] see Subscription()
+ * @param {string} url see Subscription()
+ * @param {string} [title] see Subscription()
* @constructor
* @augments Subscription
*/
@@ -267,23 +242,20 @@ function SpecialSubscription(url, title)
}
exports.SpecialSubscription = SpecialSubscription;
-SpecialSubscription.prototype =
-{
- __proto__: Subscription.prototype,
-
+SpecialSubscription.prototype = extend(Subscription, {
/**
* Filter types that should be added to this subscription by default
* (entries should correspond to keys in SpecialSubscription.defaultsMap).
- * @type Array of String
+ * @type {string[]}
*/
defaults: null,
/**
* Tests whether a filter should be added to this group by default
* @param {Filter} filter filter to be tested
- * @return {Boolean}
+ * @return {boolean}
*/
- isDefaultFor: function(filter)
+ isDefaultFor(filter)
{
if (this.defaults && this.defaults.length)
{
@@ -301,35 +273,41 @@ SpecialSubscription.prototype =
/**
* See Subscription.serialize()
+ * @inheritdoc
*/
- serialize: function(buffer)
+ serialize(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(" "));
+ {
+ 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
-};
+SpecialSubscription.defaultsMap = Object.create(null, desc({
+ whitelist: WhitelistFilter,
+ blocking: BlockingFilter,
+ elemhide: ElemHideBase
+}));
/**
* Creates a new user-defined filter group.
- * @param {String} [title] title of the new filter group
- * @result {SpecialSubscription}
+ * @param {string} [title] title of the new filter group
+ * @return {SpecialSubscription}
*/
SpecialSubscription.create = function(title)
{
let url;
do
{
- url = "~user~" + Math.round(Math.random()*1000000);
+ url = "~user~" + Math.round(Math.random() * 1000000);
} while (url in Subscription.knownSubscriptions);
return new SpecialSubscription(url, title);
};
@@ -337,8 +315,10 @@ SpecialSubscription.create = function(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.
+ * @param {Filter} filter
+ * @return {SpecialSubscription}
*/
-SpecialSubscription.createForFilter = function(/**Filter*/ filter) /**SpecialSubscription*/
+SpecialSubscription.createForFilter = function(filter)
{
let subscription = SpecialSubscription.create();
subscription.filters.push(filter);
@@ -349,16 +329,14 @@ SpecialSubscription.createForFilter = function(/**Filter*/ filter) /**SpecialSub
}
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()
+ * 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
*/
@@ -368,16 +346,13 @@ function RegularSubscription(url, title)
}
exports.RegularSubscription = RegularSubscription;
-RegularSubscription.prototype =
-{
- __proto__: Subscription.prototype,
-
+RegularSubscription.prototype = extend(Subscription, {
_homepage: null,
_lastDownload: 0,
/**
* Filter subscription homepage if known
- * @type String
+ * @type {string}
*/
get homepage()
{
@@ -389,14 +364,16 @@ RegularSubscription.prototype =
{
let oldValue = this._homepage;
this._homepage = value;
- FilterNotifier.triggerListeners("subscription.homepage", this, value, oldValue);
+ 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
+ * Time of the last subscription download (in seconds since the
+ * beginning of the epoch)
+ * @type {number}
*/
get lastDownload()
{
@@ -408,15 +385,17 @@ RegularSubscription.prototype =
{
let oldValue = this._lastDownload;
this._lastDownload = value;
- FilterNotifier.triggerListeners("subscription.lastDownload", this, value, oldValue);
+ FilterNotifier.triggerListeners("subscription.lastDownload",
+ this, value, oldValue);
}
return this._lastDownload;
},
/**
* See Subscription.serialize()
+ * @inheritdoc
*/
- serialize: function(buffer)
+ serialize(buffer)
{
Subscription.prototype.serialize.call(this, buffer);
if (this._homepage)
@@ -424,12 +403,12 @@ RegularSubscription.prototype =
if (this._lastDownload)
buffer.push("lastDownload=" + this._lastDownload);
}
-};
+});
/**
* Class for filter subscriptions updated externally (by other extension)
- * @param {String} url see Subscription()
- * @param {String} [title] see Subscription()
+ * @param {string} url see Subscription()
+ * @param {string} [title] see Subscription()
* @constructor
* @augments RegularSubscription
*/
@@ -439,23 +418,23 @@ function ExternalSubscription(url, title)
}
exports.ExternalSubscription = ExternalSubscription;
-ExternalSubscription.prototype =
-{
- __proto__: RegularSubscription.prototype,
-
+ExternalSubscription.prototype = extend(RegularSubscription, {
/**
* See Subscription.serialize()
+ * @inheritdoc
*/
- serialize: function(buffer)
+ serialize(buffer)
{
- throw new Error("Unexpected call, external subscriptions should not be serialized");
+ throw new Error(
+ "Unexpected call, external subscriptions should not be serialized"
+ );
}
-};
+});
/**
* Class for filter subscriptions updated externally (by other extension)
- * @param {String} url see Subscription()
- * @param {String} [title] see Subscription()
+ * @param {string} url see Subscription()
+ * @param {string} [title] see Subscription()
* @constructor
* @augments RegularSubscription
*/
@@ -465,17 +444,14 @@ function DownloadableSubscription(url, title)
}
exports.DownloadableSubscription = DownloadableSubscription;
-DownloadableSubscription.prototype =
-{
- __proto__: RegularSubscription.prototype,
-
+DownloadableSubscription.prototype = extend(RegularSubscription, {
_downloadStatus: null,
_lastCheck: 0,
_errors: 0,
/**
* Status of the last download (ID of a string)
- * @type String
+ * @type {string}
*/
get downloadStatus()
{
@@ -485,7 +461,8 @@ DownloadableSubscription.prototype =
{
let oldValue = this._downloadStatus;
this._downloadStatus = value;
- FilterNotifier.triggerListeners("subscription.downloadStatus", this, value, oldValue);
+ FilterNotifier.triggerListeners("subscription.downloadStatus",
+ this, value, oldValue);
return this._downloadStatus;
},
@@ -496,10 +473,11 @@ DownloadableSubscription.prototype =
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
+ * 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()
{
@@ -511,26 +489,29 @@ DownloadableSubscription.prototype =
{
let oldValue = this._lastCheck;
this._lastCheck = value;
- FilterNotifier.triggerListeners("subscription.lastCheck", this, value, oldValue);
+ 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
+ * 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
+ * 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
+ * @type {number}
*/
get errors()
{
@@ -542,32 +523,27 @@ DownloadableSubscription.prototype =
{
let oldValue = this._errors;
this._errors = value;
- FilterNotifier.triggerListeners("subscription.errors", this, value, oldValue);
+ FilterNotifier.triggerListeners("subscription.errors", this,
+ value, oldValue);
}
return this._errors;
},
/**
* Version of the subscription data retrieved on last successful download
- * @type Number
+ * @type {number}
*/
version: 0,
/**
* Minimal Adblock Plus version required for this subscription
- * @type String
+ * @type {string}
*/
requiredVersion: null,
/**
- * Should be true if requiredVersion is higher than current Adblock Plus version
- * @type Boolean
- */
- upgradeRequired: false,
-
- /**
* Number indicating how often the object was downloaded.
- * @type Number
+ * @type {number}
*/
downloadCount: 0,
@@ -579,8 +555,9 @@ DownloadableSubscription.prototype =
/**
* See Subscription.serialize()
+ * @inheritdoc
*/
- serialize: function(buffer)
+ serialize(buffer)
{
RegularSubscription.prototype.serialize.call(this, buffer);
if (this.downloadStatus)
@@ -604,4 +581,4 @@ DownloadableSubscription.prototype =
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
index a250e81..3f9d973 100644
--- a/data/extensions/spyblock@gnu.org/lib/sync.js
+++ b/data/extensions/spyblock@gnu.org/lib/sync.js
@@ -1,6 +1,6 @@
/*
* This file is part of Adblock Plus <https://adblockplus.org/>,
- * Copyright (C) 2006-2015 Eyeo GmbH
+ * Copyright (C) 2006-2017 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
diff --git a/data/extensions/spyblock@gnu.org/lib/synchronizer.js b/data/extensions/spyblock@gnu.org/lib/synchronizer.js
index 2304895..b8d14a2 100644
--- a/data/extensions/spyblock@gnu.org/lib/synchronizer.js
+++ b/data/extensions/spyblock@gnu.org/lib/synchronizer.js
@@ -1,6 +1,6 @@
/*
* This file is part of Adblock Plus <https://adblockplus.org/>,
- * Copyright (C) 2006-2015 Eyeo GmbH
+ * Copyright (C) 2006-2017 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
@@ -15,29 +15,29 @@
* along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>.
*/
+"use strict";
+
/**
* @fileOverview Manages synchronization of filter subscriptions.
*/
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
-
-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");
+const {Downloader, Downloadable,
+ MILLIS_IN_SECOND, MILLIS_IN_MINUTE,
+ MILLIS_IN_HOUR, MILLIS_IN_DAY} = require("downloader");
+const {Filter} = require("filterClasses");
+const {FilterStorage} = require("filterStorage");
+const {FilterNotifier} = require("filterNotifier");
+const {Prefs} = require("prefs");
+const {Subscription, DownloadableSubscription} = require("subscriptionClasses");
+const {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;
+const INITIAL_DELAY = 1 * MILLIS_IN_MINUTE;
+const CHECK_INTERVAL = 1 * MILLIS_IN_HOUR;
+const DEFAULT_EXPIRATION_INTERVAL = 5 * MILLIS_IN_DAY;
/**
* The object providing actual downloading functionality.
- * @type Downloader
+ * @type {Downloader}
*/
let downloader = null;
@@ -51,10 +51,11 @@ let Synchronizer = exports.Synchronizer =
/**
* Called on module startup.
*/
- init: function()
+ init()
{
- downloader = new Downloader(this._getDownloadables.bind(this), INITIAL_DELAY, CHECK_INTERVAL);
- onShutdown.add(function()
+ downloader = new Downloader(this._getDownloadables.bind(this),
+ INITIAL_DELAY, CHECK_INTERVAL);
+ onShutdown.add(() =>
{
downloader.cancel();
});
@@ -67,20 +68,23 @@ let Synchronizer = exports.Synchronizer =
/**
* Checks whether a subscription is currently being downloaded.
- * @param {String} url URL of the subscription
- * @return {Boolean}
+ * @param {string} url URL of the subscription
+ * @return {boolean}
*/
- isExecuting: function(url)
+ isExecuting(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)
+ * @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)
+ execute(subscription, manual)
{
downloader.download(this._getDownloadable(subscription, manual));
},
@@ -88,7 +92,7 @@ let Synchronizer = exports.Synchronizer =
/**
* Yields Downloadable instances for all subscriptions that can be downloaded.
*/
- _getDownloadables: function()
+ *_getDownloadables()
{
if (!Prefs.subscriptions_autoupdate)
return;
@@ -102,8 +106,11 @@ let Synchronizer = exports.Synchronizer =
/**
* Creates a Downloadable instance for a subscription.
+ * @param {Subscription} subscription
+ * @param {boolean} manual
+ * @return {Downloadable}
*/
- _getDownloadable: function(/**Subscription*/ subscription, /**Boolean*/ manual) /**Downloadable*/
+ _getDownloadable(subscription, manual)
{
let result = new Downloadable(subscription.url);
if (subscription.lastDownload != subscription.lastSuccess)
@@ -118,27 +125,34 @@ let Synchronizer = exports.Synchronizer =
return result;
},
- _onExpirationChange: function(downloadable)
+ _onExpirationChange(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);
+ 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)
+ _onDownloadStarted(downloadable)
{
let subscription = Subscription.fromURL(downloadable.url);
- FilterNotifier.triggerListeners("subscription.downloadStatus", subscription);
+ FilterNotifier.triggerListeners("subscription.downloading", subscription);
},
- _onDownloadSuccess: function(downloadable, responseText, errorCallback, redirectCallback)
+ _onDownloadSuccess(downloadable, responseText, errorCallback,
+ redirectCallback)
{
let lines = responseText.split(/[\r\n]+/);
- let match = /\[Adblock(?:\s*Plus\s*([\d\.]+)?)?\]/i.exec(lines[0]);
- if (!match)
+ let headerMatch = /\[Adblock(?:\s*Plus\s*([\d.]+)?)?\]/i.exec(lines[0]);
+ if (!headerMatch)
return errorCallback("synchronize_invalid_data");
- let minVersion = match[1];
+ let minVersion = headerMatch[1];
// Don't remove parameter comments immediately but add them to a list first,
// they need to be considered in the checksum calculation.
@@ -177,8 +191,10 @@ let Synchronizer = exports.Synchronizer =
return redirectCallback(params.redirect);
// Handle redirects
- let subscription = Subscription.fromURL(downloadable.redirectURL || downloadable.url);
- if (downloadable.redirectURL && downloadable.redirectURL != downloadable.url)
+ 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;
@@ -196,7 +212,9 @@ let Synchronizer = exports.Synchronizer =
}
// The download actually succeeded
- subscription.lastSuccess = subscription.lastDownload = Math.round(Date.now() / MILLIS_IN_SECOND);
+ subscription.lastSuccess = subscription.lastDownload = Math.round(
+ Date.now() / MILLIS_IN_SECOND
+ );
subscription.downloadStatus = "synchronize_ok";
subscription.downloadCount = downloadable.downloadCount;
subscription.errors = 0;
@@ -208,9 +226,18 @@ let Synchronizer = exports.Synchronizer =
// Process parameters
if (params.homepage)
{
- let uri = Utils.makeURI(params.homepage);
- if (uri && (uri.scheme == "http" || uri.scheme == "https"))
- subscription.homepage = uri.spec;
+ let url;
+ try
+ {
+ url = new URL(params.homepage);
+ }
+ catch (e)
+ {
+ url = null;
+ }
+
+ if (url && (url.protocol == "http:" || url.protocol == "https:"))
+ subscription.homepage = url.href;
}
if (params.privatemode)
@@ -242,19 +269,17 @@ let Synchronizer = exports.Synchronizer =
}
}
- let [softExpiration, hardExpiration] = downloader.processExpirationInterval(expirationInterval);
+ 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;
- }
+ else
+ delete subscription.requiredVersion;
// Process filters
lines.shift();
@@ -271,7 +296,8 @@ let Synchronizer = exports.Synchronizer =
return undefined;
},
- _onDownloadError: function(downloadable, downloadURL, error, channelStatus, responseStatus, redirectCallback)
+ _onDownloadError(downloadable, downloadURL, error, channelStatus,
+ responseStatus, redirectCallback)
{
let subscription = Subscription.fromURL(downloadable.url);
subscription.lastDownload = Math.round(Date.now() / MILLIS_IN_SECOND);
@@ -282,18 +308,26 @@ let Synchronizer = exports.Synchronizer =
{
subscription.errors++;
- if (redirectCallback && subscription.errors >= Prefs.subscriptions_fallbackerrors && /^https?:\/\//i.test(subscription.url))
+ 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));
+ const {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;
@@ -302,7 +336,7 @@ let Synchronizer = exports.Synchronizer =
request.channel.loadFlags = request.channel.loadFlags |
request.channel.INHIBIT_CACHING |
request.channel.VALIDATE_ALWAYS;
- request.addEventListener("load", function(ev)
+ request.addEventListener("load", ev =>
{
if (onShutdown.done)
return;
@@ -311,17 +345,21 @@ let Synchronizer = exports.Synchronizer =
return;
let match = /^(\d+)(?:\s+(\S+))?$/.exec(request.responseText);
- if (match && match[1] == "301" && match[2] && /^https?:\/\//i.test(match[2])) // Moved permanently
+ if (match && match[1] == "301" && // Moved permanently
+ match[2] && /^https?:\/\//i.test(match[2]))
+ {
redirectCallback(match[2]);
- else if (match && match[1] == "410") // Gone
+ }
+ else if (match && match[1] == "410") // Gone
{
- let data = "[Adblock]\n" + subscription.filters.map((f) => f.text).join("\n");
+ 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/ui.js b/data/extensions/spyblock@gnu.org/lib/ui.js
index 6009f9e..1941a97 100644
--- a/data/extensions/spyblock@gnu.org/lib/ui.js
+++ b/data/extensions/spyblock@gnu.org/lib/ui.js
@@ -1,6 +1,6 @@
/*
* This file is part of Adblock Plus <https://adblockplus.org/>,
- * Copyright (C) 2006-2015 Eyeo GmbH
+ * Copyright (C) 2006-2017 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
@@ -19,6 +19,7 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
let {Utils} = require("utils");
+let {port} = require("messaging");
let {Prefs} = require("prefs");
let {Policy} = require("contentPolicy");
let {FilterStorage} = require("filterStorage");
@@ -125,6 +126,14 @@ let optionsObserver =
this.value = Prefs.savestats;
});
+ hideElement("adblockplus-shownotifications", !Prefs.notifications_showui);
+ setChecked("adblockplus-shownotifications", Prefs.notifications_ignoredcategories.indexOf("*") == -1);
+ addCommandHandler("adblockplus-shownotifications", function()
+ {
+ Notification.toggleIgnoreCategory("*");
+ this.value = (Prefs.notifications_ignoredcategories.indexOf("*") == -1);
+ });
+
let hasAcceptableAds = FilterStorage.subscriptions.some((subscription) => subscription instanceof DownloadableSubscription &&
subscription.url == Prefs.subscriptions_exceptionsurl);
setChecked("adblockplus-acceptableAds", hasAcceptableAds);
@@ -268,64 +277,57 @@ let UI = exports.UI =
*/
init: function()
{
- // We should call initDone once both overlay and filters are loaded
- let overlayLoaded = false;
- let filtersLoaded = false;
- let sessionRestored = false;
+ // We have to wait for multiple events before running start-up actions
+ let prerequisites = [];
// Start loading overlay
- let request = new XMLHttpRequest();
- request.mozBackgroundRequest = true;
- request.open("GET", "chrome://adblockplus/content/ui/overlay.xul");
- request.addEventListener("load", function(event)
+ prerequisites.push(new Promise((resolve, reject) =>
{
- if (onShutdown.done)
- return;
+ let request = new XMLHttpRequest();
+ request.mozBackgroundRequest = true;
+ request.open("GET", "chrome://adblockplus/content/ui/overlay.xul");
+ request.channel.owner = Utils.systemPrincipal;
+ request.addEventListener("load", event =>
+ {
+ if (onShutdown.done)
+ return;
- this.processOverlay(request.responseXML.documentElement);
+ this.processOverlay(request.responseXML.documentElement);
- // Don't wait for the rest of the startup sequence, add icon already
- this.addToolbarButton();
+ // 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);
+ resolve();
+ }, false);
- // Wait for filters to load
- if (FilterStorage._loading)
- {
- let listener = function(action)
+ request.addEventListener("error", event =>
{
- if (action != "load")
- return;
+ reject(new Error("Unexpected: Failed to load overlay.xul"));
+ });
- FilterNotifier.removeListener(listener);
- filtersLoaded = true;
- if (overlayLoaded && filtersLoaded && sessionRestored)
- this.initDone();
- }.bind(this);
- FilterNotifier.addListener(listener);
- }
- else
- filtersLoaded = true;
+ request.send(null);
+ }));
+
+ // Wait for filters to load
+ if (!FilterStorage.initialized)
+ prerequisites.push(FilterNotifier.once("load"));
- // Initialize UI after the session is restored
- let window = this.currentWindow;
- if (!window && "nsISessionStore" in Ci)
+ // Wait for session to be restored
+ prerequisites.push(new Promise((resolve, reject) =>
{
- // No application windows yet, the application must be starting up. Wait
- // for session to be restored before initializing our UI.
- new SessionRestoreObserver(function()
+ let window = this.currentWindow;
+ if (!window && "nsISessionStore" in Ci)
{
- sessionRestored = true;
- if (overlayLoaded && filtersLoaded && sessionRestored)
- this.initDone();
- }.bind(this));
- }
- else
- sessionRestored = true;
+ // No application windows yet, the application must be starting up. Wait
+ // for session to be restored before initializing our UI.
+ new SessionRestoreObserver(resolve);
+ }
+ else
+ resolve();
+ }));
+
+ Promise.all(prerequisites).then(() => this.initDone())
+ .catch(e => Cu.reportError(e));
},
/**
@@ -403,44 +405,45 @@ let UI = exports.UI =
this.updateState();
// Listen for pref and filters changes
- Prefs.addListener(function(name)
+ Prefs.addListener(name =>
{
if (name == "enabled" || name == "defaulttoolbaraction" || name == "defaultstatusbaraction")
this.updateState();
else if (name == "showinstatusbar")
{
- for (let window in this.applicationWindows)
+ for (let window of this.applicationWindows)
this.updateStatusbarIcon(window);
}
- }.bind(this));
- FilterNotifier.addListener(function(action)
+ });
+
+ for (let eventName of [
+ "filter.added", "filter.removed", "filter.disabled",
+ "subscription.added", "subscription.removed", "subscription.disabled",
+ "subscription.updated", "load"
+ ])
{
- if (/^(filter|subscription)\.(added|removed|disabled|updated)$/.test(action) || action == "load")
- this.updateState();
- }.bind(this));
+ FilterNotifier.on(eventName, () => this.updateState());
+ }
+
+ Notification.addShowListener(notification =>
+ {
+ let window = this.currentWindow;
+ if (!window)
+ return;
+
+ let button = window.document.getElementById("abp-toolbarbutton")
+ || window.document.getElementById("abp-status");
+ if (!button)
+ return;
- 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());
+ this._showNotification(window, button, notification);
+ });
// 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);
- });
+ // Initialize subscribe link handling
+ port.on("subscribeLinkClick", data => this.subscribeLinkClicked(data));
// Execute first-run actions if a window is open already, otherwise it
// will happen in applyToWindow() when a window is opened.
@@ -494,7 +497,7 @@ let UI = exports.UI =
firstRunActions: function(window)
{
- if (this.firstRunDone || !window || FilterStorage._loading)
+ if (this.firstRunDone || !window || !FilterStorage.initialized)
return;
this.firstRunDone = true;
@@ -525,7 +528,7 @@ let UI = exports.UI =
*/
applyToWindow: function(/**Window*/ window, /**Boolean*/ noDelay)
{
- let {delayInitialization, isKnownWindow, getBrowser, addBrowserLocationListener, addBrowserClickListener} = require("appSupport");
+ let {delayInitialization, isKnownWindow, getBrowser, addBrowserLocationListener} = require("appSupport");
if (window.document.documentElement.id == "CustomizeToolbarWindow" || isKnownWindow(window))
{
// Add style processing instruction
@@ -575,12 +578,25 @@ let UI = exports.UI =
{
this.updateIconState(window, window.document.getElementById("abp-status"));
this.updateIconState(window, window.document.getElementById("abp-toolbarbutton"));
+
+ Notification.showNext(this.getCurrentLocation(window).spec);
}.bind(this));
- addBrowserClickListener(window, this.onBrowserClick.bind(this, window));
- window.document.getElementById("abp-notification-close").addEventListener("command", function(event)
+ let notificationPanel = window.document.getElementById("abp-notification");
+ notificationPanel.addEventListener("command", function(event)
{
- window.document.getElementById("abp-notification").hidePopup();
+ switch (event.target.id)
+ {
+ case "abp-notification-close":
+ notificationPanel.classList.add("abp-closing");
+ break;
+ case "abp-notification-optout":
+ Notification.toggleIgnoreCategory("*", true);
+ /* FALL THROUGH */
+ case "abp-notification-hide":
+ notificationPanel.hidePopup();
+ break;
+ }
}, false);
// First-run actions?
@@ -599,7 +615,7 @@ let UI = exports.UI =
*/
removeFromWindow: function(/**Window*/ window)
{
- let {isKnownWindow, removeBrowserLocationListeners, removeBrowserClickListeners} = require("appSupport");
+ let {isKnownWindow, removeBrowserLocationListeners} = require("appSupport");
if (window.document.documentElement.id == "CustomizeToolbarWindow" || isKnownWindow(window))
{
// Remove style processing instruction
@@ -634,7 +650,6 @@ let UI = exports.UI =
window.removeEventListener("popupshowing", this.onPopupShowing, false);
window.removeEventListener("keypress", this.onKeyPress, false);
removeBrowserLocationListeners(window);
- removeBrowserClickListeners(window);
},
/**
@@ -716,14 +731,17 @@ let UI = exports.UI =
/**
- * Brings up the filter composer dialog to block an item.
+ * Brings up the filter composer dialog to block an item. The optional nodesID
+ * parameter must be a unique ID returned by
+ * RequestNotifier.storeNodesForEntry() or similar.
*/
- blockItem: function(/**Window*/ window, /**Node*/ node, /**RequestEntry*/ item)
+ blockItem: function(/**Window*/ window, /**string*/ nodesID, /**RequestEntry*/ item)
{
if (!item)
return;
- window.openDialog("chrome://adblockplus/content/ui/composer.xul", "_blank", "chrome,centerscreen,resizable,dialog=no,dependent", [node], item);
+ window.openDialog("chrome://adblockplus/content/ui/composer.xul", "_blank",
+ "chrome,centerscreen,resizable,dialog=no,dependent", nodesID, item);
},
/**
@@ -762,7 +780,10 @@ let UI = exports.UI =
if (uri)
{
let {getBrowser} = require("appSupport");
- window.openDialog("chrome://adblockplus/content/ui/sendReport.xul", "_blank", "chrome,centerscreen,resizable=no", getBrowser(window).contentWindow, uri);
+ let browser = getBrowser(window);
+ if ("selectedBrowser" in browser)
+ browser = browser.selectedBrowser;
+ window.openDialog("chrome://adblockplus/content/ui/sendReport.xul", "_blank", "chrome,centerscreen,resizable=no", browser.outerWindowID, uri, browser);
}
}
},
@@ -852,6 +873,7 @@ let UI = exports.UI =
function notifyUser()
{return;
+
let {addTab} = require("appSupport");
if (addTab)
{
@@ -902,64 +924,12 @@ let UI = exports.UI =
},
/**
- * 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).
+ * Called whenever child/subscribeLinks module intercepts clicks on abp: links
+ * as well as links to subscribe.adblockplus.org.
*/
- onBrowserClick: function (/**Window*/ window, /**Event*/ event, /**String*/ linkTarget)
+ subscribeLinkClicked: function({title, url,
+ mainSubscriptionTitle, mainSubscriptionURL})
{
- 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;
@@ -997,7 +967,7 @@ let UI = exports.UI =
mainSubscriptionURL = mainSubscriptionURL.spec;
}
- this.openSubscriptionDialog(window, url, title, mainSubscriptionURL, mainSubscriptionTitle);
+ this.openSubscriptionDialog(this.currentWindow, url, title, mainSubscriptionURL, mainSubscriptionTitle);
},
/**
@@ -1093,7 +1063,7 @@ let UI = exports.UI =
*/
updateState: function()
{
- for (let window in this.applicationWindows)
+ for (let window of this.applicationWindows)
{
this.updateIconState(window, window.document.getElementById("abp-status"));
this.updateIconState(window, window.document.getElementById("abp-toolbarbutton"));
@@ -1197,7 +1167,10 @@ let UI = exports.UI =
FilterStorage.removeFilter(filter);
}
else
+ {
+ filter.disabled = false;
FilterStorage.addFilter(filter);
+ }
},
@@ -1419,54 +1392,66 @@ let UI = exports.UI =
}
statusDescr.setAttribute("value", statusStr);
- let activeFilters = [];
- E("abp-tooltip-blocked-label").hidden = (state != "active");
- E("abp-tooltip-blocked").hidden = (state != "active");
+ E("abp-tooltip-blocked-label").hidden = true;
+ E("abp-tooltip-blocked").hidden = true;
+ E("abp-tooltip-filters-label").hidden = true;
+ E("abp-tooltip-filters").hidden = true;
+ E("abp-tooltip-more-filters").hidden = true;
+
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)
+ let browser = getBrowser(window);
+ if ("selectedBrowser" in browser)
+ browser = browser.selectedBrowser;
+ let outerWindowID = browser.outerWindowID;
+ RequestNotifier.getWindowStatistics(outerWindowID, (stats) =>
{
- blockedStr += " " + Utils.getString("blocked_count_addendum");
- blockedStr = blockedStr.replace(/\?1\?/, stats.whitelisted).replace(/\?2\?/, stats.hidden);
- }
+ E("abp-tooltip-blocked-label").hidden = false;
+ E("abp-tooltip-blocked").hidden = false;
- E("abp-tooltip-blocked").setAttribute("value", blockedStr);
+ let blockedStr = Utils.getString("blocked_count_tooltip");
+ blockedStr = blockedStr.replace(/\?1\?/, stats ? stats.blocked : 0).replace(/\?2\?/, stats ? stats.items : 0);
- if (stats)
- {
- let filterSort = function(a, b)
+ if (stats && stats.whitelisted + stats.hidden)
{
- return stats.filters[b] - stats.filters[a];
- };
- for (let filter in stats.filters)
- activeFilters.push(filter);
- activeFilters = activeFilters.sort(filterSort);
- }
+ blockedStr += " " + Utils.getString("blocked_count_addendum");
+ blockedStr = blockedStr.replace(/\?1\?/, stats.whitelisted).replace(/\?2\?/, stats.hidden);
+ }
- if (activeFilters.length > 0)
- {
- let filtersContainer = E("abp-tooltip-filters");
- while (filtersContainer.firstChild)
- filtersContainer.removeChild(filtersContainer.firstChild);
+ E("abp-tooltip-blocked").setAttribute("value", blockedStr);
- for (let i = 0; i < activeFilters.length && i < 3; i++)
+ let activeFilters = [];
+ if (stats)
{
- let descr = filtersContainer.ownerDocument.createElement("description");
- descr.setAttribute("value", activeFilters[i] + " (" + stats.filters[activeFilters[i]] + ")");
- filtersContainer.appendChild(descr);
+ 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);
}
- }
- }
- 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);
+ 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);
+ });
+ }
},
/**
@@ -1562,10 +1547,12 @@ let UI = exports.UI =
let hasStatusBar = statusbarPosition;
hideElement(prefix + "showintoolbar", !hasToolbar || prefix == "abp-toolbar-");
hideElement(prefix + "showinstatusbar", !hasStatusBar);
+ hideElement(prefix + "shownotifications", !Prefs.notifications_showui);
hideElement(prefix + "iconSettingsSeparator", (prefix == "abp-toolbar-" || !hasToolbar) && !hasStatusBar);
setChecked(prefix + "showintoolbar", this.isToolbarIconVisible());
setChecked(prefix + "showinstatusbar", Prefs.showinstatusbar);
+ setChecked(prefix + "shownotifications", Prefs.notifications_ignoredcategories.indexOf("*") == -1);
let {Sync} = require("sync");
let syncEngine = Sync.getEngine();
@@ -1611,29 +1598,36 @@ let UI = exports.UI =
*/
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));
+ let window = popup.ownerDocument.defaultView;
+ let data = window.gContextMenuContentData;
+ if (!data)
+ {
+ // This is SeaMonkey Mail or Thunderbird, they won't get context menu data
+ // for us. Send the notification ourselves.
+ data = {
+ event: {target: popup.triggerNode},
+ addonInfo: {},
+ get wrappedJSObject() {return this;}
+ };
+ Services.obs.notifyObservers(data, "AdblockPlus:content-contextmenu", null);
}
- if (!target)
+ if (typeof data.addonInfo != "object" || typeof data.addonInfo.adblockplus != "object")
return;
- let window = popup.ownerDocument.defaultView;
+ let items = data.addonInfo.adblockplus;
+ let clicked = null;
let menuItems = [];
- let addMenuItem = function([node, nodeData])
+
+ function menuItemTriggered(id, nodeData)
{
- let type = nodeData.typeDescr.toLowerCase();
- if (type == "background")
- {
- type = "image";
- node = null;
- }
+ clicked = id;
+ this.blockItem(window, id, nodeData);
+ }
+ for (let [id, nodeData] of items)
+ {
+ let type = nodeData.type.toLowerCase();
let label = this.overlay.attributes[type + "contextlabel"];
if (!label)
return;
@@ -1641,66 +1635,10 @@ let UI = exports.UI =
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);
+ item.addEventListener("command", menuItemTriggered.bind(this, id, 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
@@ -1722,20 +1660,21 @@ let UI = exports.UI =
}
// Make sure to clean up everything once the context menu is closed
- if (menuItems.length)
+ let cleanUp = function(event)
{
- let cleanUp = function(event)
- {
- if (event.eventPhase != event.AT_TARGET)
- return;
+ 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);
- }
+ popup.removeEventListener("popuphidden", cleanUp, false);
+ for (let menuItem of menuItems)
+ if (menuItem.parentNode)
+ menuItem.parentNode.removeChild(menuItem);
+
+ for (let [id, nodeData] of items)
+ if (id && id != clicked)
+ Policy.deleteNodes(id);
+ }.bind(this);
+ popup.addEventListener("popuphidden", cleanUp, false);
},
/**
@@ -1815,8 +1754,10 @@ let UI = exports.UI =
removeBottomBar(window);
let browser = (getBrowser ? getBrowser(window) : null);
+ if (browser && "selectedBrowser" in browser)
+ browser = browser.selectedBrowser;
if (browser)
- browser.contentWindow.focus();
+ browser.focus();
}
else if (!detach)
{
@@ -1857,24 +1798,6 @@ let UI = exports.UI =
}
},
- 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");
@@ -1940,9 +1863,11 @@ let UI = exports.UI =
window.document.getElementById("abp-notification-yes").onclick = buttonHandler.bind(null, true);
window.document.getElementById("abp-notification-no").onclick = buttonHandler.bind(null, false);
}
+ else
+ Notification.markAsShown(notification.id);
panel.setAttribute("class", "abp-" + notification.type);
- panel.setAttribute("noautohide", notification.type === "question");
+ panel.setAttribute("noautohide", true);
panel.openPopup(button, "bottomcenter topcenter", 0, 0, false, false, null);
}
};
@@ -1969,12 +1894,13 @@ let eventHandlers = [
["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)]
+ ["abp-command-contribute-hide", "command", UI.hideContributeButton.bind(UI)],
+ ["abp-command-toggleshownotifications", "command", Notification.toggleIgnoreCategory.bind(Notification, "*", null)]
];
onShutdown.add(function()
{
- for (let window in UI.applicationWindows)
+ for (let window of 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
index 13f4876..7b07041 100644
--- a/data/extensions/spyblock@gnu.org/lib/utils.js
+++ b/data/extensions/spyblock@gnu.org/lib/utils.js
@@ -1,6 +1,6 @@
/*
* This file is part of Adblock Plus <https://adblockplus.org/>,
- * Copyright (C) 2006-2015 Eyeo GmbH
+ * Copyright (C) 2006-2017 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
@@ -213,47 +213,18 @@ let Utils = exports.Utils =
/**
* 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.
+ * asynchronously.
* @param {function} callback
- * @param {object} thisPtr
*/
- runAsync: function(callback, thisPtr)
+ runAsync: function(callback)
{
- 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;
+ Services.tm.currentThread.dispatch(callback, Ci.nsIEventTarget.DISPATCH_NORMAL);
},
/**
* Generates filter subscription checksum.
*
- * @param {Array of String} lines filter subscription lines (with checksum line removed)
+ * @param {string[]} lines filter subscription lines (with checksum line removed)
* @return {String} checksum or null
*/
generateChecksum: function(lines)
@@ -367,22 +338,6 @@ let Utils = exports.Utils =
},
/**
- * 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)
@@ -618,8 +573,6 @@ XPCOMUtils.defineLazyServiceGetter(Utils, "windowWatcher", "@mozilla.org/embedco
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");
@@ -627,7 +580,7 @@ XPCOMUtils.defineLazyGetter(Utils, "crypto", function()
{
try
{
- let ctypes = Components.utils.import("resource://gre/modules/ctypes.jsm", null).ctypes;
+ let ctypes = Cu.import("resource://gre/modules/ctypes.jsm", null).ctypes;
let nsslib;
try
diff --git a/data/extensions/spyblock@gnu.org/lib/whitelisting.js b/data/extensions/spyblock@gnu.org/lib/whitelisting.js
new file mode 100644
index 0000000..1006d26
--- /dev/null
+++ b/data/extensions/spyblock@gnu.org/lib/whitelisting.js
@@ -0,0 +1,46 @@
+/*
+ * This file is part of Adblock Plus <https://adblockplus.org/>,
+ * Copyright (C) 2006-2017 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 is a dummy to provide a function needed by message
+ * responder.
+ */
+
+"use strict";
+
+let {Policy} = require("contentPolicy");
+let {RegExpFilter} = require("filterClasses");
+
+// NOTE: The function interface is supposed to be compatible with
+// checkWhitelisted in adblockpluschrome. That's why there is a typeMask
+// parameter here. However, this parameter is only used to decide whether
+// elemhide whitelisting should be considered, so only supported values for this
+// parameter are RegExpFilter.typeMap.DOCUMENT and
+// RegExpFilter.typeMap.DOCUMENT | RegExpFilter.typeMap.ELEMHIDE.
+exports.checkWhitelisted = function(page, frames, typeMask)
+{
+ let match =
+ Policy.isFrameWhitelisted(frames, typeMask & RegExpFilter.typeMap.ELEMHIDE);
+ if (match)
+ {
+ let [frameIndex, matchType, docDomain, thirdParty, location, filter] = match;
+ if (matchType == "DOCUMENT" || matchType == "ELEMHIDE")
+ return filter;
+ }
+
+ return null;
+};