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

Delta Between Two Patch Sets: include.preload.js

Issue 29401596: Issue 5094 - Implement support for :has() in chrome extension (Closed) Base URL: https://hg.adblockplus.org/adblockpluschrome/
Left Patch Set: Properly implement the tracer. Improve the element hiding logic. Created April 5, 2017, 9:07 a.m.
Right Patch Set: Update dependencies to the latest. Created June 19, 2017, 2:12 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 | « dependencies ('k') | lib/filterValidation.js » ('j') | 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-2017 eyeo GmbH 3 * Copyright (C) 2006-2017 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 109 matching lines...) Expand 10 before | Expand all | Expand 10 after
120 120
121 for (let i = 0; i < urls.length; i++) 121 for (let i = 0; i < urls.length; i++)
122 { 122 {
123 if (/^(?!https?:)[\w-]+:/i.test(urls[i])) 123 if (/^(?!https?:)[\w-]+:/i.test(urls[i]))
124 urls.splice(i--, 1); 124 urls.splice(i--, 1);
125 } 125 }
126 126
127 return urls; 127 return urls;
128 } 128 }
129 129
130 /* collapse the element and ensure it stays that way */ 130 function hideElement(element)
Sebastian Noack 2017/04/05 14:58:49 If you feel that this function needs to be documen
131 function collapseElement(element) 131 {
Sebastian Noack 2017/04/05 14:58:49 "element collapsing" is a special term in the cont
132 { 132 function doHide(el)
133 function doCollapse(element)
134 { 133 {
135 let propertyName = "display"; 134 let propertyName = "display";
136 let propertyValue = "none"; 135 let propertyValue = "none";
137 if (element.localName == "frame") 136 if (el.localName == "frame")
138 { 137 {
139 propertyName = "visibility"; 138 propertyName = "visibility";
140 propertyValue = "hidden"; 139 propertyValue = "hidden";
141 } 140 }
142 141
143 if (element.style.getPropertyValue(propertyName) != propertyValue || 142 if (el.style.getPropertyValue(propertyName) != propertyValue ||
144 element.style.getPropertyPriority(propertyName) != "important") 143 el.style.getPropertyPriority(propertyName) != "important")
145 element.style.setProperty(propertyName, propertyValue, "important"); 144 el.style.setProperty(propertyName, propertyValue, "important");
146 } 145 }
147 146
148 doCollapse(element); 147 doHide(element);
149 148
150 new MutationObserver(doCollapse).observe( 149 new MutationObserver(doHide).observe(
151 element, { 150 element, {
152 attributes: true, 151 attributes: true,
153 attributeFilter: ["style"] 152 attributeFilter: ["style"]
154 } 153 }
155 ); 154 );
156 } 155 }
157 156
158 function checkCollapse(element) 157 function checkCollapse(element)
159 { 158 {
160 let mediatype = typeMap.get(element.localName); 159 let mediatype = typeMap.get(element.localName);
161 if (!mediatype) 160 if (!mediatype)
162 return; 161 return;
163 162
164 let urls = getURLsFromElement(element); 163 let urls = getURLsFromElement(element);
165 if (urls.length == 0) 164 if (urls.length == 0)
166 return; 165 return;
167 166
168 ext.backgroundPage.sendMessage( 167 ext.backgroundPage.sendMessage(
169 { 168 {
170 type: "filters.collapse", 169 type: "filters.collapse",
171 urls, 170 urls,
172 mediatype, 171 mediatype,
173 baseURL: document.location.href 172 baseURL: document.location.href
174 }, 173 },
175 174
176 collapse => 175 collapse =>
177 { 176 {
178 if (collapse) 177 if (collapse)
179 { 178 {
180 collapseElement(element); 179 hideElement(element);
181 } 180 }
182 } 181 }
183 ); 182 );
184 } 183 }
185 184
186 function checkSitekey() 185 function checkSitekey()
187 { 186 {
188 let attr = document.documentElement.getAttribute("data-adblockkey"); 187 let attr = document.documentElement.getAttribute("data-adblockkey");
189 if (attr) 188 if (attr)
190 ext.backgroundPage.sendMessage({type: "filters.addKey", token: attr}); 189 ext.backgroundPage.sendMessage({type: "filters.addKey", token: attr});
191 } 190 }
192 191
193 function ElementHidingTracer() 192 function ElementHidingTracer()
194 { 193 {
195 this.selectors = []; 194 this.selectors = [];
196 this.filters = [];
197
198 this.changedNodes = []; 195 this.changedNodes = [];
199 this.timeout = null; 196 this.timeout = null;
200
201 this.observer = new MutationObserver(this.observe.bind(this)); 197 this.observer = new MutationObserver(this.observe.bind(this));
202 this.trace = this.trace.bind(this); 198 this.trace = this.trace.bind(this);
203 199
204 if (document.readyState == "loading") 200 if (document.readyState == "loading")
205 document.addEventListener("DOMContentLoaded", this.trace); 201 document.addEventListener("DOMContentLoaded", this.trace);
206 else 202 else
207 this.trace(); 203 this.trace();
208 } 204 }
209 ElementHidingTracer.prototype = { 205 ElementHidingTracer.prototype = {
210 addSelectors(selectors, filters) 206 addSelectors(selectors, filters)
211 { 207 {
208 let pairs = selectors.map((sel, i) => [sel, filters && filters[i]]);
209
212 if (document.readyState != "loading") 210 if (document.readyState != "loading")
213 this.checkNodes([document], selectors, filters); 211 this.checkNodes([document], pairs);
214 212
215 this.selectors.push(...selectors); 213 this.selectors.push(...pairs);
216 this.filters.push(...filters); 214 },
217 }, 215
218 216 checkNodes(nodes, pairs)
219 hideElements(filters) 217 {
Sebastian Noack 2017/04/05 14:58:49 The name of this function isn't optimal. It doesn'
hub 2017/04/06 10:15:20 ok
220 { 218 let selectors = [];
221 let matchedSelectors = []; 219 let filters = [];
222 for (let filter of filters) 220
223 matchedSelectors.push(filter.replace(/^.*?##/, "")); 221 for (let [selector, filter] of pairs)
Sebastian Noack 2017/04/05 14:58:49 This logic is redundant with code in checkNodes().
hub 2017/04/06 10:15:20 Acknowledged.
224
225 if (document.readyState != "loading")
Sebastian Noack 2017/04/05 14:58:49 This check seems wrong. ElementHidingTracer.addSel
hub 2017/04/06 10:15:20 Acknowledged.
226 ext.backgroundPage.sendMessage({
227 type: "devtools.traceElemHide",
228 selectors: matchedSelectors
229 });
230 },
231
232 checkNodes(nodes, selectors, filters)
233 {
234 let matchedSelectors = [];
235
236 for (let i = 0; i < selectors.length; i++)
237 { 222 {
238 nodes: for (let node of nodes) 223 nodes: for (let node of nodes)
239 { 224 {
240 let elements = node.querySelectorAll(selectors[i]); 225 for (let element of node.querySelectorAll(selector))
241
242 for (let element of elements)
243 { 226 {
244 // Only consider selectors that actually have an effect on the 227 // Only consider selectors that actually have an effect on the
245 // computed styles, and aren't overridden by rules with higher 228 // computed styles, and aren't overridden by rules with higher
246 // priority, or haven't been circumvented in a different way. 229 // priority, or haven't been circumvented in a different way.
247 if (getComputedStyle(element).display == "none") 230 if (getComputedStyle(element).display == "none")
248 { 231 {
249 matchedSelectors.push(filters[i].replace(/^.*?##/, "")); 232 // For regular element hiding, we don't know the exact filter,
233 // but the background page can find it with the given selector.
234 // In case of element hiding emulation, the generated selector
235 // we got here is different from the selector part of the filter,
236 // but in this case we can send the whole filter text instead.
237 if (filter)
238 filters.push(filter);
239 else
240 selectors.push(selector);
241
250 break nodes; 242 break nodes;
251 } 243 }
252 } 244 }
253 } 245 }
254 } 246 }
255 247
256 if (matchedSelectors.length > 0) 248 if (selectors.length > 0 || filters.length > 0)
257 { 249 {
258 ext.backgroundPage.sendMessage({ 250 ext.backgroundPage.sendMessage({
259 type: "devtools.traceElemHide", 251 type: "devtools.traceElemHide",
260 selectors: matchedSelectors 252 selectors, filters
261 }); 253 });
262 } 254 }
263 }, 255 },
264 256
265 onTimeout() 257 onTimeout()
266 { 258 {
267 this.checkNodes(this.changedNodes, this.selectors, this.filters); 259 this.checkNodes(this.changedNodes, this.selectors);
268 this.changedNodes = []; 260 this.changedNodes = [];
269 this.timeout = null; 261 this.timeout = null;
270 }, 262 },
271 263
272 observe(mutations) 264 observe(mutations)
273 { 265 {
274 // Forget previously changed nodes that are no longer in the DOM. 266 // Forget previously changed nodes that are no longer in the DOM.
275 for (let i = 0; i < this.changedNodes.length; i++) 267 for (let i = 0; i < this.changedNodes.length; i++)
276 { 268 {
277 if (!document.contains(this.changedNodes[i])) 269 if (!document.contains(this.changedNodes[i]))
(...skipping 41 matching lines...) Expand 10 before | Expand all | Expand 10 after
319 311
320 // Check only nodes whose descendants have changed, and not more often 312 // Check only nodes whose descendants have changed, and not more often
321 // than once a second. Otherwise large pages with a lot of DOM mutations 313 // than once a second. Otherwise large pages with a lot of DOM mutations
322 // (like YouTube) freeze when the devtools panel is active. 314 // (like YouTube) freeze when the devtools panel is active.
323 if (this.timeout == null) 315 if (this.timeout == null)
324 this.timeout = setTimeout(this.onTimeout.bind(this), 1000); 316 this.timeout = setTimeout(this.onTimeout.bind(this), 1000);
325 }, 317 },
326 318
327 trace() 319 trace()
328 { 320 {
329 this.checkNodes([document], this.selectors, this.filters); 321 this.checkNodes([document], this.selectors);
330 322
331 this.observer.observe( 323 this.observer.observe(
332 document, 324 document,
333 { 325 {
334 childList: true, 326 childList: true,
335 attributes: true, 327 attributes: true,
336 subtree: true 328 subtree: true
337 } 329 }
338 ); 330 );
339 }, 331 },
340 332
341 disconnect() 333 disconnect()
342 { 334 {
343 document.removeEventListener("DOMContentLoaded", this.trace); 335 document.removeEventListener("DOMContentLoaded", this.trace);
344 this.observer.disconnect(); 336 this.observer.disconnect();
345 clearTimeout(this.timeout); 337 clearTimeout(this.timeout);
346 } 338 }
347 }; 339 };
348 340
349 function runInPageContext(fn, arg)
350 {
351 let script = document.createElement("script");
352 script.type = "application/javascript";
353 script.async = false;
354 script.textContent = "(" + fn + ")(" + JSON.stringify(arg) + ");";
355 document.documentElement.appendChild(script);
356 document.documentElement.removeChild(script);
357 }
358
359 // Before Chrome 58 the webRequest API didn't allow us to intercept
360 // WebSockets[1], and therefore some ad networks are misusing them as a way to
361 // serve adverts and circumvent us. As a workaround we wrap WebSocket,
362 // preventing blocked WebSocket connections from being opened.
363 // [1] - https://bugs.chromium.org/p/chromium/issues/detail?id=129353
364 function wrapWebSocket()
365 {
366 let randomEventName = "abpws-" + Math.random().toString(36).substr(2);
367
368 document.addEventListener(randomEventName, event =>
369 {
370 ext.backgroundPage.sendMessage({
371 type: "request.websocket",
372 url: event.detail.url
373 }, block =>
374 {
375 document.dispatchEvent(new CustomEvent(
376 randomEventName + "-" + event.detail.url, {detail: block}
377 ));
378 });
379 });
380
381 runInPageContext(eventName =>
382 {
383 // As far as possible we must track everything we use that could be
384 // sabotaged by the website later in order to circumvent us.
385 let RealWebSocket = WebSocket;
386 let RealCustomEvent = window.CustomEvent;
387 let closeWebSocket = Function.prototype.call.bind(
388 RealWebSocket.prototype.close
389 );
390 let addEventListener = document.addEventListener.bind(document);
391 let removeEventListener = document.removeEventListener.bind(document);
392 let dispatchEvent = document.dispatchEvent.bind(document);
393
394 function checkRequest(url, callback)
395 {
396 let incomingEventName = eventName + "-" + url;
397 function listener(event)
398 {
399 callback(event.detail);
400 removeEventListener(incomingEventName, listener);
401 }
402 addEventListener(incomingEventName, listener);
403
404 dispatchEvent(new RealCustomEvent(eventName, {detail: {url}}));
405 }
406
407 function WrappedWebSocket(url, ...args)
408 {
409 // Throw correct exceptions if the constructor is used improperly.
410 if (!(this instanceof WrappedWebSocket)) return RealWebSocket();
411 if (arguments.length < 1) return new RealWebSocket();
412
413 let websocket = new RealWebSocket(url, ...args);
414
415 checkRequest(websocket.url, blocked =>
416 {
417 if (blocked)
418 closeWebSocket(websocket);
419 });
420
421 return websocket;
422 }
423 WrappedWebSocket.prototype = RealWebSocket.prototype;
424 window.WebSocket = WrappedWebSocket.bind();
425 Object.defineProperties(WebSocket, {
426 CONNECTING: {value: RealWebSocket.CONNECTING, enumerable: true},
427 OPEN: {value: RealWebSocket.OPEN, enumerable: true},
428 CLOSING: {value: RealWebSocket.CLOSING, enumerable: true},
429 CLOSED: {value: RealWebSocket.CLOSED, enumerable: true},
430 prototype: {value: RealWebSocket.prototype}
431 });
432
433 RealWebSocket.prototype.constructor = WebSocket;
434 }, randomEventName);
435 }
436
437 function ElemHide() 341 function ElemHide()
438 { 342 {
439 this.shadow = this.createShadowTree(); 343 this.shadow = this.createShadowTree();
440 this.style = null; 344 this.style = null;
441 this.tracer = null; 345 this.tracer = null;
442 346
443 this.elemHideEmulation = new ElemHideEmulation( 347 this.elemHideEmulation = new ElemHideEmulation(
444 window, 348 window,
445 callback => 349 callback =>
446 { 350 {
(...skipping 23 matching lines...) Expand all
470 if (/\.(?:google|blogger)\.com$/.test(document.domain)) 374 if (/\.(?:google|blogger)\.com$/.test(document.domain))
471 return null; 375 return null;
472 376
473 // Finally since some users have both AdBlock and Adblock Plus installed we 377 // Finally since some users have both AdBlock and Adblock Plus installed we
474 // have to consider how the two extensions interact. For example we want to 378 // have to consider how the two extensions interact. For example we want to
475 // avoid creating the shadowRoot twice. 379 // avoid creating the shadowRoot twice.
476 let shadow = document.documentElement.shadowRoot || 380 let shadow = document.documentElement.shadowRoot ||
477 document.documentElement.createShadowRoot(); 381 document.documentElement.createShadowRoot();
478 shadow.appendChild(document.createElement("shadow")); 382 shadow.appendChild(document.createElement("shadow"));
479 383
480 // Stop the website from messing with our shadow root (#4191, #4298).
481 if ("shadowRoot" in Element.prototype)
482 {
483 runInPageContext(() =>
484 {
485 let ourShadowRoot = document.documentElement.shadowRoot;
486 if (!ourShadowRoot)
487 return;
488 let desc = Object.getOwnPropertyDescriptor(Element.prototype,
489 "shadowRoot");
490 let shadowRoot = Function.prototype.call.bind(desc.get);
491
492 Object.defineProperty(Element.prototype, "shadowRoot", {
493 configurable: true, enumerable: true, get()
494 {
495 let thisShadow = shadowRoot(this);
496 return thisShadow == ourShadowRoot ? null : thisShadow;
497 }
498 });
499 }, null);
500 }
501
502 return shadow; 384 return shadow;
503 }, 385 },
504 386
505 addSelectors(selectors, filters) 387 addSelectors(selectors, filters)
506 { 388 {
507 if (selectors.length == 0) 389 if (selectors.length == 0)
508 return; 390 return;
509 391
510 if (!this.style) 392 if (!this.style)
511 { 393 {
(...skipping 39 matching lines...) Expand 10 before | Expand all | Expand 10 after
551 for (let i = 0; i < preparedSelectors.length; i += this.selectorGroupSize) 433 for (let i = 0; i < preparedSelectors.length; i += this.selectorGroupSize)
552 { 434 {
553 let selector = preparedSelectors.slice( 435 let selector = preparedSelectors.slice(
554 i, i + this.selectorGroupSize 436 i, i + this.selectorGroupSize
555 ).join(", "); 437 ).join(", ");
556 this.style.sheet.insertRule(selector + "{display: none !important;}", 438 this.style.sheet.insertRule(selector + "{display: none !important;}",
557 this.style.sheet.cssRules.length); 439 this.style.sheet.cssRules.length);
558 } 440 }
559 441
560 if (this.tracer) 442 if (this.tracer)
561 this.tracer.addSelectors(selectors, filters || selectors); 443 this.tracer.addSelectors(selectors, filters);
562 }, 444 },
563 445
564 hideElements(elements, filters) 446 hideElements(elements, filters)
565 { 447 {
566 for (let element of elements) 448 for (let element of elements)
567 collapseElement(element); 449 hideElement(element);
568 450
569 if (this.tracer) 451 if (this.tracer)
570 this.tracer.hideElements(filters); 452 {
453 ext.backgroundPage.sendMessage({
454 type: "devtools.traceElemHide",
455 selectors: [],
456 filters
457 });
458 }
571 }, 459 },
572 460
573 apply() 461 apply()
574 { 462 {
575 ext.backgroundPage.sendMessage({type: "get-selectors"}, response => 463 ext.backgroundPage.sendMessage({type: "get-selectors"}, response =>
576 { 464 {
577 if (this.tracer) 465 if (this.tracer)
578 this.tracer.disconnect(); 466 this.tracer.disconnect();
579 this.tracer = null; 467 this.tracer = null;
580 468
581 if (this.style && this.style.parentElement) 469 if (this.style && this.style.parentElement)
582 this.style.parentElement.removeChild(this.style); 470 this.style.parentElement.removeChild(this.style);
583 this.style = null; 471 this.style = null;
584 472
585 if (response.trace) 473 if (response.trace)
586 this.tracer = new ElementHidingTracer(); 474 this.tracer = new ElementHidingTracer();
587 475
588 this.addSelectors(response.selectors); 476 this.addSelectors(response.selectors);
589 this.elemHideEmulation.apply(); 477 this.elemHideEmulation.apply();
590 }); 478 });
591 } 479 }
592 }; 480 };
593 481
594 if (document instanceof HTMLDocument) 482 if (document instanceof HTMLDocument)
595 { 483 {
596 checkSitekey(); 484 checkSitekey();
597 wrapWebSocket();
598 485
599 elemhide = new ElemHide(); 486 elemhide = new ElemHide();
600 elemhide.apply(); 487 elemhide.apply();
601 488
602 document.addEventListener("error", event => 489 document.addEventListener("error", event =>
603 { 490 {
604 checkCollapse(event.target); 491 checkCollapse(event.target);
605 }, true); 492 }, true);
606 493
607 document.addEventListener("load", event => 494 document.addEventListener("load", event =>
608 { 495 {
609 let element = event.target; 496 let element = event.target;
610 if (/^i?frame$/.test(element.localName)) 497 if (/^i?frame$/.test(element.localName))
611 checkCollapse(element); 498 checkCollapse(element);
612 }, true); 499 }, true);
613 } 500 }
LEFTRIGHT

Powered by Google App Engine
This is Rietveld