Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code

Side by Side Diff: include.preload.js

Issue 29350213: Issue 4364 - Drop support for Chrome 29-40 and remove legacy code (Closed)
Patch Set: Rebased, renamed function to runInPageContext(), fixed typo in comment Created Sept. 9, 2016, 2:38 p.m.
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
View unified diff | Download patch
OLDNEW
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
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
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
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 runInPageContext(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 runInPageContext(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
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 as to not mess with with web pages that
447 type: "filters.get", 457 // rely on the order of their own <style> tags (#309). However, creating
448 what: "cssproperties" 458 // a 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 runInPageContext(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 }
OLDNEW
« chrome/ext/background.js ('K') | « composer.postload.js ('k') | metadata.chrome » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld