diff options
Diffstat (limited to 'data/extensions/spyblock@gnu.org/lib/child/contentPolicy.js')
-rw-r--r-- | data/extensions/spyblock@gnu.org/lib/child/contentPolicy.js | 518 |
1 files changed, 518 insertions, 0 deletions
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); + } + }); +} |