OLD | NEW |
1 /* | 1 /* |
2 * This file is part of Adblock Plus <https://adblockplus.org/>, | 2 * This file is part of Adblock Plus <https://adblockplus.org/>, |
3 * Copyright (C) 2006-2016 Eyeo GmbH | 3 * Copyright (C) 2006-2016 Eyeo GmbH |
4 * | 4 * |
5 * Adblock Plus is free software: you can redistribute it and/or modify | 5 * Adblock Plus is free software: you can redistribute it and/or modify |
6 * it under the terms of the GNU General Public License version 3 as | 6 * it under the terms of the GNU General Public License version 3 as |
7 * published by the Free Software Foundation. | 7 * published by the Free Software Foundation. |
8 * | 8 * |
9 * Adblock Plus is distributed in the hope that it will be useful, | 9 * Adblock Plus is distributed in the hope that it will be useful, |
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of | 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of |
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
12 * GNU General Public License for more details. | 12 * GNU General Public License for more details. |
13 * | 13 * |
14 * You should have received a copy of the GNU General Public License | 14 * You should have received a copy of the GNU General Public License |
15 * along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>. | 15 * along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>. |
16 */ | 16 */ |
17 | 17 |
18 var MutationObserver = window.MutationObserver || window.WebKitMutationObserver; | 18 var MutationObserver = window.MutationObserver || window.WebKitMutationObserver; |
19 var SELECTOR_GROUP_SIZE = 200; | |
20 | 19 |
21 var typeMap = { | 20 var typeMap = { |
22 "img": "IMAGE", | 21 "img": "IMAGE", |
23 "input": "IMAGE", | 22 "input": "IMAGE", |
24 "picture": "IMAGE", | 23 "picture": "IMAGE", |
25 "audio": "MEDIA", | 24 "audio": "MEDIA", |
26 "video": "MEDIA", | 25 "video": "MEDIA", |
27 "frame": "SUBDOCUMENT", | 26 "frame": "SUBDOCUMENT", |
28 "iframe": "SUBDOCUMENT", | 27 "iframe": "SUBDOCUMENT", |
29 "object": "OBJECT", | 28 "object": "OBJECT", |
(...skipping 159 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
189 try | 188 try |
190 { | 189 { |
191 return element.contentDocument; | 190 return element.contentDocument; |
192 } | 191 } |
193 catch (e) | 192 catch (e) |
194 { | 193 { |
195 return null; | 194 return null; |
196 } | 195 } |
197 } | 196 } |
198 | 197 |
199 function ElementHidingTracer(document, selectors) | 198 function ElementHidingTracer(selectors) |
200 { | 199 { |
201 this.document = document; | |
202 this.selectors = selectors; | 200 this.selectors = selectors; |
203 | 201 |
204 this.changedNodes = []; | 202 this.changedNodes = []; |
205 this.timeout = null; | 203 this.timeout = null; |
206 | 204 |
207 this.observer = new MutationObserver(this.observe.bind(this)); | 205 this.observer = new MutationObserver(this.observe.bind(this)); |
208 this.trace = this.trace.bind(this); | 206 this.trace = this.trace.bind(this); |
209 | 207 |
210 if (document.readyState == "loading") | 208 if (document.readyState == "loading") |
211 document.addEventListener("DOMContentLoaded", this.trace); | 209 document.addEventListener("DOMContentLoaded", this.trace); |
(...skipping 45 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
257 this.checkNodes(this.changedNodes); | 255 this.checkNodes(this.changedNodes); |
258 this.changedNodes = []; | 256 this.changedNodes = []; |
259 this.timeout = null; | 257 this.timeout = null; |
260 }, | 258 }, |
261 | 259 |
262 observe: function(mutations) | 260 observe: function(mutations) |
263 { | 261 { |
264 // Forget previously changed nodes that are no longer in the DOM. | 262 // Forget previously changed nodes that are no longer in the DOM. |
265 for (var i = 0; i < this.changedNodes.length; i++) | 263 for (var i = 0; i < this.changedNodes.length; i++) |
266 { | 264 { |
267 if (!this.document.contains(this.changedNodes[i])) | 265 if (!document.contains(this.changedNodes[i])) |
268 this.changedNodes.splice(i--, 1); | 266 this.changedNodes.splice(i--, 1); |
269 } | 267 } |
270 | 268 |
271 for (var j = 0; j < mutations.length; j++) | 269 for (var j = 0; j < mutations.length; j++) |
272 { | 270 { |
273 var mutation = mutations[j]; | 271 var mutation = mutations[j]; |
274 var node = mutation.target; | 272 var node = mutation.target; |
275 | 273 |
276 // Ignore mutations of nodes that aren't in the DOM anymore. | 274 // Ignore mutations of nodes that aren't in the DOM anymore. |
277 if (!this.document.contains(node)) | 275 if (!document.contains(node)) |
278 continue; | 276 continue; |
279 | 277 |
280 // Since querySelectorAll() doesn't consider the root itself | 278 // Since querySelectorAll() doesn't consider the root itself |
281 // and since CSS selectors can also match siblings, we have | 279 // and since CSS selectors can also match siblings, we have |
282 // to consider the parent node for attribute mutations. | 280 // to consider the parent node for attribute mutations. |
283 if (mutation.type == "attributes") | 281 if (mutation.type == "attributes") |
284 node = node.parentNode; | 282 node = node.parentNode; |
285 | 283 |
286 var addNode = true; | 284 var addNode = true; |
287 for (var k = 0; k < this.changedNodes.length; k++) | 285 for (var k = 0; k < this.changedNodes.length; k++) |
(...skipping 22 matching lines...) Expand all Loading... |
310 | 308 |
311 // Check only nodes whose descendants have changed, and not more often | 309 // Check only nodes whose descendants have changed, and not more often |
312 // than once a second. Otherwise large pages with a lot of DOM mutations | 310 // than once a second. Otherwise large pages with a lot of DOM mutations |
313 // (like YouTube) freeze when the devtools panel is active. | 311 // (like YouTube) freeze when the devtools panel is active. |
314 if (this.timeout == null) | 312 if (this.timeout == null) |
315 this.timeout = setTimeout(this.onTimeout.bind(this), 1000); | 313 this.timeout = setTimeout(this.onTimeout.bind(this), 1000); |
316 }, | 314 }, |
317 | 315 |
318 trace: function() | 316 trace: function() |
319 { | 317 { |
320 this.checkNodes([this.document]); | 318 this.checkNodes([document]); |
321 | 319 |
322 this.observer.observe( | 320 this.observer.observe( |
323 this.document, | 321 document, |
324 { | 322 { |
325 childList: true, | 323 childList: true, |
326 attributes: true, | 324 attributes: true, |
327 subtree: true | 325 subtree: true |
328 } | 326 } |
329 ); | 327 ); |
330 }, | 328 }, |
331 | 329 |
332 disconnect: function() | 330 disconnect: function() |
333 { | 331 { |
334 this.document.removeEventListener("DOMContentLoaded", this.trace); | 332 document.removeEventListener("DOMContentLoaded", this.trace); |
335 this.observer.disconnect(); | 333 this.observer.disconnect(); |
336 clearTimeout(this.timeout); | 334 clearTimeout(this.timeout); |
337 } | 335 } |
338 }; | 336 }; |
339 | 337 |
340 function runInDocument(document, fn, arg) | 338 function runInDocument(fn, arg) |
341 { | 339 { |
342 var script = document.createElement("script"); | 340 var script = document.createElement("script"); |
343 script.type = "application/javascript"; | 341 script.type = "application/javascript"; |
344 script.async = false; | 342 script.async = false; |
345 script.textContent = "(" + fn + ")(" + JSON.stringify(arg) + ");"; | 343 script.textContent = "(" + fn + ")(" + JSON.stringify(arg) + ");"; |
346 document.documentElement.appendChild(script); | 344 document.documentElement.appendChild(script); |
347 document.documentElement.removeChild(script); | 345 document.documentElement.removeChild(script); |
348 } | 346 } |
349 | 347 |
350 // Neither Chrome[1] nor Safari allow us to intercept WebSockets, and therefore | 348 // Neither Chrome[1] nor Safari allow us to intercept WebSockets, and therefore |
351 // some ad networks are misusing them as a way to serve adverts and circumvent | 349 // some ad networks are misusing them as a way to serve adverts and circumvent |
352 // us. As a workaround we wrap WebSocket, preventing blocked WebSocket | 350 // us. As a workaround we wrap WebSocket, preventing blocked WebSocket |
353 // connections from being opened. | 351 // connections from being opened. |
354 // [1] - https://bugs.chromium.org/p/chromium/issues/detail?id=129353 | 352 // [1] - https://bugs.chromium.org/p/chromium/issues/detail?id=129353 |
355 function wrapWebSocket(document) | 353 function wrapWebSocket() |
356 { | 354 { |
357 if (typeof WebSocket == "undefined") | 355 if (typeof WebSocket == "undefined") |
358 return; | 356 return; |
359 | 357 |
360 var eventName = "abpws-" + Math.random().toString(36).substr(2); | 358 var eventName = "abpws-" + Math.random().toString(36).substr(2); |
361 | 359 |
362 document.addEventListener(eventName, function(event) | 360 document.addEventListener(eventName, function(event) |
363 { | 361 { |
364 ext.backgroundPage.sendMessage({ | 362 ext.backgroundPage.sendMessage({ |
365 type: "request.websocket", | 363 type: "request.websocket", |
366 url: event.detail.url | 364 url: event.detail.url |
367 }, function (block) | 365 }, function (block) |
368 { | 366 { |
369 document.dispatchEvent( | 367 document.dispatchEvent( |
370 new CustomEvent(eventName + "-" + event.detail.url, {detail: block}) | 368 new CustomEvent(eventName + "-" + event.detail.url, {detail: block}) |
371 ); | 369 ); |
372 }); | 370 }); |
373 }); | 371 }); |
374 | 372 |
375 runInDocument(document, function(eventName) | 373 runInDocument(function(eventName) |
376 { | 374 { |
377 // As far as possible we must track everything we use that could be | 375 // As far as possible we must track everything we use that could be |
378 // sabotaged by the website later in order to circumvent us. | 376 // sabotaged by the website later in order to circumvent us. |
379 var RealWebSocket = WebSocket; | 377 var RealWebSocket = WebSocket; |
380 var closeWebSocket = Function.prototype.call.bind(RealWebSocket.prototype.cl
ose); | 378 var closeWebSocket = Function.prototype.call.bind(RealWebSocket.prototype.cl
ose); |
381 var addEventListener = document.addEventListener.bind(document); | 379 var addEventListener = document.addEventListener.bind(document); |
382 var removeEventListener = document.removeEventListener.bind(document); | 380 var removeEventListener = document.removeEventListener.bind(document); |
383 var dispatchEvent = document.dispatchEvent.bind(document); | 381 var dispatchEvent = document.dispatchEvent.bind(document); |
384 var CustomEvent = window.CustomEvent; | 382 var CustomEvent = window.CustomEvent; |
385 | 383 |
(...skipping 39 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
425 OPEN: {value: RealWebSocket.OPEN, enumerable: true}, | 423 OPEN: {value: RealWebSocket.OPEN, enumerable: true}, |
426 CLOSING: {value: RealWebSocket.CLOSING, enumerable: true}, | 424 CLOSING: {value: RealWebSocket.CLOSING, enumerable: true}, |
427 CLOSED: {value: RealWebSocket.CLOSED, enumerable: true}, | 425 CLOSED: {value: RealWebSocket.CLOSED, enumerable: true}, |
428 prototype: {value: RealWebSocket.prototype} | 426 prototype: {value: RealWebSocket.prototype} |
429 }); | 427 }); |
430 | 428 |
431 RealWebSocket.prototype.constructor = WebSocket; | 429 RealWebSocket.prototype.constructor = WebSocket; |
432 }, eventName); | 430 }, eventName); |
433 } | 431 } |
434 | 432 |
435 function init(document) | 433 function ElemHide() |
436 { | 434 { |
437 var shadow = null; | 435 this.shadow = this.createShadowTree(); |
438 var style = null; | 436 this.style = null; |
439 var observer = null; | 437 this.tracer = null; |
440 var tracer = null; | |
441 | 438 |
442 wrapWebSocket(document); | 439 this.propertyFilters = new CSSPropertyFilters( |
| 440 window, |
| 441 function(callback) |
| 442 { |
| 443 ext.backgroundPage.sendMessage({ |
| 444 type: "filters.get", |
| 445 what: "cssproperties" |
| 446 }, callback); |
| 447 }, |
| 448 this.addSelectors.bind(this) |
| 449 ); |
| 450 } |
| 451 ElemHide.prototype = { |
| 452 selectorGroupSize: 200, |
443 | 453 |
444 function getPropertyFilters(callback) | 454 createShadowTree: function() |
445 { | 455 { |
446 ext.backgroundPage.sendMessage({ | 456 // Use Shadow DOM if available to don't mess with web pages that rely |
447 type: "filters.get", | 457 // on the order of their own <style> tags (#309). However, creating a |
448 what: "cssproperties" | 458 // shadow root breaks running CSS transitions. So we have to create |
449 }, callback); | 459 // the shadow root before transistions might start (#452). |
450 } | 460 if (!("createShadowRoot" in document.documentElement)) |
451 var propertyFilters = new CSSPropertyFilters(window, getPropertyFilters, | 461 return null; |
452 addElemHideSelectors); | |
453 | 462 |
454 // Use Shadow DOM if available to don't mess with web pages that rely on | 463 // Using shadow DOM causes issues on some Google websites, |
455 // the order of their own <style> tags (#309). | 464 // including Google Docs, Gmail and Blogger (#1770, #2602, #2687). |
456 // | 465 if (/\.(?:google|blogger)\.com$/.test(document.domain)) |
457 // However, creating a shadow root breaks running CSS transitions. So we | 466 return null; |
458 // have to create the shadow root before transistions might start (#452). | 467 |
459 // | 468 var shadow = document.documentElement.createShadowRoot(); |
460 // Also, using shadow DOM causes issues on some Google websites, | |
461 // including Google Docs, Gmail and Blogger (#1770, #2602, #2687). | |
462 if ("createShadowRoot" in document.documentElement && | |
463 !/\.(?:google|blogger)\.com$/.test(document.domain)) | |
464 { | |
465 shadow = document.documentElement.createShadowRoot(); | |
466 shadow.appendChild(document.createElement("shadow")); | 469 shadow.appendChild(document.createElement("shadow")); |
467 | 470 |
468 // Stop the website from messing with our shadowRoot | 471 // Stop the website from messing with our shadow root (#4191, #4298). |
469 if ("shadowRoot" in Element.prototype) | 472 if ("shadowRoot" in Element.prototype) |
470 { | 473 { |
471 runInDocument(document, function() | 474 runInDocument(function() |
472 { | 475 { |
473 var ourShadowRoot = document.documentElement.shadowRoot; | 476 var ourShadowRoot = document.documentElement.shadowRoot; |
474 var desc = Object.getOwnPropertyDescriptor(Element.prototype, "shadowRoo
t"); | 477 var desc = Object.getOwnPropertyDescriptor(Element.prototype, "shadowRoo
t"); |
475 var shadowRoot = Function.prototype.call.bind(desc.get); | 478 var shadowRoot = Function.prototype.call.bind(desc.get); |
476 | 479 |
477 Object.defineProperty(Element.prototype, "shadowRoot", { | 480 Object.defineProperty(Element.prototype, "shadowRoot", { |
478 configurable: true, enumerable: true, get: function() | 481 configurable: true, enumerable: true, get: function() |
479 { | 482 { |
480 var shadow = shadowRoot(this); | 483 var shadow = shadowRoot(this); |
481 return shadow == ourShadowRoot ? null : shadow; | 484 return shadow == ourShadowRoot ? null : shadow; |
482 } | 485 } |
483 }); | 486 }); |
484 }, null); | 487 }, null); |
485 } | 488 } |
486 } | |
487 | 489 |
488 function addElemHideSelectors(selectors) | 490 return shadow; |
| 491 }, |
| 492 |
| 493 addSelectors: function(selectors) |
489 { | 494 { |
490 if (selectors.length == 0) | 495 if (selectors.length == 0) |
491 return; | 496 return; |
492 | 497 |
493 if (!style) | 498 if (!this.style) |
494 { | 499 { |
495 // Create <style> element lazily, only if we add styles. Add it to | 500 // Create <style> element lazily, only if we add styles. Add it to |
496 // the shadow DOM if possible. Otherwise fallback to the <head> or | 501 // the shadow DOM if possible. Otherwise fallback to the <head> or |
497 // <html> element. If we have injected a style element before that | 502 // <html> element. If we have injected a style element before that |
498 // has been removed (the sheet property is null), create a new one. | 503 // has been removed (the sheet property is null), create a new one. |
499 style = document.createElement("style"); | 504 this.style = document.createElement("style"); |
500 (shadow || document.head || document.documentElement).appendChild(style); | 505 (this.shadow || document.head |
| 506 || document.documentElement).appendChild(this.style); |
501 | 507 |
502 // It can happen that the frame already navigated to a different | 508 // It can happen that the frame already navigated to a different |
503 // document while we were waiting for the background page to respond. | 509 // document while we were waiting for the background page to respond. |
504 // In that case the sheet property will stay null, after addind the | 510 // In that case the sheet property will stay null, after addind the |
505 // <style> element to the shadow DOM. | 511 // <style> element to the shadow DOM. |
506 if (!style.sheet) | 512 if (!this.style.sheet) |
507 return; | 513 return; |
508 } | 514 } |
509 | 515 |
510 // If using shadow DOM, we have to add the ::content pseudo-element | 516 // If using shadow DOM, we have to add the ::content pseudo-element |
511 // before each selector, in order to match elements within the | 517 // before each selector, in order to match elements within the |
512 // insertion point. | 518 // insertion point. |
513 if (shadow) | 519 if (this.shadow) |
514 { | 520 { |
515 var preparedSelectors = []; | 521 var preparedSelectors = []; |
516 for (var i = 0; i < selectors.length; i++) | 522 for (var i = 0; i < selectors.length; i++) |
517 { | 523 { |
518 var subSelectors = splitSelector(selectors[i]); | 524 var subSelectors = splitSelector(selectors[i]); |
519 for (var j = 0; j < subSelectors.length; j++) | 525 for (var j = 0; j < subSelectors.length; j++) |
520 preparedSelectors.push("::content " + subSelectors[j]); | 526 preparedSelectors.push("::content " + subSelectors[j]); |
521 } | 527 } |
522 selectors = preparedSelectors; | 528 selectors = preparedSelectors; |
523 } | 529 } |
524 | 530 |
525 // Safari only allows 8192 primitive selectors to be injected at once[1], we | 531 // Safari only allows 8192 primitive selectors to be injected at once[1], we |
526 // therefore chunk the inserted selectors into groups of 200 to be safe. | 532 // therefore chunk the inserted selectors into groups of 200 to be safe. |
527 // (Chrome also has a limit, larger... but we're not certain exactly what it | 533 // (Chrome also has a limit, larger... but we're not certain exactly what it |
528 // is! Edge apparently has no such limit.) | 534 // is! Edge apparently has no such limit.) |
529 // [1] - https://github.com/WebKit/webkit/blob/1cb2227f6b2a1035f7bdc46e5ab69
debb75fc1de/Source/WebCore/css/RuleSet.h#L68 | 535 // [1] - https://github.com/WebKit/webkit/blob/1cb2227f6b2a1035f7bdc46e5ab69
debb75fc1de/Source/WebCore/css/RuleSet.h#L68 |
530 for (var i = 0; i < selectors.length; i += SELECTOR_GROUP_SIZE) | 536 for (var i = 0; i < selectors.length; i += this.selectorGroupSize) |
531 { | 537 { |
532 var selector = selectors.slice(i, i + SELECTOR_GROUP_SIZE).join(", "); | 538 var selector = selectors.slice(i, i + this.selectorGroupSize).join(", "); |
533 style.sheet.addRule(selector, "display: none !important;"); | 539 this.style.sheet.addRule(selector, "display: none !important;"); |
534 } | 540 } |
535 }; | 541 }, |
536 | 542 |
537 var updateStylesheet = function() | 543 apply: function() |
538 { | 544 { |
539 var selectors = null; | 545 var selectors = null; |
540 var CSSPropertyFiltersLoaded = false; | 546 var propertyFiltersLoaded = false; |
541 | 547 |
542 var checkLoaded = function() | 548 var checkLoaded = function() |
543 { | 549 { |
544 if (!selectors || !CSSPropertyFiltersLoaded) | 550 if (!selectors || !propertyFiltersLoaded) |
545 return; | 551 return; |
546 | 552 |
547 if (observer) | 553 if (this.tracer) |
548 observer.disconnect(); | 554 this.tracer.disconnect(); |
549 observer = null; | 555 this.tracer = null; |
550 | 556 |
551 if (tracer) | 557 if (this.style && this.style.parentElement) |
552 tracer.disconnect(); | 558 this.style.parentElement.removeChild(this.style); |
553 tracer = null; | 559 this.style = null; |
554 | 560 |
555 if (style && style.parentElement) | 561 this.addSelectors(selectors.selectors); |
556 style.parentElement.removeChild(style); | 562 this.propertyFilters.apply(); |
557 style = null; | |
558 | |
559 addElemHideSelectors(selectors.selectors); | |
560 propertyFilters.apply(); | |
561 | 563 |
562 if (selectors.trace) | 564 if (selectors.trace) |
563 tracer = new ElementHidingTracer(document, selectors.selectors); | 565 this.tracer = new ElementHidingTracer(selectors.selectors); |
564 }; | 566 }.bind(this); |
565 | 567 |
566 ext.backgroundPage.sendMessage({type: "get-selectors"}, function(response) | 568 ext.backgroundPage.sendMessage({type: "get-selectors"}, function(response) |
567 { | 569 { |
568 selectors = response; | 570 selectors = response; |
569 checkLoaded(); | 571 checkLoaded(); |
570 }); | 572 }); |
571 | 573 |
572 propertyFilters.load(function() | 574 this.propertyFilters.load(function() |
573 { | 575 { |
574 CSSPropertyFiltersLoaded = true; | 576 propertyFiltersLoaded = true; |
575 checkLoaded(); | 577 checkLoaded(); |
576 }); | 578 }); |
577 }; | 579 } |
| 580 }; |
578 | 581 |
579 updateStylesheet(); | 582 if (document instanceof HTMLDocument) |
| 583 { |
| 584 checkSitekey(); |
| 585 wrapWebSocket(); |
| 586 |
| 587 var elemhide = new ElemHide(); |
| 588 elemhide.apply(); |
580 | 589 |
581 document.addEventListener("error", function(event) | 590 document.addEventListener("error", function(event) |
582 { | 591 { |
583 checkCollapse(event.target); | 592 checkCollapse(event.target); |
584 }, true); | 593 }, true); |
585 | 594 |
586 document.addEventListener("load", function(event) | 595 document.addEventListener("load", function(event) |
587 { | 596 { |
588 var element = event.target; | 597 var element = event.target; |
589 | |
590 if (/^i?frame$/.test(element.localName)) | 598 if (/^i?frame$/.test(element.localName)) |
591 checkCollapse(element); | 599 checkCollapse(element); |
592 | |
593 if (/\bChrome\//.test(navigator.userAgent)) | |
594 { | |
595 var contentDocument = getContentDocument(element); | |
596 if (contentDocument) | |
597 { | |
598 var contentWindow = contentDocument.defaultView; | |
599 if (contentDocument instanceof contentWindow.HTMLDocument) | |
600 { | |
601 // Prior to Chrome 37, content scripts cannot run in | |
602 // dynamically created frames. Also on Chrome 37-40 | |
603 // document_start content scripts (like this one) don't | |
604 // run either in those frames due to https://crbug.com/416907. | |
605 // So we have to apply element hiding from the parent frame. | |
606 if (!("init" in contentWindow)) | |
607 init(contentDocument); | |
608 } | |
609 } | |
610 } | |
611 }, true); | 600 }, true); |
612 | |
613 return updateStylesheet; | |
614 } | 601 } |
615 | |
616 if (document instanceof HTMLDocument) | |
617 { | |
618 checkSitekey(); | |
619 window.updateStylesheet = init(document); | |
620 } | |
OLD | NEW |