| 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 |