/* * 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); } }); }