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 |