/*
* This file is part of Adblock Plus ,
* 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 .
*/
/**
* @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.
*/
let collapsedClass = port.emitWithResponse("getCollapsedClass");
/**
* Maps numerical content type IDs to strings.
* @type Map.
*/
let types = new Map();
/**
* Contains nodes stored by storeNodes() mapped by their IDs.
* @type Map.
*/
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);
}
});
}