| 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 => |