| Left: | ||
| Right: | 
| LEFT | RIGHT | 
|---|---|
| 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; | 19 var SELECTOR_GROUP_SIZE = 200; | 
| 20 var id = Math.random().toString(36).substr(2); | |
| 20 | 21 | 
| 21 var typeMap = { | 22 var typeMap = { | 
| 22 "img": "IMAGE", | 23 "img": "IMAGE", | 
| 23 "input": "IMAGE", | 24 "input": "IMAGE", | 
| 24 "picture": "IMAGE", | 25 "picture": "IMAGE", | 
| 25 "audio": "MEDIA", | 26 "audio": "MEDIA", | 
| 26 "video": "MEDIA", | 27 "video": "MEDIA", | 
| 27 "frame": "SUBDOCUMENT", | 28 "frame": "SUBDOCUMENT", | 
| 28 "iframe": "SUBDOCUMENT", | 29 "iframe": "SUBDOCUMENT", | 
| 29 "object": "OBJECT", | 30 "object": "OBJECT", | 
| (...skipping 312 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 342 var observer = new MutationObserver(function() | 343 var observer = new MutationObserver(function() | 
| 343 { | 344 { | 
| 344 if (style.parentNode != parentNode) | 345 if (style.parentNode != parentNode) | 
| 345 parentNode.appendChild(style); | 346 parentNode.appendChild(style); | 
| 346 }); | 347 }); | 
| 347 | 348 | 
| 348 observer.observe(parentNode, {childList: true}); | 349 observer.observe(parentNode, {childList: true}); | 
| 349 return observer; | 350 return observer; | 
| 350 } | 351 } | 
| 351 | 352 | 
| 352 function protectStyleSheet(document, style, script) | 353 function runInPage(fn, arg) | 
| 353 { | 354 { | 
| 354 var id = Math.random().toString(36).substr(2); | 355 var script = document.createElement("script"); | 
| 356 script.type = "application/javascript"; | |
| 357 script.async = false; | |
| 358 script.textContent = "(" + fn + ")(" + JSON.stringify(arg) + ");"; | |
| 359 document.documentElement.appendChild(script); | |
| 360 document.documentElement.removeChild(script); | |
| 361 } | |
| 362 | |
| 363 function protectStyleSheet(document, style) | |
| 364 { | |
| 355 style.id = id; | 365 style.id = id; | 
| 356 | 366 | 
| 357 var code = [ | 367 runInPage(function(id) | 
| 358 "(function()", | 368 { | 
| 359 "{", | 369 var style = document.getElementById(id) || | 
| 360 ' var style = document.getElementById("' + id + '") ||', | 370 document.documentElement.shadowRoot.getElementById(id); | 
| 361 ' document.documentElement.shadowRoot.getElementById("' + id + '");', | 371 style.removeAttribute("id"); | 
| 362 ' style.removeAttribute("id");' | 372 | 
| 363 ]; | 373 var disableables = [style, style.sheet]; | 
| 364 | 374 for (var i = 0; i < disableables.length; i++) | 
| 365 var disableables = ["style", "style.sheet"]; | 375 Object.defineProperty(disableables[i], "disabled", | 
| 366 for (var i = 0; i < disableables.length; i++) | 376 {value: false, enumerable: true}); | 
| 367 { | 377 | 
| 368 code.push(" Object.defineProperty(" + disableables[i] + ', "disabled", ' | 378 ["deleteRule", "removeRule"].forEach(function(method) | 
| 369 + "{value: false, enumerable: true});") ; | 379 { | 
| 370 } | 380 var original = CSSStyleSheet.prototype[method]; | 
| 371 | 381 CSSStyleSheet.prototype[method] = function(index) | 
| 372 var methods = ["deleteRule", "removeRule"]; | 382 { | 
| 373 for (var j = 0; j < methods.length; j++) | 383 if (this != style.sheet) | 
| 374 { | 384 original.call(this, index); | 
| 375 var method = methods[j]; | 385 }; | 
| 376 if (method in CSSStyleSheet.prototype) | 386 }); | 
| 377 { | 387 }, id); | 
| 378 var origin = "CSSStyleSheet.prototype." + method; | |
| 379 code.push(" var " + method + " = " + origin + ";", | |
| 380 " " + origin + " = function(index)", | |
| 381 " {", | |
| 382 " if (this != style.sheet)", | |
| 383 " " + method + ".call(this, index);", | |
| 384 " }"); | |
| 385 } | |
| 386 } | |
| 387 | |
| 388 code.push("})();"); | |
| 389 | |
| 390 script.textContent += code.join("\n"); | |
| 391 } | 388 } | 
| 392 | 389 | 
| 393 // Neither Chrome[1] nor Safari allow us to intercept WebSockets, and therefore | 390 // Neither Chrome[1] nor Safari allow us to intercept WebSockets, and therefore | 
| 394 // some ad networks are misusing them as a way to serve adverts and circumvent | 391 // some ad networks are misusing them as a way to serve adverts and circumvent | 
| 395 // us. As a workaround we wrap WebSocket, closing connections that would have | 392 // us. As a workaround we wrap WebSocket, preventing blocked WebSocket | 
| 396 // otherwise been blocked. | 393 // connections from being opened. | 
| 397 // [1] - https://bugs.chromium.org/p/chromium/issues/detail?id=129353 | 394 // [1] - https://bugs.chromium.org/p/chromium/issues/detail?id=129353 | 
| 398 function wrapWebSocket(script) | 395 function wrapWebSocket() | 
| 399 { | 396 { | 
| 400 if (typeof WebSocket == "undefined") | 397 if (typeof WebSocket == "undefined") | 
| 401 return; | 398 return; | 
| 402 | 399 | 
| 403 var eventName = "abpws-" + Math.random().toString().substr(2); | 400 var eventName = "abpws-" + id; | 
| 
 
Sebastian Noack
2016/07/01 17:03:39
Instead of duplicating this logic, perhaps just as
 
kzar
2016/07/06 16:39:19
Done.
 
 | |
| 404 | 401 | 
| 405 document.addEventListener(eventName, function(event) | 402 document.addEventListener(eventName, function(event) | 
| 406 { | 403 { | 
| 407 ext.backgroundPage.sendMessage({ | 404 ext.backgroundPage.sendMessage({ | 
| 408 type: "websocket-request", | 405 type: "websocket-request", | 
| 409 url: event.detail.url | 406 url: event.detail.url | 
| 410 }, function (block) | 407 }, function (block) | 
| 411 { | 408 { | 
| 412 document.dispatchEvent( | 409 document.dispatchEvent( | 
| 413 new CustomEvent(eventName + "-" + event.detail.url, {detail: block}) | 410 new CustomEvent(eventName + "-" + event.detail.url, {detail: block}) | 
| 414 ); | 411 ); | 
| 415 }); | 412 }); | 
| 416 }); | 413 }); | 
| 417 | 414 | 
| 418 function wrapper(eventName) | 415 runInPage(function(eventName) | 
| 419 { | 416 { | 
| 420 var originalWebSocket = WebSocket; | 417 // As far as possible we must track everything we use that could be | 
| 421 var readyStates = { | 418 // sabotaged by the website later in order to circumvent us. | 
| 422 CLOSED: {value: 3, enumerable: true}, | 419 var RealWebSocket = WebSocket; | 
| 423 CLOSING: {value: 2, enumerable: true}, | 420 var closeWebSocket = Function.prototype.call.bind(RealWebSocket.prototype.cl ose); | 
| 424 OPEN: {value: 1, enumerable: true}, | 421 var addEventListener = document.addEventListener.bind(document); | 
| 425 CONNECTING: {value: 0, enumerable: true} | 422 var removeEventListener = document.removeEventListener.bind(document); | 
| 426 }; | 423 var dispatchEvent = document.dispatchEvent.bind(document); | 
| 427 | 424 var CustomEvent = window.CustomEvent; | 
| 428 WebSocket = function(url, protocol) | 425 | 
| 429 { | 426 function checkRequest(url, callback) | 
| 430 var blocked = false; | 427 { | 
| 431 var websocket; | |
| 
 
Sebastian Noack
2016/07/01 17:03:39
Since you access this variable before it might get
 
kzar
2016/07/06 16:39:19
Done.
 
 | |
| 432 var websocketProperties = { | |
| 
 
Sebastian Noack
2016/07/01 17:03:39
It seems you don't need to hard-code that properti
 
kzar
2016/07/06 16:39:19
Done.
 
 | |
| 433 attributes: { | |
| 434 url: url, | |
| 435 protocol: protocol, | |
| 436 readyState: readyStates.CONNECTING, | |
| 437 bufferedAmount: 0, | |
| 438 extensions: "", | |
| 439 binaryType: "blob", | |
| 440 onopen: null, | |
| 441 onerror: null, | |
| 442 onclose: null, | |
| 443 onmessage: null | |
| 444 }, | |
| 445 methods: ["close", "send", "addEventListener", "removeEventListener"] | |
| 446 }; | |
| 447 var properties = Object.create(null); | |
| 448 var queue = []; | |
| 449 | |
| 450 for (var key in websocketProperties.attributes) | |
| 451 { | |
| 452 properties[key] = function(key, defaultValue) | |
| 453 { | |
| 454 return { | |
| 455 enumerable: true, | |
| 
 
Sebastian Noack
2016/07/01 17:03:40
Instead hard-coding metadata like "enumerable" you
 
kzar
2016/07/06 16:39:19
Done.
 
 | |
| 456 get: function() | |
| 457 { | |
| 458 if (blocked && key == "readyState") | |
| 459 return readyStates.CLOSED; | |
| 460 return websocket ? websocket[key] : defaultValue; | |
| 461 }, | |
| 462 set: function(value) | |
| 463 { | |
| 464 if (websocket) | |
| 465 return websocket[key] = value; | |
| 
 
Sebastian Noack
2016/07/01 17:03:40
Why do you return in a setter?
 
kzar
2016/07/06 16:39:20
Because the return value for an assignment is usua
 
Sebastian Noack
2016/08/08 16:42:53
An assignment operation returns it's right-hand va
 
kzar
2016/08/08 18:19:05
Ah, I didn't know that!
 
 | |
| 466 else | |
| 467 { | |
| 468 queue.push(["set", key, value]); | |
| 469 return value; | |
| 470 } | |
| 471 } | |
| 472 }; | |
| 473 }(key, websocketProperties.attributes[key]); | |
| 474 } | |
| 475 for (var i = 0; i < websocketProperties.methods.length; i += 1) | |
| 476 { | |
| 477 var method = websocketProperties.methods[i]; | |
| 478 properties[method] = (function(method) | |
| 479 { | |
| 480 return { | |
| 481 enumerable: true, | |
| 482 value: function() | |
| 483 { | |
| 484 if (websocket) | |
| 485 websocket[method].apply(websocket, arguments); | |
| 486 else | |
| 487 queue.push(["call", method, arguments]); | |
| 488 } | |
| 489 }; | |
| 490 }(method)); | |
| 491 } | |
| 492 Object.defineProperties(this, properties); | |
| 
 
Sebastian Noack
2016/07/01 17:03:40
Note that those properties (in Chrome at least) ar
 
kzar
2016/07/06 16:39:20
Done.
 
 | |
| 493 | |
| 494 function processQueue() | |
| 495 { | |
| 496 for (var i = 0; i < queue.length; i += 1) | |
| 497 { | |
| 498 var action = queue[i][0]; | |
| 499 var key = queue[i][1]; | |
| 500 var value = queue[i][2]; | |
| 501 switch (action) | |
| 502 { | |
| 503 case "set": | |
| 504 websocket[key] = value; | |
| 505 break; | |
| 506 case "call": | |
| 507 websocket[key].apply(websocket, value); | |
| 508 break; | |
| 509 } | |
| 510 } | |
| 511 queue = undefined; | |
| 512 } | |
| 513 | |
| 514 var incomingEventName = eventName + "-" + url; | 428 var incomingEventName = eventName + "-" + url; | 
| 515 function listener(event) | 429 function listener(event) | 
| 516 { | 430 { | 
| 517 blocked = event.detail; | 431 callback(event.detail); | 
| 432 removeEventListener(incomingEventName, listener); | |
| 433 } | |
| 434 addEventListener(incomingEventName, listener); | |
| 435 | |
| 436 dispatchEvent(new CustomEvent(eventName, { | |
| 437 detail: {url: url} | |
| 438 })); | |
| 439 } | |
| 440 | |
| 441 WebSocket = function WrappedWebSocket(url, protocols) | |
| 442 { | |
| 443 // Throw correct exceptions if the constructor is used improperly. | |
| 444 if (!(this instanceof WrappedWebSocket)) return RealWebSocket(); | |
| 445 if (arguments.length < 1) return new RealWebSocket(); | |
| 446 | |
| 447 var websocket = new RealWebSocket(url, protocols); | |
| 448 | |
| 449 checkRequest(websocket.url, function(blocked) | |
| 450 { | |
| 518 if (blocked) | 451 if (blocked) | 
| 519 queue = undefined; | 452 closeWebSocket(websocket); | 
| 520 else | 453 }); | 
| 521 { | 454 | 
| 522 websocket = new originalWebSocket(url, protocol); | 455 return websocket; | 
| 523 processQueue(); | 456 }.bind(); | 
| 524 } | 457 | 
| 525 | 458 Object.defineProperties(WebSocket, { | 
| 526 document.removeEventListener(incomingEventName, listener); | 459 CONNECTING: {value: RealWebSocket.CONNECTING, enumerable: true}, | 
| 527 } | 460 OPEN: {value: RealWebSocket.OPEN, enumerable: true}, | 
| 528 document.addEventListener(incomingEventName, listener); | 461 CLOSING: {value: RealWebSocket.CLOSING, enumerable: true}, | 
| 529 | 462 CLOSED: {value: RealWebSocket.CLOSED, enumerable: true}, | 
| 530 document.dispatchEvent(new CustomEvent(eventName, { | 463 prototype: {value: RealWebSocket.prototype} | 
| 531 detail: {url: url, protocol: protocol} | 464 }); | 
| 532 })); | 465 | 
| 533 }; | 466 RealWebSocket.prototype.constructor = WebSocket; | 
| 534 WebSocket.prototype = Object.create(window.EventTarget.prototype, readyState s); | 467 }, eventName); | 
| 
 
Sebastian Noack
2016/07/01 17:03:40
Can't you simply reuse the original parent prototy
 
kzar
2016/07/06 16:39:20
Done. (Good point as it is different in Safari.)
 
 | |
| 535 Object.defineProperties(WebSocket, readyStates); | |
| 536 } | |
| 537 | |
| 538 script.textContent += "(" + wrapper.toString() + ")(\"" + eventName + "\");"; | |
| 
 
Sebastian Noack
2016/07/01 17:03:40
This is inconsistent with how we generate the code
 
kzar
2016/07/06 16:39:19
Done, however wasn't sure how to test the code abo
 
 | |
| 539 } | 468 } | 
| 540 | 469 | 
| 541 function init(document) | 470 function init(document) | 
| 542 { | 471 { | 
| 543 var shadow = null; | 472 var shadow = null; | 
| 544 var style = null; | 473 var style = null; | 
| 545 var observer = null; | 474 var observer = null; | 
| 546 var tracer = null; | 475 var tracer = null; | 
| 547 | 476 | 
| 548 // Scripts to be injected into the page, executed at the end of initialization | 477 wrapWebSocket(); | 
| 549 var script = document.createElement("script"); | |
| 550 script.async = false; | |
| 551 | |
| 552 wrapWebSocket(script); | |
| 553 | 478 | 
| 554 function getPropertyFilters(callback) | 479 function getPropertyFilters(callback) | 
| 555 { | 480 { | 
| 556 ext.backgroundPage.sendMessage({ | 481 ext.backgroundPage.sendMessage({ | 
| 557 type: "filters.get", | 482 type: "filters.get", | 
| 558 what: "cssproperties" | 483 what: "cssproperties" | 
| 559 }, callback); | 484 }, callback); | 
| 560 } | 485 } | 
| 561 var propertyFilters = new CSSPropertyFilters(window, getPropertyFilters, | 486 var propertyFilters = new CSSPropertyFilters(window, getPropertyFilters, | 
| 562 addElemHideSelectors); | 487 addElemHideSelectors); | 
| (...skipping 28 matching lines...) Expand all Loading... | |
| 591 (shadow || document.head || document.documentElement).appendChild(style); | 516 (shadow || document.head || document.documentElement).appendChild(style); | 
| 592 | 517 | 
| 593 // It can happen that the frame already navigated to a different | 518 // It can happen that the frame already navigated to a different | 
| 594 // document while we were waiting for the background page to respond. | 519 // document while we were waiting for the background page to respond. | 
| 595 // In that case the sheet property will stay null, after addind the | 520 // In that case the sheet property will stay null, after addind the | 
| 596 // <style> element to the shadow DOM. | 521 // <style> element to the shadow DOM. | 
| 597 if (!style.sheet) | 522 if (!style.sheet) | 
| 598 return; | 523 return; | 
| 599 | 524 | 
| 600 observer = reinjectStyleSheetWhenRemoved(document, style); | 525 observer = reinjectStyleSheetWhenRemoved(document, style); | 
| 601 if (script) | 526 protectStyleSheet(document, style); | 
| 
 
Sebastian Noack
2016/07/01 17:03:39
Sorry, I didn't notice that this a different code
 
kzar
2016/07/06 16:39:19
Done.
 
 | |
| 602 protectStyleSheet(document, style, script); | |
| 603 } | 527 } | 
| 604 | 528 | 
| 605 // If using shadow DOM, we have to add the ::content pseudo-element | 529 // If using shadow DOM, we have to add the ::content pseudo-element | 
| 606 // before each selector, in order to match elements within the | 530 // before each selector, in order to match elements within the | 
| 607 // insertion point. | 531 // insertion point. | 
| 608 if (shadow) | 532 if (shadow) | 
| 609 { | 533 { | 
| 610 var preparedSelectors = []; | 534 var preparedSelectors = []; | 
| 611 for (var i = 0; i < selectors.length; i++) | 535 for (var i = 0; i < selectors.length; i++) | 
| 612 { | 536 { | 
| (...skipping 94 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 707 if (!contentWindow.collapsing) | 631 if (!contentWindow.collapsing) | 
| 708 Array.prototype.forEach.call( | 632 Array.prototype.forEach.call( | 
| 709 contentDocument.querySelectorAll(Object.keys(typeMap).join(",")), | 633 contentDocument.querySelectorAll(Object.keys(typeMap).join(",")), | 
| 710 checkCollapse | 634 checkCollapse | 
| 711 ); | 635 ); | 
| 712 } | 636 } | 
| 713 } | 637 } | 
| 714 } | 638 } | 
| 715 }, true); | 639 }, true); | 
| 716 | 640 | 
| 717 document.documentElement.appendChild(script); | |
| 718 document.documentElement.removeChild(script); | |
| 719 script = undefined; | |
| 720 | |
| 721 return updateStylesheet; | 641 return updateStylesheet; | 
| 722 } | 642 } | 
| 723 | 643 | 
| 724 if (document instanceof HTMLDocument) | 644 if (document instanceof HTMLDocument) | 
| 725 { | 645 { | 
| 726 checkSitekey(); | 646 checkSitekey(); | 
| 727 window.updateStylesheet = init(document); | 647 window.updateStylesheet = init(document); | 
| 728 } | 648 } | 
| LEFT | RIGHT |