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