/* * 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"; 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, RegExpFilter} = require("filterClasses"); let {defaultMatcher} = require("matcher"); /** * Public policy checking functions and auxiliary objects * @class */ var Policy = exports.Policy = { /** * 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. */ contentTypes: new Map(function* () { // Treat navigator.sendBeacon() the same as , // it's essentially the same concept - merely generalized. yield ["BEACON", "PING"]; // Treat and 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]; }()), /** * Set of content types that aren't associated with a visual document area * @type Set. */ 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 Set. */ whitelistSchemes: new Set(), /** * Called on module startup, initializes various exported properties. */ init: function() { // whitelisted URL schemes for (let scheme of Prefs.whitelistschemes.toLowerCase().split(" ")) this.whitelistSchemes.add(scheme); 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(() => { Utils.styleService.unregisterSheet(collapseStyle, Ci.nsIStyleSheetService.USER_SHEET); }); }, /** * Checks whether a node should be blocked, hides it if necessary * @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. */ shouldAllow: function({contentType, location, frames, isPrivate}) { let hits = []; function addHit(frameIndex, contentType, docDomain, thirdParty, location, filter) { if (filter && !isPrivate) FilterStorage.increaseHitCount(filter); hits.push({ frameIndex, contentType, docDomain, thirdParty, location, filter: filter ? filter.text : null, filterType: filter ? filter.type : null }); } function response(allow, collapse) { return {allow, collapse, hits}; } // Ignore whitelisted schemes if (contentType != "POPUP" && !this.isBlockableScheme(location)) return response(true, false); // Interpret unknown types as "other" contentType = this.contentTypes.get(contentType) || "OTHER"; let nogeneric = false; if (Prefs.enabled) { let whitelistHit = this.isFrameWhitelisted(frames, false); if (whitelistHit) { let [frameIndex, matchType, docDomain, thirdParty, location, filter] = whitelistHit; addHit(frameIndex, matchType, docDomain, thirdParty, location, filter); if (matchType == "DOCUMENT") return response(true, false); else nogeneric = true; } } 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 && RegExpFilter.typeMap.hasOwnProperty(contentType)) { 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); return response(!match || match instanceof WhitelistFilter, collapse); }, /** * Checks whether the location's scheme is blockable. * @param location {nsIURI|String} * @return {Boolean} */ isBlockableScheme: function(location) { 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 top-level window is whitelisted. * @param {String} url * URL of the document loaded into the window * @return {?WhitelistFilter} * exception rule that matched the URL if any */ isWhitelisted: function(url) { if (!url) return null; // Do not apply exception rules to schemes on our whitelistschemes list. if (!this.isBlockableScheme(url)) return null; // Ignore fragment identifier let index = url.indexOf("#"); if (index >= 0) url = url.substring(0, index); let result = defaultMatcher.matchesAny(url, RegExpFilter.typeMap.DOCUMENT, getHostname(url), false, null); return (result instanceof WhitelistFilter ? result : null); }, /** * 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. */ isFrameWhitelisted: function(frames, isElemHide) { let [sitekey, sitekeyFrame] = getSitekey(frames); let nogenericHit = null; let typeMap = RegExpFilter.typeMap.DOCUMENT; if (isElemHide) typeMap = typeMap | RegExpFilter.typeMap.ELEMHIDE; let genericType = (isElemHide ? "GENERICHIDE" : "GENERICBLOCK"); for (let i = 0; i < frames.length; i++) { let frame = frames[i]; let wndLocation = frame.location; let parentWndLocation = frames[Math.min(i + 1, frames.length - 1)].location; let parentDocDomain = getHostname(parentWndLocation); let match = defaultMatcher.matchesAny(wndLocation, typeMap, parentDocDomain, false, sitekey); if (match instanceof WhitelistFilter) { let whitelistType = (match.contentType & RegExpFilter.typeMap.DOCUMENT) ? "DOCUMENT" : "ELEMHIDE"; return [i, whitelistType, parentDocDomain, false, wndLocation, match]; } if (!nogenericHit) { match = defaultMatcher.matchesAny(wndLocation, RegExpFilter.typeMap[genericType], parentDocDomain, false, sitekey); if (match instanceof WhitelistFilter) nogenericHit = [i, genericType, parentDocDomain, false, wndLocation, match]; } if (frame == sitekeyFrame) [sitekey, sitekeyFrame] = getSitekey(frames.slice(i + 1)); } return nogenericHit; }, /** * Deletes nodes that were previously stored with a * RequestNotifier.storeNodesForEntries() call or similar. * @param {string} id unique ID of the nodes */ deleteNodes: function(id) { port.emit("deleteNodes", id); }, /** * 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) { port.emit("refilterNodes", { nodesID: id, entry: entry }); } }; Policy.init(); /** * Extracts the hostname from a URL (might return null). */ function getHostname(/**String*/ url) /**String*/ { try { return Utils.unwrapURL(url).host; } catch(e) { return null; } } /** * Retrieves and validates the sitekey for a frame structure. */ function getSitekey(frames) { for (let frame of frames) { if (frame.sitekey && frame.sitekey.indexOf("_") >= 0) { 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]; } } return [null, null]; } /** * Retrieves the location of a window. * @param wnd {nsIDOMWindow} * @return {String} window location or null on failure */ function getWindowLocation(wnd) { if ("name" in wnd && wnd.name == "messagepane") { // Thunderbird branch try { let mailWnd = wnd.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) { return 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) return 'mailto:' + emailAddress.replace(/^[\s"]+/, "").replace(/[\s"]+$/, "").replace(/\s/g, '%20'); } } catch(e) {} } // Firefox branch return wnd.location.href; } /** * Checks whether the location's origin is different from document's origin. */ function isThirdParty(/**String*/location, /**String*/ docDomain) /**Boolean*/ { if (!location || !docDomain) return true; let uri = Utils.makeURI(location); try { return Utils.effectiveTLD.getBaseDomain(uri) != Utils.effectiveTLD.getBaseDomainFromHost(docDomain); } catch (e) { // EffectiveTLDService throws on IP addresses, just compare the host name let host = ""; try { host = uri.host; } catch (e) {} return host != docDomain; } }