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 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 pair ot setter/getter is only used in the tests to |
Wladimir Palant
2017/08/25 20:57:41
Nit: "This property is only used" (simpler and cap
hub
2017/08/26 01:45:06
Done.
| |
506 // 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 |