Index: include.preload.js |
=================================================================== |
--- a/include.preload.js |
+++ b/include.preload.js |
@@ -1,6 +1,6 @@ |
/* |
* This file is part of Adblock Plus <https://adblockplus.org/>, |
- * Copyright (C) 2006-2016 Eyeo GmbH |
+ * 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 |
@@ -15,19 +15,24 @@ |
* along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>. |
*/ |
+/* globals ElemHideEmulation, splitSelector */ |
+ |
"use strict"; |
-const typeMap = { |
- "img": "IMAGE", |
- "input": "IMAGE", |
- "picture": "IMAGE", |
- "audio": "MEDIA", |
- "video": "MEDIA", |
- "frame": "SUBDOCUMENT", |
- "iframe": "SUBDOCUMENT", |
- "object": "OBJECT", |
- "embed": "OBJECT" |
-}; |
+// This variable is also used by our other content scripts. |
+let elemhide; |
+ |
+const typeMap = new Map([ |
+ ["img", "IMAGE"], |
+ ["input", "IMAGE"], |
+ ["picture", "IMAGE"], |
+ ["audio", "MEDIA"], |
+ ["video", "MEDIA"], |
+ ["frame", "SUBDOCUMENT"], |
+ ["iframe", "SUBDOCUMENT"], |
+ ["object", "OBJECT"], |
+ ["embed", "OBJECT"] |
+]); |
function getURLsFromObjectElement(element) |
{ |
@@ -41,9 +46,9 @@ |
continue; |
let name = child.getAttribute("name"); |
- if (name != "movie" && // Adobe Flash |
+ if (name != "movie" && // Adobe Flash |
name != "source" && // Silverlight |
- name != "src" && // Real Media + Quicktime |
+ name != "src" && // Real Media + Quicktime |
name != "FileName") // Windows Media |
continue; |
@@ -84,7 +89,7 @@ |
for (let child of element.children) |
{ |
if (child.localName == "source" || child.localName == "track") |
- urls.push.apply(urls, getURLsFromAttributes(child)); |
+ urls.push(...getURLsFromAttributes(child)); |
} |
if (element.poster) |
@@ -124,7 +129,7 @@ |
function checkCollapse(element) |
{ |
- let mediatype = typeMap[element.localName]; |
+ let mediatype = typeMap.get(element.localName); |
if (!mediatype) |
return; |
@@ -135,8 +140,8 @@ |
ext.backgroundPage.sendMessage( |
{ |
type: "filters.collapse", |
- urls: urls, |
- mediatype: mediatype, |
+ urls, |
+ mediatype, |
baseURL: document.location.href |
}, |
@@ -179,25 +184,11 @@ |
ext.backgroundPage.sendMessage({type: "filters.addKey", token: attr}); |
} |
-function getContentDocument(element) |
+function ElementHidingTracer() |
{ |
- try |
- { |
- return element.contentDocument; |
- } |
- catch (e) |
- { |
- return null; |
- } |
-} |
- |
-function ElementHidingTracer(selectors) |
-{ |
- this.selectors = selectors; |
- |
+ this.selectors = []; |
this.changedNodes = []; |
this.timeout = null; |
- |
this.observer = new MutationObserver(this.observe.bind(this)); |
this.trace = this.trace.bind(this); |
@@ -207,46 +198,60 @@ |
this.trace(); |
} |
ElementHidingTracer.prototype = { |
- checkNodes(nodes) |
+ addSelectors(selectors, filters) |
{ |
- let matchedSelectors = []; |
+ let pairs = selectors.map((sel, i) => [sel, filters && filters[i]]); |
- // Find all selectors that match any hidden element inside the given nodes. |
- for (let selector of this.selectors) |
+ if (document.readyState != "loading") |
+ this.checkNodes([document], pairs); |
+ |
+ this.selectors.push(...pairs); |
+ }, |
+ |
+ checkNodes(nodes, pairs) |
+ { |
+ let selectors = []; |
+ let filters = []; |
+ |
+ for (let [selector, filter] of pairs) |
{ |
- for (let node of nodes) |
+ nodes: for (let node of nodes) |
{ |
- let elements = node.querySelectorAll(selector); |
- let matched = false; |
- |
- for (let element of elements) |
+ for (let element of node.querySelectorAll(selector)) |
{ |
// Only consider selectors that actually have an effect on the |
// computed styles, and aren't overridden by rules with higher |
// priority, or haven't been circumvented in a different way. |
if (getComputedStyle(element).display == "none") |
{ |
- matchedSelectors.push(selector); |
- matched = true; |
- break; |
+ // For regular element hiding, we don't know the exact filter, |
+ // but the background page can find it with the given selector. |
+ // In case of element hiding emulation, the generated selector |
+ // we got here is different from the selector part of the filter, |
+ // but in this case we can send the whole filter text instead. |
+ if (filter) |
+ filters.push(filter); |
+ else |
+ selectors.push(selector); |
+ |
+ break nodes; |
} |
} |
- |
- if (matched) |
- break; |
} |
} |
- if (matchedSelectors.length > 0) |
+ if (selectors.length > 0 || filters.length > 0) |
+ { |
ext.backgroundPage.sendMessage({ |
type: "devtools.traceElemHide", |
- selectors: matchedSelectors |
+ selectors, filters |
}); |
+ } |
}, |
onTimeout() |
{ |
- this.checkNodes(this.changedNodes); |
+ this.checkNodes(this.changedNodes, this.selectors); |
this.changedNodes = []; |
this.timeout = null; |
}, |
@@ -308,7 +313,7 @@ |
trace() |
{ |
- this.checkNodes([document]); |
+ this.checkNodes([document], this.selectors); |
this.observer.observe( |
document, |
@@ -328,98 +333,6 @@ |
} |
}; |
-function runInPageContext(fn, arg) |
-{ |
- let script = document.createElement("script"); |
- script.type = "application/javascript"; |
- script.async = false; |
- script.textContent = "(" + fn + ")(" + JSON.stringify(arg) + ");"; |
- document.documentElement.appendChild(script); |
- document.documentElement.removeChild(script); |
-} |
- |
-// Chrome doesn't allow us to intercept WebSockets[1], and therefore |
-// some ad networks are misusing them as a way to serve adverts and circumvent |
-// us. As a workaround we wrap WebSocket, preventing blocked WebSocket |
-// connections from being opened. |
-// [1] - https://bugs.chromium.org/p/chromium/issues/detail?id=129353 |
-function wrapWebSocket() |
-{ |
- let eventName = "abpws-" + Math.random().toString(36).substr(2); |
- |
- document.addEventListener(eventName, event => |
- { |
- ext.backgroundPage.sendMessage({ |
- type: "request.websocket", |
- url: event.detail.url |
- }, block => |
- { |
- document.dispatchEvent( |
- new CustomEvent(eventName + "-" + event.detail.url, {detail: block}) |
- ); |
- }); |
- }); |
- |
- runInPageContext(eventName => |
- { |
- // As far as possible we must track everything we use that could be |
- // sabotaged by the website later in order to circumvent us. |
- let RealWebSocket = WebSocket; |
- let closeWebSocket = Function.prototype.call.bind(RealWebSocket.prototype.close); |
- let addEventListener = document.addEventListener.bind(document); |
- let removeEventListener = document.removeEventListener.bind(document); |
- let dispatchEvent = document.dispatchEvent.bind(document); |
- let CustomEvent = window.CustomEvent; |
- |
- function checkRequest(url, callback) |
- { |
- let incomingEventName = eventName + "-" + url; |
- function listener(event) |
- { |
- callback(event.detail); |
- removeEventListener(incomingEventName, listener); |
- } |
- addEventListener(incomingEventName, listener); |
- |
- dispatchEvent(new CustomEvent(eventName, { |
- detail: {url: url} |
- })); |
- } |
- |
- function WrappedWebSocket(url) |
- { |
- // Throw correct exceptions if the constructor is used improperly. |
- if (!(this instanceof WrappedWebSocket)) return RealWebSocket(); |
- if (arguments.length < 1) return new RealWebSocket(); |
- |
- let websocket; |
- if (arguments.length == 1) |
- websocket = new RealWebSocket(url); |
- else |
- websocket = new RealWebSocket(url, arguments[1]); |
- |
- checkRequest(websocket.url, blocked => |
- { |
- if (blocked) |
- closeWebSocket(websocket); |
- }); |
- |
- return websocket; |
- } |
- WrappedWebSocket.prototype = RealWebSocket.prototype; |
- WebSocket = WrappedWebSocket.bind(); |
- Object.defineProperties(WebSocket, { |
- CONNECTING: {value: RealWebSocket.CONNECTING, enumerable: true}, |
- OPEN: {value: RealWebSocket.OPEN, enumerable: true}, |
- CLOSING: {value: RealWebSocket.CLOSING, enumerable: true}, |
- CLOSED: {value: RealWebSocket.CLOSED, enumerable: true}, |
- prototype: {value: RealWebSocket.prototype} |
- }); |
- |
- RealWebSocket.prototype.constructor = WebSocket; |
- }, eventName); |
-} |
- |
function ElemHide() |
{ |
this.shadow = this.createShadowTree(); |
@@ -462,31 +375,10 @@ |
document.documentElement.createShadowRoot(); |
shadow.appendChild(document.createElement("shadow")); |
- // Stop the website from messing with our shadow root (#4191, #4298). |
- if ("shadowRoot" in Element.prototype) |
- { |
- runInPageContext(() => |
- { |
- let ourShadowRoot = document.documentElement.shadowRoot; |
- if (!ourShadowRoot) |
- return; |
- let desc = Object.getOwnPropertyDescriptor(Element.prototype, "shadowRoot"); |
- let shadowRoot = Function.prototype.call.bind(desc.get); |
- |
- Object.defineProperty(Element.prototype, "shadowRoot", { |
- configurable: true, enumerable: true, get() |
- { |
- let shadow = shadowRoot(this); |
- return shadow == ourShadowRoot ? null : shadow; |
- } |
- }); |
- }, null); |
- } |
- |
return shadow; |
}, |
- addSelectors(selectors) |
+ addSelectors(selectors, filters) |
{ |
if (selectors.length == 0) |
return; |
@@ -498,8 +390,8 @@ |
// <html> element. If we have injected a style element before that |
// has been removed (the sheet property is null), create a new one. |
this.style = document.createElement("style"); |
- (this.shadow || document.head |
- || document.documentElement).appendChild(this.style); |
+ (this.shadow || document.head || |
+ document.documentElement).appendChild(this.style); |
// It can happen that the frame already navigated to a different |
// document while we were waiting for the background page to respond. |
@@ -512,16 +404,19 @@ |
// If using shadow DOM, we have to add the ::content pseudo-element |
// before each selector, in order to match elements within the |
// insertion point. |
+ let preparedSelectors = []; |
if (this.shadow) |
{ |
- let preparedSelectors = []; |
for (let selector of selectors) |
{ |
let subSelectors = splitSelector(selector); |
for (let subSelector of subSelectors) |
preparedSelectors.push("::content " + subSelector); |
} |
- selectors = preparedSelectors; |
+ } |
+ else |
+ { |
+ preparedSelectors = selectors; |
} |
// Safari only allows 8192 primitive selectors to be injected at once[1], we |
@@ -529,24 +424,23 @@ |
// (Chrome also has a limit, larger... but we're not certain exactly what it |
// is! Edge apparently has no such limit.) |
// [1] - https://github.com/WebKit/webkit/blob/1cb2227f6b2a1035f7bdc46e5ab69debb75fc1de/Source/WebCore/css/RuleSet.h#L68 |
- for (let i = 0; i < selectors.length; i += this.selectorGroupSize) |
+ for (let i = 0; i < preparedSelectors.length; i += this.selectorGroupSize) |
{ |
- let selector = selectors.slice(i, i + this.selectorGroupSize).join(", "); |
+ let selector = preparedSelectors.slice( |
+ i, i + this.selectorGroupSize |
+ ).join(", "); |
this.style.sheet.insertRule(selector + "{display: none !important;}", |
this.style.sheet.cssRules.length); |
} |
+ |
+ if (this.tracer) |
+ this.tracer.addSelectors(selectors, filters); |
}, |
apply() |
{ |
- let selectors = null; |
- let elemHideEmulationLoaded = false; |
- |
- let checkLoaded = function() |
+ ext.backgroundPage.sendMessage({type: "get-selectors"}, response => |
{ |
- if (!selectors || !elemHideEmulationLoaded) |
- return; |
- |
if (this.tracer) |
this.tracer.disconnect(); |
this.tracer = null; |
@@ -555,23 +449,11 @@ |
this.style.parentElement.removeChild(this.style); |
this.style = null; |
- this.addSelectors(selectors.selectors); |
+ if (response.trace) |
+ this.tracer = new ElementHidingTracer(); |
+ |
+ this.addSelectors(response.selectors); |
this.elemHideEmulation.apply(); |
- |
- if (selectors.trace) |
- this.tracer = new ElementHidingTracer(selectors.selectors); |
- }.bind(this); |
- |
- ext.backgroundPage.sendMessage({type: "get-selectors"}, response => |
- { |
- selectors = response; |
- checkLoaded(); |
- }); |
- |
- this.elemHideEmulation.load(() => |
- { |
- elemHideEmulationLoaded = true; |
- checkLoaded(); |
}); |
} |
}; |
@@ -579,11 +461,8 @@ |
if (document instanceof HTMLDocument) |
{ |
checkSitekey(); |
- wrapWebSocket(); |
- // This variable is also used by our other content scripts, outside of the |
- // current scope. |
- var elemhide = new ElemHide(); |
+ elemhide = new ElemHide(); |
elemhide.apply(); |
document.addEventListener("error", event => |