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-present eyeo GmbH | 3 * Copyright (C) 2006-present 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 "use strict"; | 18 "use strict"; |
19 | 19 |
20 const {filterToRegExp, splitSelector} = require("common"); | 20 const {filterToRegExp, splitSelector} = require("common"); |
21 | 21 |
22 const MIN_INVOCATION_INTERVAL = 3000; | 22 let MIN_INVOCATION_INTERVAL = 3000; |
| 23 const MAX_SYNCHRONOUS_PROCESSING_TIME = 50; |
23 const abpSelectorRegexp = /:-abp-([\w-]+)\(/i; | 24 const abpSelectorRegexp = /:-abp-([\w-]+)\(/i; |
24 | 25 |
25 /** Return position of node from parent. | 26 /** Return position of node from parent. |
26 * @param {Node} node the node to find the position of. | 27 * @param {Node} node the node to find the position of. |
27 * @return {number} One-based index like for :nth-child(), or 0 on error. | 28 * @return {number} One-based index like for :nth-child(), or 0 on error. |
28 */ | 29 */ |
29 function positionInParent(node) | 30 function positionInParent(node) |
30 { | 31 { |
31 let {children} = node.parentNode; | 32 let {children} = node.parentNode; |
32 for (let i = 0; i < children.length; i++) | 33 for (let i = 0; i < children.length; i++) |
33 if (children[i] == node) | 34 if (children[i] == node) |
34 return i + 1; | 35 return i + 1; |
35 return 0; | 36 return 0; |
36 } | 37 } |
37 | 38 |
38 function makeSelector(node, selector) | 39 function makeSelector(node, selector) |
39 { | 40 { |
| 41 if (node == null) |
| 42 return null; |
40 if (!node.parentElement) | 43 if (!node.parentElement) |
41 { | 44 { |
42 let newSelector = ":root"; | 45 let newSelector = ":root"; |
43 if (selector) | 46 if (selector) |
44 newSelector += " > " + selector; | 47 newSelector += " > " + selector; |
45 return newSelector; | 48 return newSelector; |
46 } | 49 } |
47 let idx = positionInParent(node); | 50 let idx = positionInParent(node); |
48 if (idx > 0) | 51 if (idx > 0) |
49 { | 52 { |
(...skipping 71 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
121 | 124 |
122 function* evaluate(chain, index, prefix, subtree, styles) | 125 function* evaluate(chain, index, prefix, subtree, styles) |
123 { | 126 { |
124 if (index >= chain.length) | 127 if (index >= chain.length) |
125 { | 128 { |
126 yield prefix; | 129 yield prefix; |
127 return; | 130 return; |
128 } | 131 } |
129 for (let [selector, element] of | 132 for (let [selector, element] of |
130 chain[index].getSelectors(prefix, subtree, styles)) | 133 chain[index].getSelectors(prefix, subtree, styles)) |
131 yield* evaluate(chain, index + 1, selector, element, styles); | 134 { |
| 135 if (selector == null) |
| 136 yield null; |
| 137 else |
| 138 yield* evaluate(chain, index + 1, selector, element, styles); |
| 139 } |
| 140 // Just in case the getSelectors() generator above had to run some heavy |
| 141 // document.querySelectorAll() call which didn't produce any results, make |
| 142 // sure there is at least one point where execution can pause. |
| 143 yield null; |
132 } | 144 } |
133 | 145 |
134 function PlainSelector(selector) | 146 function PlainSelector(selector) |
135 { | 147 { |
136 this._selector = selector; | 148 this._selector = selector; |
137 } | 149 } |
138 | 150 |
139 PlainSelector.prototype = { | 151 PlainSelector.prototype = { |
140 /** | 152 /** |
141 * Generator function returning a pair of selector | 153 * Generator function returning a pair of selector |
(...skipping 39 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
181 *getElements(prefix, subtree, styles) | 193 *getElements(prefix, subtree, styles) |
182 { | 194 { |
183 let actualPrefix = (!prefix || incompletePrefixRegexp.test(prefix)) ? | 195 let actualPrefix = (!prefix || incompletePrefixRegexp.test(prefix)) ? |
184 prefix + "*" : prefix; | 196 prefix + "*" : prefix; |
185 let elements = subtree.querySelectorAll(actualPrefix); | 197 let elements = subtree.querySelectorAll(actualPrefix); |
186 for (let element of elements) | 198 for (let element of elements) |
187 { | 199 { |
188 let iter = evaluate(this._innerSelectors, 0, "", element, styles); | 200 let iter = evaluate(this._innerSelectors, 0, "", element, styles); |
189 for (let selector of iter) | 201 for (let selector of iter) |
190 { | 202 { |
| 203 if (selector == null) |
| 204 { |
| 205 yield null; |
| 206 continue; |
| 207 } |
191 if (relativeSelectorRegexp.test(selector)) | 208 if (relativeSelectorRegexp.test(selector)) |
192 selector = ":scope" + selector; | 209 selector = ":scope" + selector; |
193 try | 210 try |
194 { | 211 { |
195 if (element.querySelector(selector)) | 212 if (element.querySelector(selector)) |
196 yield element; | 213 yield element; |
197 } | 214 } |
198 catch (e) | 215 catch (e) |
199 { | 216 { |
200 // :scope isn't supported on Edge, ignore error caused by it. | 217 // :scope isn't supported on Edge, ignore error caused by it. |
201 } | 218 } |
202 } | 219 } |
| 220 yield null; |
203 } | 221 } |
204 } | 222 } |
205 }; | 223 }; |
206 | 224 |
207 function ContainsSelector(textContent) | 225 function ContainsSelector(textContent) |
208 { | 226 { |
209 this._text = textContent; | 227 this._text = textContent; |
210 } | 228 } |
211 | 229 |
212 ContainsSelector.prototype = { | 230 ContainsSelector.prototype = { |
213 requiresHiding: true, | 231 requiresHiding: true, |
214 | 232 |
215 *getSelectors(prefix, subtree, stylesheet) | 233 *getSelectors(prefix, subtree, stylesheet) |
216 { | 234 { |
217 for (let element of this.getElements(prefix, subtree, stylesheet)) | 235 for (let element of this.getElements(prefix, subtree, stylesheet)) |
218 yield [makeSelector(element, ""), subtree]; | 236 yield [makeSelector(element, ""), subtree]; |
219 }, | 237 }, |
220 | 238 |
221 *getElements(prefix, subtree, stylesheet) | 239 *getElements(prefix, subtree, stylesheet) |
222 { | 240 { |
223 let actualPrefix = (!prefix || incompletePrefixRegexp.test(prefix)) ? | 241 let actualPrefix = (!prefix || incompletePrefixRegexp.test(prefix)) ? |
224 prefix + "*" : prefix; | 242 prefix + "*" : prefix; |
225 let elements = subtree.querySelectorAll(actualPrefix); | 243 let elements = subtree.querySelectorAll(actualPrefix); |
| 244 |
226 for (let element of elements) | 245 for (let element of elements) |
| 246 { |
227 if (element.textContent.includes(this._text)) | 247 if (element.textContent.includes(this._text)) |
228 yield element; | 248 yield element; |
| 249 else |
| 250 yield null; |
| 251 } |
229 } | 252 } |
230 }; | 253 }; |
231 | 254 |
232 function PropsSelector(propertyExpression) | 255 function PropsSelector(propertyExpression) |
233 { | 256 { |
234 let regexpString; | 257 let regexpString; |
235 if (propertyExpression.length >= 2 && propertyExpression[0] == "/" && | 258 if (propertyExpression.length >= 2 && propertyExpression[0] == "/" && |
236 propertyExpression[propertyExpression.length - 1] == "/") | 259 propertyExpression[propertyExpression.length - 1] == "/") |
237 { | 260 { |
238 regexpString = propertyExpression.slice(1, -1) | 261 regexpString = propertyExpression.slice(1, -1) |
(...skipping 27 matching lines...) Expand all Loading... |
266 } | 289 } |
267 }, | 290 }, |
268 | 291 |
269 *getSelectors(prefix, subtree, styles) | 292 *getSelectors(prefix, subtree, styles) |
270 { | 293 { |
271 for (let selector of this.findPropsSelectors(styles, prefix, this._regexp)) | 294 for (let selector of this.findPropsSelectors(styles, prefix, this._regexp)) |
272 yield [selector, subtree]; | 295 yield [selector, subtree]; |
273 } | 296 } |
274 }; | 297 }; |
275 | 298 |
| 299 function isSelectorHidingOnlyPattern(pattern) |
| 300 { |
| 301 return pattern.selectors.some(s => s.preferHideWithSelector) && |
| 302 !pattern.selectors.some(s => s.requiresHiding); |
| 303 } |
| 304 |
276 function ElemHideEmulation(window, getFiltersFunc, addSelectorsFunc, | 305 function ElemHideEmulation(window, getFiltersFunc, addSelectorsFunc, |
277 hideElemsFunc) | 306 hideElemsFunc) |
278 { | 307 { |
279 this.window = window; | 308 this.window = window; |
280 this.getFiltersFunc = getFiltersFunc; | 309 this.getFiltersFunc = getFiltersFunc; |
281 this.addSelectorsFunc = addSelectorsFunc; | 310 this.addSelectorsFunc = addSelectorsFunc; |
282 this.hideElemsFunc = hideElemsFunc; | 311 this.hideElemsFunc = hideElemsFunc; |
| 312 this.observer = new window.MutationObserver(this.observe.bind(this)); |
283 } | 313 } |
284 | 314 |
285 ElemHideEmulation.prototype = { | 315 ElemHideEmulation.prototype = { |
286 isSameOrigin(stylesheet) | 316 isSameOrigin(stylesheet) |
287 { | 317 { |
288 try | 318 try |
289 { | 319 { |
290 return new URL(stylesheet.href).origin == this.window.location.origin; | 320 return new URL(stylesheet.href).origin == this.window.location.origin; |
291 } | 321 } |
292 catch (e) | 322 catch (e) |
(...skipping 62 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
355 { | 385 { |
356 this.window.console.error( | 386 this.window.console.error( |
357 new SyntaxError("Failed to parse Adblock Plus " + | 387 new SyntaxError("Failed to parse Adblock Plus " + |
358 `selector ${selector}, can't ` + | 388 `selector ${selector}, can't ` + |
359 "have a lonely :-abp-contains().")); | 389 "have a lonely :-abp-contains().")); |
360 return null; | 390 return null; |
361 } | 391 } |
362 return selectors; | 392 return selectors; |
363 }, | 393 }, |
364 | 394 |
365 _lastInvocation: 0, | |
366 | |
367 /** | 395 /** |
368 * Processes the current document and applies all rules to it. | 396 * Processes the current document and applies all rules to it. |
369 * @param {CSSStyleSheet[]} [stylesheets] | 397 * @param {CSSStyleSheet[]} [stylesheets] |
370 * The list of new stylesheets that have been added to the document and | 398 * The list of new stylesheets that have been added to the document and |
371 * made reprocessing necessary. This parameter shouldn't be passed in for | 399 * made reprocessing necessary. This parameter shouldn't be passed in for |
372 * the initial processing, all of document's stylesheets will be considered | 400 * the initial processing, all of document's stylesheets will be considered |
373 * then and all rules, including the ones not dependent on styles. | 401 * then and all rules, including the ones not dependent on styles. |
| 402 * @param {function} [done] |
| 403 * Callback to call when done. |
374 */ | 404 */ |
375 addSelectors(stylesheets) | 405 _addSelectors(stylesheets, done) |
376 { | 406 { |
377 this._lastInvocation = Date.now(); | |
378 | |
379 let selectors = []; | 407 let selectors = []; |
380 let selectorFilters = []; | 408 let selectorFilters = []; |
381 | 409 |
382 let elements = []; | 410 let elements = []; |
383 let elementFilters = []; | 411 let elementFilters = []; |
384 | 412 |
385 let cssStyles = []; | 413 let cssStyles = []; |
386 | 414 |
387 let stylesheetOnlyChange = !!stylesheets; | 415 let stylesheetOnlyChange = !!stylesheets; |
388 if (!stylesheets) | 416 if (!stylesheets) |
(...skipping 16 matching lines...) Expand all Loading... |
405 for (let rule of rules) | 433 for (let rule of rules) |
406 { | 434 { |
407 if (rule.type != rule.STYLE_RULE) | 435 if (rule.type != rule.STYLE_RULE) |
408 continue; | 436 continue; |
409 | 437 |
410 cssStyles.push(stringifyStyle(rule)); | 438 cssStyles.push(stringifyStyle(rule)); |
411 } | 439 } |
412 } | 440 } |
413 | 441 |
414 let {document} = this.window; | 442 let {document} = this.window; |
415 for (let pattern of this.patterns) | 443 |
| 444 let patterns = this.patterns.slice(); |
| 445 let pattern = null; |
| 446 let generator = null; |
| 447 |
| 448 let processPatterns = () => |
416 { | 449 { |
417 if (stylesheetOnlyChange && | 450 let cycleStart = this.window.performance.now(); |
418 !pattern.selectors.some(selector => selector.dependsOnStyles)) | 451 |
| 452 if (!pattern) |
419 { | 453 { |
420 continue; | 454 if (!patterns.length) |
| 455 { |
| 456 this.addSelectorsFunc(selectors, selectorFilters); |
| 457 this.hideElemsFunc(elements, elementFilters); |
| 458 if (typeof done == "function") |
| 459 done(); |
| 460 return; |
| 461 } |
| 462 |
| 463 pattern = patterns.shift(); |
| 464 |
| 465 if (stylesheetOnlyChange && |
| 466 !pattern.selectors.some(selector => selector.dependsOnStyles)) |
| 467 { |
| 468 pattern = null; |
| 469 return processPatterns(); |
| 470 } |
| 471 generator = evaluate(pattern.selectors, 0, "", document, cssStyles); |
421 } | 472 } |
422 | 473 for (let selector of generator) |
423 for (let selector of evaluate(pattern.selectors, | |
424 0, "", document, cssStyles)) | |
425 { | 474 { |
426 if (pattern.selectors.some(s => s.preferHideWithSelector) && | 475 if (selector != null) |
427 !pattern.selectors.some(s => s.requiresHiding)) | |
428 { | 476 { |
429 selectors.push(selector); | 477 if (isSelectorHidingOnlyPattern(pattern)) |
430 selectorFilters.push(pattern.text); | |
431 } | |
432 else | |
433 { | |
434 for (let element of document.querySelectorAll(selector)) | |
435 { | 478 { |
436 elements.push(element); | 479 selectors.push(selector); |
437 elementFilters.push(pattern.text); | 480 selectorFilters.push(pattern.text); |
| 481 } |
| 482 else |
| 483 { |
| 484 for (let element of document.querySelectorAll(selector)) |
| 485 { |
| 486 elements.push(element); |
| 487 elementFilters.push(pattern.text); |
| 488 } |
438 } | 489 } |
439 } | 490 } |
| 491 if (this.window.performance.now() - |
| 492 cycleStart > MAX_SYNCHRONOUS_PROCESSING_TIME) |
| 493 { |
| 494 this.window.setTimeout(processPatterns, 0); |
| 495 return; |
| 496 } |
440 } | 497 } |
441 } | 498 pattern = null; |
| 499 return processPatterns(); |
| 500 }; |
442 | 501 |
443 this.addSelectorsFunc(selectors, selectorFilters); | 502 processPatterns(); |
444 this.hideElemsFunc(elements, elementFilters); | |
445 }, | 503 }, |
446 | 504 |
447 _stylesheetQueue: null, | 505 // This property is only used in the tests |
| 506 // to shorten the invocation interval |
| 507 get MIN_INVOCATION_INTERVAL() |
| 508 { |
| 509 return MIN_INVOCATION_INTERVAL; |
| 510 }, |
| 511 |
| 512 set MIN_INVOCATION_INTERVAL(interval) |
| 513 { |
| 514 MIN_INVOCATION_INTERVAL = interval; |
| 515 }, |
| 516 |
| 517 _filteringInProgress: false, |
| 518 _lastInvocation: -MIN_INVOCATION_INTERVAL, |
| 519 _scheduledProcessing: null, |
| 520 |
| 521 /** |
| 522 * Re-run filtering either immediately or queued. |
| 523 * @param {CSSStyleSheet[]} [stylesheets] |
| 524 * new stylesheets to be processed. This parameter should be omitted |
| 525 * for DOM modification (full reprocessing required). |
| 526 */ |
| 527 queueFiltering(stylesheets) |
| 528 { |
| 529 let completion = () => |
| 530 { |
| 531 this._lastInvocation = this.window.performance.now(); |
| 532 this._filteringInProgress = false; |
| 533 if (this._scheduledProcessing) |
| 534 { |
| 535 let newStylesheets = this._scheduledProcessing.stylesheets; |
| 536 this._scheduledProcessing = null; |
| 537 this.queueFiltering(newStylesheets); |
| 538 } |
| 539 }; |
| 540 |
| 541 if (this._scheduledProcessing) |
| 542 { |
| 543 if (!stylesheets) |
| 544 this._scheduledProcessing.stylesheets = null; |
| 545 else if (this._scheduledProcessing.stylesheets) |
| 546 this._scheduledProcessing.stylesheets.push(...stylesheets); |
| 547 } |
| 548 else if (this._filteringInProgress) |
| 549 { |
| 550 this._scheduledProcessing = {stylesheets}; |
| 551 } |
| 552 else if (this.window.performance.now() - |
| 553 this._lastInvocation < MIN_INVOCATION_INTERVAL) |
| 554 { |
| 555 this._scheduledProcessing = {stylesheets}; |
| 556 this.window.setTimeout(() => |
| 557 { |
| 558 let newStylesheets = this._scheduledProcessing.stylesheets; |
| 559 this._filteringInProgress = true; |
| 560 this._scheduledProcessing = null; |
| 561 this._addSelectors(newStylesheets, completion); |
| 562 }, |
| 563 MIN_INVOCATION_INTERVAL - |
| 564 (this.window.performance.now() - this._lastInvocation)); |
| 565 } |
| 566 else |
| 567 { |
| 568 this._filteringInProgress = true; |
| 569 this._addSelectors(stylesheets, completion); |
| 570 } |
| 571 }, |
448 | 572 |
449 onLoad(event) | 573 onLoad(event) |
450 { | 574 { |
451 let stylesheet = event.target.sheet; | 575 let stylesheet = event.target.sheet; |
452 if (stylesheet) | 576 if (stylesheet) |
453 { | 577 this.queueFiltering([stylesheet]); |
454 if (!this._stylesheetQueue && | 578 }, |
455 Date.now() - this._lastInvocation < MIN_INVOCATION_INTERVAL) | |
456 { | |
457 this._stylesheetQueue = []; | |
458 this.window.setTimeout(() => | |
459 { | |
460 let stylesheets = this._stylesheetQueue; | |
461 this._stylesheetQueue = null; | |
462 this.addSelectors(stylesheets); | |
463 }, MIN_INVOCATION_INTERVAL - (Date.now() - this._lastInvocation)); | |
464 } | |
465 | 579 |
466 if (this._stylesheetQueue) | 580 observe(mutations) |
467 this._stylesheetQueue.push(stylesheet); | 581 { |
468 else | 582 this.queueFiltering(); |
469 this.addSelectors([stylesheet]); | |
470 } | |
471 }, | 583 }, |
472 | 584 |
473 apply() | 585 apply() |
474 { | 586 { |
475 this.getFiltersFunc(patterns => | 587 this.getFiltersFunc(patterns => |
476 { | 588 { |
477 this.patterns = []; | 589 this.patterns = []; |
478 for (let pattern of patterns) | 590 for (let pattern of patterns) |
479 { | 591 { |
480 let selectors = this.parseSelector(pattern.selector); | 592 let selectors = this.parseSelector(pattern.selector); |
481 if (selectors != null && selectors.length > 0) | 593 if (selectors != null && selectors.length > 0) |
482 this.patterns.push({selectors, text: pattern.text}); | 594 this.patterns.push({selectors, text: pattern.text}); |
483 } | 595 } |
484 | 596 |
485 if (this.patterns.length > 0) | 597 if (this.patterns.length > 0) |
486 { | 598 { |
487 let {document} = this.window; | 599 let {document} = this.window; |
488 this.addSelectors(); | 600 this.queueFiltering(); |
| 601 this.observer.observe( |
| 602 document, |
| 603 { |
| 604 childList: true, |
| 605 attributes: true, |
| 606 characterData: true, |
| 607 subtree: true |
| 608 } |
| 609 ); |
489 document.addEventListener("load", this.onLoad.bind(this), true); | 610 document.addEventListener("load", this.onLoad.bind(this), true); |
490 } | 611 } |
491 }); | 612 }); |
492 } | 613 } |
493 }; | 614 }; |
494 | 615 |
495 exports.ElemHideEmulation = ElemHideEmulation; | 616 exports.ElemHideEmulation = ElemHideEmulation; |
OLD | NEW |