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