summaryrefslogtreecommitdiff
path: root/data/extensions/spyblock@gnu.org/lib/child/contentPolicy.js
diff options
context:
space:
mode:
Diffstat (limited to 'data/extensions/spyblock@gnu.org/lib/child/contentPolicy.js')
-rw-r--r--data/extensions/spyblock@gnu.org/lib/child/contentPolicy.js518
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);
+ }
+ });
+}