diff options
Diffstat (limited to 'data/extensions/spyblock@gnu.org/lib')
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; +}; |