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

Delta Between Two Patch Sets: include.preload.js

Issue 29348869: Issue 4101 - Prevent runaway MutationObservers (Closed)
Left Patch Set: Created July 29, 2016, 9:06 a.m.
Right Patch Set: Removed _collapsed logic, addressed nits Created Aug. 11, 2016, 1:52 p.m.
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
Left: Side by side diff | Download
Right: Side by side diff | Download
« no previous file with change/comment | « no previous file | no next file » | no next file with change/comment »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
LEFTRIGHT
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
(...skipping 140 matching lines...) Expand 10 before | Expand all | Expand 10 after
151 function collapseElement() 151 function collapseElement()
152 { 152 {
153 var propertyName = "display"; 153 var propertyName = "display";
154 var propertyValue = "none"; 154 var propertyValue = "none";
155 if (element.localName == "frame") 155 if (element.localName == "frame")
156 { 156 {
157 propertyName = "visibility"; 157 propertyName = "visibility";
158 propertyValue = "hidden"; 158 propertyValue = "hidden";
159 } 159 }
160 160
161 if (element.style.getPropertyValue(propertyName) !== propertyValue || 161 if (element.style.getPropertyValue(propertyName) != propertyValue ||
Sebastian Noack 2016/08/10 12:55:57 Nit: AS per Mozilla's coding practices we perfer !
kzar 2016/08/11 13:53:30 Done.
162 element.style.getPropertyPriority(propertyName) !== "important") 162 element.style.getPropertyPriority(propertyName) != "important")
163 element.style.setProperty(propertyName, propertyValue, "important"); 163 element.style.setProperty(propertyName, propertyValue, "important");
164 } 164 }
165 165
166 if (collapse && !element._collapsed) 166 if (collapse)
167 { 167 {
168 collapseElement(); 168 collapseElement();
169 element._collapsed = true;
170 169
171 if (MutationObserver) 170 if (MutationObserver)
172 new MutationObserver(collapseElement).observe( 171 new MutationObserver(collapseElement).observe(
173 element, { 172 element, {
174 attributes: true, 173 attributes: true,
175 attributeFilter: ["style"] 174 attributeFilter: ["style"]
176 } 175 }
177 ); 176 );
178 } 177 }
179 } 178 }
180 ); 179 );
181 } 180 }
182 181
183 function checkSitekey() 182 function checkSitekey()
184 { 183 {
185 var attr = document.documentElement.getAttribute("data-adblockkey"); 184 var attr = document.documentElement.getAttribute("data-adblockkey");
186 if (attr) 185 if (attr)
187 ext.backgroundPage.sendMessage({type: "filter.addKey", token: attr}); 186 ext.backgroundPage.sendMessage({type: "filters.addKey", token: attr});
188 } 187 }
189 188
190 function getContentDocument(element) 189 function getContentDocument(element)
191 { 190 {
192 try 191 try
193 { 192 {
194 return element.contentDocument; 193 return element.contentDocument;
195 } 194 }
196 catch (e) 195 catch (e)
197 { 196 {
(...skipping 135 matching lines...) Expand 10 before | Expand all | Expand 10 after
333 }, 332 },
334 333
335 disconnect: function() 334 disconnect: function()
336 { 335 {
337 this.document.removeEventListener("DOMContentLoaded", this.trace); 336 this.document.removeEventListener("DOMContentLoaded", this.trace);
338 this.observer.disconnect(); 337 this.observer.disconnect();
339 clearTimeout(this.timeout); 338 clearTimeout(this.timeout);
340 } 339 }
341 }; 340 };
342 341
343 function reinjectStyleSheetWhenRemoved(document, style) 342 function runInDocument(document, fn, arg)
344 { 343 {
345 if (!MutationObserver)
346 return null;
347
348 var parentNode = style.parentNode;
349 var observer = new MutationObserver(function()
350 {
351 if (style.parentNode != parentNode)
352 parentNode.appendChild(style);
353 });
354
355 observer.observe(parentNode, {childList: true});
356 return observer;
357 }
358
359 function protectStyleSheet(document, style)
360 {
361 var id = Math.random().toString(36).substr(2)
362 style.id = id;
363
364 var code = [
365 "(function()",
366 "{",
367 ' var style = document.getElementById("' + id + '") ||',
368 ' document.documentElement.shadowRoot.getElementById("' + id + '");',
369 ' style.removeAttribute("id");'
370 ];
371
372 var disableables = ["style", "style.sheet"];
373 for (var i = 0; i < disableables.length; i++)
374 {
375 code.push(" Object.defineProperty(" + disableables[i] + ', "disabled", '
376 + "{value: false, enumerable: true});") ;
377 }
378
379 var methods = ["deleteRule", "removeRule"];
380 for (var j = 0; j < methods.length; j++)
381 {
382 var method = methods[j];
383 if (method in CSSStyleSheet.prototype)
384 {
385 var origin = "CSSStyleSheet.prototype." + method;
386 code.push(" var " + method + " = " + origin + ";",
387 " " + origin + " = function(index)",
388 " {",
389 " if (this != style.sheet)",
390 " " + method + ".call(this, index);",
391 " }");
392 }
393 }
394
395 code.push("})();");
396
397 var script = document.createElement("script"); 344 var script = document.createElement("script");
345 script.type = "application/javascript";
398 script.async = false; 346 script.async = false;
399 script.textContent = code.join("\n"); 347 script.textContent = "(" + fn + ")(" + JSON.stringify(arg) + ");";
400 document.documentElement.appendChild(script); 348 document.documentElement.appendChild(script);
401 document.documentElement.removeChild(script); 349 document.documentElement.removeChild(script);
350 }
351
352 // Neither Chrome[1] nor Safari allow us to intercept WebSockets, and therefore
353 // some ad networks are misusing them as a way to serve adverts and circumvent
354 // us. As a workaround we wrap WebSocket, preventing blocked WebSocket
355 // connections from being opened.
356 // [1] - https://bugs.chromium.org/p/chromium/issues/detail?id=129353
357 function wrapWebSocket(document)
358 {
359 if (typeof WebSocket == "undefined")
360 return;
361
362 var eventName = "abpws-" + Math.random().toString(36).substr(2);
363
364 document.addEventListener(eventName, function(event)
365 {
366 ext.backgroundPage.sendMessage({
367 type: "websocket-request",
368 url: event.detail.url
369 }, function (block)
370 {
371 document.dispatchEvent(
372 new CustomEvent(eventName + "-" + event.detail.url, {detail: block})
373 );
374 });
375 });
376
377 runInDocument(document, function(eventName)
378 {
379 // As far as possible we must track everything we use that could be
380 // sabotaged by the website later in order to circumvent us.
381 var RealWebSocket = WebSocket;
382 var closeWebSocket = Function.prototype.call.bind(RealWebSocket.prototype.cl ose);
383 var addEventListener = document.addEventListener.bind(document);
384 var removeEventListener = document.removeEventListener.bind(document);
385 var dispatchEvent = document.dispatchEvent.bind(document);
386 var CustomEvent = window.CustomEvent;
387
388 function checkRequest(url, callback)
389 {
390 var incomingEventName = eventName + "-" + url;
391 function listener(event)
392 {
393 callback(event.detail);
394 removeEventListener(incomingEventName, listener);
395 }
396 addEventListener(incomingEventName, listener);
397
398 dispatchEvent(new CustomEvent(eventName, {
399 detail: {url: url}
400 }));
401 }
402
403 WebSocket = function WrappedWebSocket(url, protocols)
404 {
405 // Throw correct exceptions if the constructor is used improperly.
406 if (!(this instanceof WrappedWebSocket)) return RealWebSocket();
407 if (arguments.length < 1) return new RealWebSocket();
408
409 var websocket = new RealWebSocket(url, protocols);
410
411 checkRequest(websocket.url, function(blocked)
412 {
413 if (blocked)
414 closeWebSocket(websocket);
415 });
416
417 return websocket;
418 }.bind();
419
420 Object.defineProperties(WebSocket, {
421 CONNECTING: {value: RealWebSocket.CONNECTING, enumerable: true},
422 OPEN: {value: RealWebSocket.OPEN, enumerable: true},
423 CLOSING: {value: RealWebSocket.CLOSING, enumerable: true},
424 CLOSED: {value: RealWebSocket.CLOSED, enumerable: true},
425 prototype: {value: RealWebSocket.prototype}
426 });
427
428 RealWebSocket.prototype.constructor = WebSocket;
429 }, eventName);
402 } 430 }
403 431
404 function init(document) 432 function init(document)
405 { 433 {
406 var shadow = null; 434 var shadow = null;
407 var style = null; 435 var style = null;
408 var observer = null; 436 var observer = null;
409 var tracer = null; 437 var tracer = null;
438
439 wrapWebSocket(document);
410 440
411 function getPropertyFilters(callback) 441 function getPropertyFilters(callback)
412 { 442 {
413 ext.backgroundPage.sendMessage({ 443 ext.backgroundPage.sendMessage({
414 type: "filters.get", 444 type: "filters.get",
415 what: "cssproperties" 445 what: "cssproperties"
416 }, callback); 446 }, callback);
417 } 447 }
418 var propertyFilters = new CSSPropertyFilters(window, getPropertyFilters, 448 var propertyFilters = new CSSPropertyFilters(window, getPropertyFilters,
419 addElemHideSelectors); 449 addElemHideSelectors);
420 450
421 // Use Shadow DOM if available to don't mess with web pages that rely on 451 // Use Shadow DOM if available to don't mess with web pages that rely on
422 // the order of their own <style> tags (#309). 452 // the order of their own <style> tags (#309).
423 // 453 //
424 // However, creating a shadow root breaks running CSS transitions. So we 454 // However, creating a shadow root breaks running CSS transitions. So we
425 // have to create the shadow root before transistions might start (#452). 455 // have to create the shadow root before transistions might start (#452).
426 // 456 //
427 // Also, using shadow DOM causes issues on some Google websites, 457 // Also, using shadow DOM causes issues on some Google websites,
428 // including Google Docs, Gmail and Blogger (#1770, #2602, #2687). 458 // including Google Docs, Gmail and Blogger (#1770, #2602, #2687).
429 if ("createShadowRoot" in document.documentElement && 459 if ("createShadowRoot" in document.documentElement &&
430 !/\.(?:google|blogger)\.com$/.test(document.domain)) 460 !/\.(?:google|blogger)\.com$/.test(document.domain))
431 { 461 {
432 shadow = document.documentElement.createShadowRoot(); 462 shadow = document.documentElement.createShadowRoot();
433 shadow.appendChild(document.createElement("shadow")); 463 shadow.appendChild(document.createElement("shadow"));
464
465 // Stop the website from messing with our shadowRoot
466 runInDocument(document, function()
467 {
468 var ourShadowRoot = document.documentElement.shadowRoot;
469 var desc = Object.getOwnPropertyDescriptor(Element.prototype, "shadowRoot" );
470 var shadowRoot = Function.prototype.call.bind(desc.get);
471
472 Object.defineProperty(Element.prototype, "shadowRoot", {
473 conifgurable: true, enumerable: true, get: function()
474 {
475 var shadow = shadowRoot(this);
476 return shadow == ourShadowRoot ? null : shadow;
477 }
478 });
479 }, null);
434 } 480 }
435 481
436 function addElemHideSelectors(selectors) 482 function addElemHideSelectors(selectors)
437 { 483 {
438 if (selectors.length == 0) 484 if (selectors.length == 0)
439 return; 485 return;
440 486
441 if (!style) 487 if (!style)
442 { 488 {
443 // Create <style> element lazily, only if we add styles. Add it to 489 // Create <style> element lazily, only if we add styles. Add it to
444 // the shadow DOM if possible. Otherwise fallback to the <head> or 490 // the shadow DOM if possible. Otherwise fallback to the <head> or
445 // <html> element. If we have injected a style element before that 491 // <html> element. If we have injected a style element before that
446 // has been removed (the sheet property is null), create a new one. 492 // has been removed (the sheet property is null), create a new one.
447 style = document.createElement("style"); 493 style = document.createElement("style");
448 (shadow || document.head || document.documentElement).appendChild(style); 494 (shadow || document.head || document.documentElement).appendChild(style);
449 495
450 // It can happen that the frame already navigated to a different 496 // It can happen that the frame already navigated to a different
451 // document while we were waiting for the background page to respond. 497 // document while we were waiting for the background page to respond.
452 // In that case the sheet property will stay null, after addind the 498 // In that case the sheet property will stay null, after addind the
453 // <style> element to the shadow DOM. 499 // <style> element to the shadow DOM.
454 if (!style.sheet) 500 if (!style.sheet)
455 return; 501 return;
456
457 observer = reinjectStyleSheetWhenRemoved(document, style);
458 protectStyleSheet(document, style);
459 } 502 }
460 503
461 // If using shadow DOM, we have to add the ::content pseudo-element 504 // If using shadow DOM, we have to add the ::content pseudo-element
462 // before each selector, in order to match elements within the 505 // before each selector, in order to match elements within the
463 // insertion point. 506 // insertion point.
464 if (shadow) 507 if (shadow)
465 { 508 {
466 var preparedSelectors = []; 509 var preparedSelectors = [];
467 for (var i = 0; i < selectors.length; i++) 510 for (var i = 0; i < selectors.length; i++)
468 { 511 {
(...skipping 102 matching lines...) Expand 10 before | Expand all | Expand 10 after
571 }, true); 614 }, true);
572 615
573 return updateStylesheet; 616 return updateStylesheet;
574 } 617 }
575 618
576 if (document instanceof HTMLDocument) 619 if (document instanceof HTMLDocument)
577 { 620 {
578 checkSitekey(); 621 checkSitekey();
579 window.updateStylesheet = init(document); 622 window.updateStylesheet = init(document);
580 } 623 }
LEFTRIGHT
« no previous file | no next file » | Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Toggle Comments ('s')

Powered by Google App Engine
This is Rietveld