From 6e7918b6ccb69876d339a320091fdee811445395 Mon Sep 17 00:00:00 2001 From: Ruben Rodriguez Date: Mon, 20 Oct 2014 02:24:51 +0200 Subject: Generalize data directory --- .../spyblock@gnu.org/lib/contentPolicy.js | 779 +++++++++++++++++++++ 1 file changed, 779 insertions(+) create mode 100644 data/extensions/spyblock@gnu.org/lib/contentPolicy.js (limited to 'data/extensions/spyblock@gnu.org/lib/contentPolicy.js') diff --git a/data/extensions/spyblock@gnu.org/lib/contentPolicy.js b/data/extensions/spyblock@gnu.org/lib/contentPolicy.js new file mode 100644 index 0000000..084ebc5 --- /dev/null +++ b/data/extensions/spyblock@gnu.org/lib/contentPolicy.js @@ -0,0 +1,779 @@ +/* + * This file is part of Adblock Plus , + * Copyright (C) 2006-2014 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. + */ + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +let {TimeLine} = require("timeline"); +let {Utils} = require("utils"); +let {Prefs} = require("prefs"); +let {FilterStorage} = require("filterStorage"); +let {BlockingFilter, WhitelistFilter} = require("filterClasses"); +let {defaultMatcher} = require("matcher"); +let {objectMouseEventHander} = require("objectTabs"); +let {RequestNotifier} = require("requestNotifier"); +let {ElemHide} = require("elemHide"); + +/** + * List of explicitly supported content types + * @type Array of String + */ +let contentTypes = ["OTHER", "SCRIPT", "IMAGE", "STYLESHEET", "OBJECT", "SUBDOCUMENT", "DOCUMENT", "XMLHTTPREQUEST", "OBJECT_SUBREQUEST", "FONT", "MEDIA"]; + +/** + * List of content types that aren't associated with a visual document area + * @type Array of String + */ +let nonVisualTypes = ["SCRIPT", "STYLESHEET", "XMLHTTPREQUEST", "OBJECT_SUBREQUEST", "FONT"]; + +/** + * Randomly generated class name, to be applied to collapsed nodes. + */ +let collapsedClass = ""; + +/** + * Public policy checking functions and auxiliary objects + * @class + */ +let Policy = exports.Policy = +{ + /** + * Map of content type identifiers by their name. + * @type Object + */ + type: {}, + + /** + * Map of content type names by their identifiers (reverse of type map). + * @type Object + */ + typeDescr: {}, + + /** + * Map of localized content type names by their identifiers. + * @type Object + */ + localizedDescr: {}, + + /** + * Lists the non-visual content types. + * @type Object + */ + nonVisual: {}, + + /** + * Map containing all schemes that should be ignored by content policy. + * @type Object + */ + whitelistSchemes: {}, + + /** + * Called on module startup, initializes various exported properties. + */ + init: function() + { + TimeLine.enter("Entered content policy initialization"); + + // type constant by type description and type description by type constant + let iface = Ci.nsIContentPolicy; + for (let typeName of contentTypes) + { + if ("TYPE_" + typeName in iface) + { + let id = iface["TYPE_" + typeName]; + this.type[typeName] = id; + this.typeDescr[id] = typeName; + this.localizedDescr[id] = Utils.getString("type_label_" + typeName.toLowerCase()); + } + } + + this.type.ELEMHIDE = 0xFFFD; + this.typeDescr[0xFFFD] = "ELEMHIDE"; + this.localizedDescr[0xFFFD] = Utils.getString("type_label_elemhide"); + + this.type.POPUP = 0xFFFE; + this.typeDescr[0xFFFE] = "POPUP"; + this.localizedDescr[0xFFFE] = Utils.getString("type_label_popup"); + + for (let type of nonVisualTypes) + this.nonVisual[this.type[type]] = true; + + // whitelisted URL schemes + for (let scheme of Prefs.whitelistschemes.toLowerCase().split(" ")) + this.whitelistSchemes[scheme] = true; + + TimeLine.log("done initializing types"); + + // Generate class identifier used to collapse node and register corresponding + // stylesheet. + TimeLine.log("registering global stylesheet"); + + let offset = "a".charCodeAt(0); + for (let i = 0; i < 20; i++) + collapsedClass += String.fromCharCode(offset + Math.random() * 26); + + 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(function() + { + Utils.styleService.unregisterSheet(collapseStyle, Ci.nsIStyleSheetService.USER_SHEET); + }) + TimeLine.log("done registering stylesheet"); + + TimeLine.leave("Done initializing content policy"); + }, + + /** + * Checks whether a node should be blocked, hides it if necessary + * @param wnd {nsIDOMWindow} + * @param node {nsIDOMElement} + * @param contentType {String} + * @param location {nsIURI} + * @param collapse {Boolean} true to force hiding of the node + * @return {Boolean} false if the node should be blocked + */ + processNode: function(wnd, node, contentType, location, collapse) + { + let topWnd = wnd.top; + if (!topWnd || !topWnd.location || !topWnd.location.href) + return true; + + let privatenode=false; + Components.utils.import("resource://gre/modules/PrivateBrowsingUtils.jsm"); + if (PrivateBrowsingUtils.isWindowPrivate(wnd)) + privatenode=true; + + let originWindow = Utils.getOriginWindow(wnd); + let wndLocation = originWindow.location.href; + let docDomain = getHostname(wndLocation); + let match = null; + if (!match && Prefs.enabled) + { + let testWnd = wnd; + let parentWndLocation = getWindowLocation(testWnd); + while (true) + { + let testWndLocation = parentWndLocation; + parentWndLocation = (testWnd == testWnd.parent ? testWndLocation : getWindowLocation(testWnd.parent)); + match = Policy.isWhitelisted(testWndLocation, parentWndLocation); + + if (!(match instanceof WhitelistFilter)) + { + let keydata = (testWnd.document && testWnd.document.documentElement ? testWnd.document.documentElement.getAttribute("data-adblockkey") : null); + if (keydata && keydata.indexOf("_") >= 0) + { + let [key, signature] = keydata.split("_", 2); + let keyMatch = defaultMatcher.matchesByKey(testWndLocation, key.replace(/=/g, ""), docDomain); + if (keyMatch && Utils.crypto) + { + // Website specifies a key that we know but is the signature valid? + let uri = Services.io.newURI(testWndLocation, null, null); + let params = [ + uri.path.replace(/#.*/, ""), // REQUEST_URI + uri.asciiHost, // HTTP_HOST + Utils.httpProtocol.userAgent // HTTP_USER_AGENT + ]; + if (Utils.verifySignature(key, signature, params.join("\0"))) + match = keyMatch; + } + } + } + + if (match instanceof WhitelistFilter) + { + FilterStorage.increaseHitCount(match, wnd); + RequestNotifier.addNodeData(testWnd.document, topWnd, Policy.type.DOCUMENT, getHostname(parentWndLocation), false, testWndLocation, match); + return true; + } + + if (testWnd.parent == testWnd) + break; + else + testWnd = testWnd.parent; + } + } + + // Data loaded by plugins should be attached to the document + if (contentType == Policy.type.OBJECT_SUBREQUEST && node instanceof Ci.nsIDOMElement) + node = node.ownerDocument; + + // Fix type for objects misrepresented as frames or images + if (contentType != Policy.type.OBJECT && (node instanceof Ci.nsIDOMHTMLObjectElement || node instanceof Ci.nsIDOMHTMLEmbedElement)) + contentType = Policy.type.OBJECT; + + let locationText = location.spec; + if (!match && contentType == Policy.type.ELEMHIDE) + { + let testWnd = wnd; + let parentWndLocation = getWindowLocation(testWnd); + while (true) + { + let testWndLocation = parentWndLocation; + parentWndLocation = (testWnd == testWnd.parent ? testWndLocation : getWindowLocation(testWnd.parent)); + let parentDocDomain = getHostname(parentWndLocation); + match = defaultMatcher.matchesAny(testWndLocation, "ELEMHIDE", parentDocDomain, false); + if (match instanceof WhitelistFilter) + { + FilterStorage.increaseHitCount(match, wnd); + RequestNotifier.addNodeData(testWnd.document, topWnd, contentType, parentDocDomain, false, testWndLocation, match); + return true; + } + + if (testWnd.parent == testWnd) + break; + else + testWnd = testWnd.parent; + } + + match = location; + locationText = match.text.replace(/^.*?#/, '#'); + location = locationText; + + if (!match.isActiveOnDomain(docDomain)) + return true; + + let exception = ElemHide.getException(match, docDomain); + if (exception) + { + FilterStorage.increaseHitCount(exception, wnd); + RequestNotifier.addNodeData(node, topWnd, contentType, docDomain, thirdParty, locationText, exception); + return true; + } + } + + let thirdParty = (contentType == Policy.type.ELEMHIDE ? false : isThirdParty(location, docDomain)); + + if (!match && Prefs.enabled) + { + match = defaultMatcher.matchesAny(locationText, Policy.typeDescr[contentType] || "", docDomain, thirdParty, privatenode); + if (match instanceof BlockingFilter && node.ownerDocument && !(contentType in Policy.nonVisual)) + { + let prefCollapse = (match.collapse != null ? match.collapse : !Prefs.fastcollapse); + if (collapse || prefCollapse) + schedulePostProcess(node); + } + + // Track mouse events for objects + if (!match && contentType == Policy.type.OBJECT && node.nodeType == Ci.nsIDOMNode.ELEMENT_NODE) + { + node.addEventListener("mouseover", objectMouseEventHander, true); + node.addEventListener("mouseout", objectMouseEventHander, true); + } + } + + // Store node data + RequestNotifier.addNodeData(node, topWnd, contentType, docDomain, thirdParty, locationText, match); + if (match) + FilterStorage.increaseHitCount(match, wnd); + + return !match || match instanceof WhitelistFilter; + }, + + /** + * Checks whether the location's scheme is blockable. + * @param location {nsIURI} + * @return {Boolean} + */ + isBlockableScheme: function(location) + { + return !(location.scheme in Policy.whitelistSchemes); + }, + + /** + * Checks whether a page is whitelisted. + * @param {String} url + * @param {String} [parentUrl] location of the parent page + * @return {Filter} filter that matched the URL or null if not whitelisted + */ + isWhitelisted: function(url, parentUrl) + { + if (!url) + return null; + + // Do not apply exception rules to schemes on our whitelistschemes list. + let match = /^([\w\-]+):/.exec(url); + if (match && match[1] in Policy.whitelistSchemes) + return null; + + if (!parentUrl) + parentUrl = url; + + // Ignore fragment identifier + let index = url.indexOf("#"); + if (index >= 0) + url = url.substring(0, index); + + let result = defaultMatcher.matchesAny(url, "DOCUMENT", getHostname(parentUrl), false); + return (result instanceof WhitelistFilter ? result : null); + }, + + /** + * Checks whether the page loaded in a window is whitelisted. + * @param wnd {nsIDOMWindow} + * @return {Filter} matching exception rule or null if not whitelisted + */ + isWindowWhitelisted: function(wnd) + { + return Policy.isWhitelisted(getWindowLocation(wnd)); + }, + + + /** + * Asynchronously re-checks filters for given nodes. + */ + refilterNodes: function(/**Node[]*/ nodes, /**RequestEntry*/ entry) + { + // Ignore nodes that have been blocked already + if (entry.filter && !(entry.filter instanceof WhitelistFilter)) + return; + + for (let node of nodes) + Utils.runAsync(refilterNode, this, node, entry); + } +}; +Policy.init(); + +/** + * Actual nsIContentPolicy and nsIChannelEventSink implementation + * @class + */ +let 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() + { + let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar); + try + { + registrar.registerFactory(this.classID, this.classDescription, this.contractID, this); + } + catch (e if e.result == Cr.NS_ERROR_FACTORY_EXISTS) + { + // See bug 924340 - it might be too early to init now, the old version + // we are replacing didn't finish removing itself yet. + Utils.runAsync(this.init.bind(this)); + return; + } + + let catMan = Utils.categoryManager; + for (let category of this.xpcom_categories) + catMan.addCategoryEntry(category, this.contractID, this.contractID, false, true); + + // http-on-opening-request is new in Gecko 18, http-on-modify-request can + // be used in earlier releases. + let httpTopic = "http-on-opening-request"; + if (Services.vc.compare(Utils.platformVersion, "18.0") < 0) + httpTopic = "http-on-modify-request"; + + Services.obs.addObserver(this, httpTopic, true); + Services.obs.addObserver(this, "content-document-global-created", true); + Services.obs.addObserver(this, "xpcom-category-entry-removed", true); + Services.obs.addObserver(this, "xpcom-category-cleared", true); + + onShutdown.add(function() + { + // Our category observers should be removed before changing category + // memberships, just in case. + Services.obs.removeObserver(this, httpTopic); + Services.obs.removeObserver(this, "content-document-global-created"); + Services.obs.removeObserver(this, "xpcom-category-entry-removed"); + Services.obs.removeObserver(this, "xpcom-category-cleared"); + + for (let category of this.xpcom_categories) + catMan.deleteCategoryEntry(category, this.contractID, false); + + // This needs to run asynchronously, see bug 753687 + Utils.runAsync(function() + { + registrar.unregisterFactory(this.classID, this); + }.bind(this)); + + this.previousRequest = null; + }.bind(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 == Policy.type.DOCUMENT) + return Ci.nsIContentPolicy.ACCEPT; + + // Ignore standalone objects + if (contentType == Policy.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; + + // Ignore whitelisted schemes + let location = Utils.unwrapURL(contentLocation); + if (!Policy.isBlockableScheme(location)) + return Ci.nsIContentPolicy.ACCEPT; + + // Interpret unknown types as "other" + if (!(contentType in Policy.typeDescr)) + contentType = Policy.type.OTHER; + + let result = Policy.processNode(wnd, node, contentType, location, false); + if (result) + { + // We didn't block this request so we will probably see it again in + // http-on-opening-request. Keep it so that we can associate it with the + // channel there - will be needed in case of redirect. + this.previousRequest = [location, contentType]; + } + return (result ? Ci.nsIContentPolicy.ACCEPT : Ci.nsIContentPolicy.REJECT_REQUEST); + }, + + shouldProcess: function(contentType, contentLocation, requestOrigin, insecNode, mimeType, extra) + { + return Ci.nsIContentPolicy.ACCEPT; + }, + + // + // nsIObserver interface implementation + // + observe: function(subject, topic, data, additional) + { + switch (topic) + { + case "content-document-global-created": + { + if (!(subject instanceof Ci.nsIDOMWindow) || !subject.opener) + return; + + let uri = additional || Utils.makeURI(subject.location.href); + if (!Policy.processNode(subject.opener, subject.opener.document, Policy.type.POPUP, uri, false)) + { + subject.stop(); + Utils.runAsync(subject.close, subject); + } + else if (uri.spec == "about:blank") + { + // An about:blank pop-up most likely means that a load will be + // initiated synchronously. Set a flag for our "http-on-opening-request" + // handler. + this.expectingPopupLoad = true; + Utils.runAsync(function() + { + this.expectingPopupLoad = false; + }); + } + break; + } + case "http-on-opening-request": + case "http-on-modify-request": + { + if (!(subject instanceof Ci.nsIHttpChannel)) + return; + + if (this.previousRequest && subject.URI == this.previousRequest[0] && + subject instanceof Ci.nsIWritablePropertyBag) + { + // We just handled a content policy call for this request - associate + // the data with the channel so that we can find it in case of a redirect. + subject.setProperty("abpRequestType", this.previousRequest[1]); + this.previousRequest = null; + } + + if (this.expectingPopupLoad) + { + let wnd = Utils.getRequestWindow(subject); + if (wnd && wnd.opener && wnd.location.href == "about:blank") + { + this.observe(wnd, "content-document-global-created", null, subject.URI); + if (subject instanceof Ci.nsIWritablePropertyBag) + subject.setProperty("abpRequestType", Policy.type.POPUP); + } + } + + break; + } + case "xpcom-category-entry-removed": + case "xpcom-category-cleared": + { + let category = data; + if (this.xpcom_categories.indexOf(category) < 0) + return; + + if (topic == "xpcom-category-entry-removed" && + subject instanceof Ci.nsISupportsCString && + subject.data != this.contractID) + { + return; + } + + // Our category entry was removed, make sure to add it back + let catMan = Utils.categoryManager; + catMan.addCategoryEntry(category, this.contractID, this.contractID, false, true); + break; + } + } + }, + + // + // nsIChannelEventSink interface implementation + // + + asyncOnChannelRedirect: function(oldChannel, newChannel, flags, callback) + { + let result = Cr.NS_OK; + try + { + // Try to retrieve previously stored request data from the channel + let contentType; + if (oldChannel instanceof Ci.nsIWritablePropertyBag) + { + try + { + contentType = oldChannel.getProperty("abpRequestType"); + } + catch(e) + { + // No data attached, ignore this redirect + return; + } + } + + let newLocation = null; + try + { + newLocation = newChannel.URI; + } catch(e2) {} + if (!newLocation) + return; + + let wnd = Utils.getRequestWindow(newChannel); + if (!wnd) + return; + + if (contentType == Policy.type.SUBDOCUMENT && wnd.parent == wnd.top && wnd.opener) + { + // This is a window opened in a new tab miscategorized as frame load, + // see bug 467514. Get the frame as context to be at least consistent. + wnd = wnd.opener; + } + + if (contentType == Policy.type.POPUP && wnd.opener) + { + // Popups are initiated by their opener, not their own window. + wnd = wnd.opener; + } + + if (!Policy.processNode(wnd, wnd.document, contentType, newLocation, false)) + result = Cr.NS_BINDING_ABORTED; + } + catch (e) + { + // We shouldn't throw exceptions here - this will prevent the redirect. + Cu.reportError(e); + } + finally + { + callback.onRedirectVerifyCallback(result); + } + }, + + // + // 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 Array of 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() +{ + let nodes = scheduledNodes; + scheduledNodes = null; + + 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(collapsedClass); + } +} + +/** + * 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 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(/**nsIURI*/location, /**String*/ docDomain) /**Boolean*/ +{ + if (!location || !docDomain) + return true; + + try + { + return Utils.effectiveTLD.getBaseDomain(location) != Utils.effectiveTLD.getBaseDomainFromHost(docDomain); + } + catch (e) + { + // EffectiveTLDService throws on IP addresses, just compare the host name + let host = ""; + try + { + host = location.host; + } catch (e) {} + return host != docDomain; + } +} + +/** + * Re-checks filters on an element. + */ +function refilterNode(/**Node*/ node, /**RequestEntry*/ entry) +{ + let wnd = Utils.getWindow(node); + if (!wnd || wnd.closed) + return; + + if (entry.type == Policy.type.OBJECT) + { + node.removeEventListener("mouseover", objectMouseEventHander, true); + node.removeEventListener("mouseout", objectMouseEventHander, true); + } + Policy.processNode(wnd, node, entry.type, Utils.makeURI(entry.location), true); +} -- cgit v1.2.3