Left: | ||
Right: |
LEFT | RIGHT |
---|---|
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 {textToRegExp, filterToRegExp, splitSelector} = require("../common"); | 20 const {textToRegExp, filterToRegExp, splitSelector} = require("../common"); |
21 const {indexOf} = require("../coreUtils"); | |
21 | 22 |
22 let MIN_INVOCATION_INTERVAL = 3000; | 23 let MIN_INVOCATION_INTERVAL = 3000; |
23 const MAX_SYNCHRONOUS_PROCESSING_TIME = 50; | 24 const MAX_SYNCHRONOUS_PROCESSING_TIME = 50; |
24 const abpSelectorRegexp = /:-abp-([\w-]+)\(/i; | 25 const abpSelectorRegexp = /:-abp-([\w-]+)\(/i; |
26 | |
27 let testInfo = null; | |
28 | |
29 function setTestMode() | |
30 { | |
31 testInfo = { | |
32 lastProcessedElements: new Set() | |
33 }; | |
34 } | |
35 | |
36 exports.setTestMode = setTestMode; | |
37 | |
38 function getTestInfo() | |
39 { | |
40 return testInfo; | |
41 } | |
42 | |
43 exports.getTestInfo = getTestInfo; | |
hub
2018/05/24 19:19:09
I'm a bit skeptical with that way or test, includi
Manish Jethani
2018/05/25 07:21:25
We can work on improving our test framework, but f
| |
44 | |
45 function getCachedPropertyValue(object, name, defaultValueFunc = () => {}) | |
46 { | |
47 let value = object[name]; | |
48 if (typeof value == "undefined") | |
49 Object.defineProperty(object, name, {value: value = defaultValueFunc()}); | |
50 return value; | |
51 } | |
25 | 52 |
26 /** Return position of node from parent. | 53 /** Return position of node from parent. |
27 * @param {Node} node the node to find the position of. | 54 * @param {Node} node the node to find the position of. |
28 * @return {number} One-based index like for :nth-child(), or 0 on error. | 55 * @return {number} One-based index like for :nth-child(), or 0 on error. |
29 */ | 56 */ |
30 function positionInParent(node) | 57 function positionInParent(node) |
31 { | 58 { |
32 let {children} = node.parentNode; | 59 return indexOf(node.parentNode.children, node) + 1; |
33 for (let i = 0; i < children.length; i++) | 60 } |
34 if (children[i] == node) | 61 |
35 return i + 1; | 62 function makeSelector(node, selector = "") |
36 return 0; | |
37 } | |
38 | |
39 function makeSelector(node, selector) | |
40 { | 63 { |
41 if (node == null) | 64 if (node == null) |
42 return null; | 65 return null; |
43 if (!node.parentElement) | 66 if (!node.parentElement) |
44 { | 67 { |
45 let newSelector = ":root"; | 68 let newSelector = ":root"; |
46 if (selector) | 69 if (selector) |
47 newSelector += " > " + selector; | 70 newSelector += " > " + selector; |
48 return newSelector; | 71 return newSelector; |
49 } | 72 } |
(...skipping 114 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
164 } | 187 } |
165 return all ? subtree.querySelectorAll(selector) : | 188 return all ? subtree.querySelectorAll(selector) : |
166 subtree.querySelector(selector); | 189 subtree.querySelector(selector); |
167 } | 190 } |
168 | 191 |
169 function scopedQuerySelectorAll(subtree, selector) | 192 function scopedQuerySelectorAll(subtree, selector) |
170 { | 193 { |
171 return scopedQuerySelector(subtree, selector, true); | 194 return scopedQuerySelector(subtree, selector, true); |
172 } | 195 } |
173 | 196 |
174 const regexpRegexp = /^\/(.*)\/([im]*)$/; | 197 const regexpRegexp = /^\/(.*)\/([imu]*)$/; |
175 | 198 |
176 /** | 199 /** |
177 * Make a regular expression from a text argument. If it can be parsed as a | 200 * Make a regular expression from a text argument. If it can be parsed as a |
178 * regular expression, parse it and the flags. | 201 * regular expression, parse it and the flags. |
179 * @param {string} text the text argument. | 202 * @param {string} text the text argument. |
180 * @return {?RegExp} a RegExp object or null in case of error. | 203 * @return {?RegExp} a RegExp object or null in case of error. |
181 */ | 204 */ |
182 function makeRegExpParameter(text) | 205 function makeRegExpParameter(text) |
183 { | 206 { |
184 let [, pattern, flags] = | 207 let [, pattern, flags] = |
(...skipping 27 matching lines...) Expand all Loading... | |
212 // Just in case the getSelectors() generator above had to run some heavy | 235 // Just in case the getSelectors() generator above had to run some heavy |
213 // document.querySelectorAll() call which didn't produce any results, make | 236 // document.querySelectorAll() call which didn't produce any results, make |
214 // sure there is at least one point where execution can pause. | 237 // sure there is at least one point where execution can pause. |
215 yield null; | 238 yield null; |
216 } | 239 } |
217 | 240 |
218 function PlainSelector(selector) | 241 function PlainSelector(selector) |
219 { | 242 { |
220 this._selector = selector; | 243 this._selector = selector; |
221 this.maybeDependsOnAttributes = /[#.]|\[.+\]/.test(selector); | 244 this.maybeDependsOnAttributes = /[#.]|\[.+\]/.test(selector); |
245 this.dependsOnDOM = this.maybeDependsOnAttributes; | |
246 this.maybeContainsSiblingCombinators = /[~+]/.test(selector); | |
222 } | 247 } |
223 | 248 |
224 PlainSelector.prototype = { | 249 PlainSelector.prototype = { |
225 /** | 250 /** |
226 * Generator function returning a pair of selector | 251 * Generator function returning a pair of selector |
227 * string and subtree. | 252 * string and subtree. |
228 * @param {string} prefix the prefix for the selector. | 253 * @param {string} prefix the prefix for the selector. |
229 * @param {Node} subtree the subtree we work on. | 254 * @param {Node} subtree the subtree we work on. |
230 * @param {StringifiedStyle[]} styles the stringified style objects. | 255 * @param {StringifiedStyle[]} styles the stringified style objects. |
231 * @param {Node[]} [targets] the nodes we are interested in. | 256 * @param {Node[]} [targets] the nodes we are interested in. |
232 */ | 257 */ |
233 *getSelectors(prefix, subtree, styles, targets) | 258 *getSelectors(prefix, subtree, styles, targets) |
234 { | 259 { |
235 yield [prefix + this._selector, subtree]; | 260 yield [prefix + this._selector, subtree]; |
236 } | 261 } |
237 }; | 262 }; |
238 | 263 |
239 const incompletePrefixRegexp = /[\s>+~]$/; | 264 const incompletePrefixRegexp = /[\s>+~]$/; |
240 | 265 |
241 function HasSelector(selectors) | 266 function HasSelector(selectors) |
242 { | 267 { |
243 this._innerSelectors = selectors; | 268 this._innerSelectors = selectors; |
244 } | 269 } |
245 | 270 |
246 HasSelector.prototype = { | 271 HasSelector.prototype = { |
247 requiresHiding: true, | |
248 dependsOnDOM: true, | 272 dependsOnDOM: true, |
249 | 273 |
250 get dependsOnStyles() | 274 get dependsOnStyles() |
251 { | 275 { |
252 return this._innerSelectors.some(selector => selector.dependsOnStyles); | 276 return this._innerSelectors.some(selector => selector.dependsOnStyles); |
253 }, | 277 }, |
254 | 278 |
255 get dependsOnCharacterData() | 279 get dependsOnCharacterData() |
256 { | 280 { |
257 return this._innerSelectors.some( | 281 return this._innerSelectors.some( |
258 selector => selector.dependsOnCharacterData | 282 selector => selector.dependsOnCharacterData |
259 ); | 283 ); |
260 }, | 284 }, |
261 | 285 |
262 get maybeDependsOnAttributes() | 286 get maybeDependsOnAttributes() |
263 { | 287 { |
264 return this._innerSelectors.some( | 288 return this._innerSelectors.some( |
265 selector => selector.maybeDependsOnAttributes | 289 selector => selector.maybeDependsOnAttributes |
266 ); | 290 ); |
267 }, | 291 }, |
268 | 292 |
269 *getSelectors(prefix, subtree, styles, targets) | 293 *getSelectors(prefix, subtree, styles, targets) |
270 { | 294 { |
271 for (let element of this.getElements(prefix, subtree, styles, targets)) | 295 for (let element of this.getElements(prefix, subtree, styles, targets)) |
272 yield [makeSelector(element, ""), element]; | 296 yield [makeSelector(element), element]; |
273 }, | 297 }, |
274 | 298 |
275 /** | 299 /** |
276 * Generator function returning selected elements. | 300 * Generator function returning selected elements. |
277 * @param {string} prefix the prefix for the selector. | 301 * @param {string} prefix the prefix for the selector. |
278 * @param {Node} subtree the subtree we work on. | 302 * @param {Node} subtree the subtree we work on. |
279 * @param {StringifiedStyle[]} styles the stringified style objects. | 303 * @param {StringifiedStyle[]} styles the stringified style objects. |
280 * @param {Node[]} [targets] the nodes we are interested in. | 304 * @param {Node[]} [targets] the nodes we are interested in. |
281 */ | 305 */ |
282 *getElements(prefix, subtree, styles, targets) | 306 *getElements(prefix, subtree, styles, targets) |
(...skipping 17 matching lines...) Expand all Loading... | |
300 let iter = evaluate(this._innerSelectors, 0, "", element, styles, | 324 let iter = evaluate(this._innerSelectors, 0, "", element, styles, |
301 targets); | 325 targets); |
302 for (let selector of iter) | 326 for (let selector of iter) |
303 { | 327 { |
304 if (selector == null) | 328 if (selector == null) |
305 yield null; | 329 yield null; |
306 else if (scopedQuerySelector(element, selector)) | 330 else if (scopedQuerySelector(element, selector)) |
307 yield element; | 331 yield element; |
308 } | 332 } |
309 yield null; | 333 yield null; |
334 | |
335 if (testInfo) | |
336 testInfo.lastProcessedElements.add(element); | |
310 } | 337 } |
311 } | 338 } |
312 } | 339 } |
313 }; | 340 }; |
314 | 341 |
315 function ContainsSelector(textContent) | 342 function ContainsSelector(textContent) |
316 { | 343 { |
317 this._regexp = makeRegExpParameter(textContent); | 344 this._regexp = makeRegExpParameter(textContent); |
318 } | 345 } |
319 | 346 |
320 ContainsSelector.prototype = { | 347 ContainsSelector.prototype = { |
321 requiresHiding: true, | |
322 dependsOnDOM: true, | 348 dependsOnDOM: true, |
323 dependsOnCharacterData: true, | 349 dependsOnCharacterData: true, |
324 | 350 |
325 *getSelectors(prefix, subtree, styles, targets) | 351 *getSelectors(prefix, subtree, styles, targets) |
326 { | 352 { |
327 for (let element of this.getElements(prefix, subtree, styles, targets)) | 353 for (let element of this.getElements(prefix, subtree, styles, targets)) |
328 yield [makeSelector(element, ""), subtree]; | 354 yield [makeSelector(element), subtree]; |
329 }, | 355 }, |
330 | 356 |
331 *getElements(prefix, subtree, styles, targets) | 357 *getElements(prefix, subtree, styles, targets) |
332 { | 358 { |
333 let actualPrefix = (!prefix || incompletePrefixRegexp.test(prefix)) ? | 359 let actualPrefix = (!prefix || incompletePrefixRegexp.test(prefix)) ? |
334 prefix + "*" : prefix; | 360 prefix + "*" : prefix; |
335 | 361 |
336 let elements = scopedQuerySelectorAll(subtree, actualPrefix); | 362 let elements = scopedQuerySelectorAll(subtree, actualPrefix); |
363 | |
337 if (elements) | 364 if (elements) |
338 { | 365 { |
366 let lastRoot = null; | |
339 for (let element of elements) | 367 for (let element of elements) |
340 { | 368 { |
369 // For a filter like div:-abp-contains(Hello) and a subtree like | |
370 // <div id="a"><div id="b"><div id="c">Hello</div></div></div> | |
371 // we're only interested in div#a | |
372 if (lastRoot && lastRoot.contains(element)) | |
373 { | |
374 yield null; | |
375 continue; | |
376 } | |
377 | |
378 lastRoot = element; | |
379 | |
341 if (targets && !targets.some(target => element.contains(target) || | 380 if (targets && !targets.some(target => element.contains(target) || |
342 target.contains(element))) | 381 target.contains(element))) |
343 { | 382 { |
344 yield null; | 383 yield null; |
345 continue; | 384 continue; |
346 } | 385 } |
347 | 386 |
348 if (this._regexp && this._regexp.test(element.textContent)) | 387 if (this._regexp && this._regexp.test(element.textContent)) |
349 yield element; | 388 yield element; |
350 else | 389 else |
351 yield null; | 390 yield null; |
391 | |
392 if (testInfo) | |
393 testInfo.lastProcessedElements.add(element); | |
352 } | 394 } |
353 } | 395 } |
354 } | 396 } |
355 }; | 397 }; |
356 | 398 |
357 function PropsSelector(propertyExpression) | 399 function PropsSelector(propertyExpression) |
358 { | 400 { |
359 let regexpString; | 401 let regexpString; |
360 if (propertyExpression.length >= 2 && propertyExpression[0] == "/" && | 402 if (propertyExpression.length >= 2 && propertyExpression[0] == "/" && |
361 propertyExpression[propertyExpression.length - 1] == "/") | 403 propertyExpression[propertyExpression.length - 1] == "/") |
362 { | 404 { |
363 regexpString = propertyExpression.slice(1, -1) | 405 regexpString = propertyExpression.slice(1, -1) |
364 .replace("\\7B ", "{").replace("\\7D ", "}"); | 406 .replace("\\7B ", "{").replace("\\7D ", "}"); |
365 } | 407 } |
366 else | 408 else |
367 regexpString = filterToRegExp(propertyExpression); | 409 regexpString = filterToRegExp(propertyExpression); |
368 | 410 |
369 this._regexp = new RegExp(regexpString, "i"); | 411 this._regexp = new RegExp(regexpString, "i"); |
370 } | 412 } |
371 | 413 |
372 PropsSelector.prototype = { | 414 PropsSelector.prototype = { |
373 preferHideWithSelector: true, | |
374 dependsOnStyles: true, | 415 dependsOnStyles: true, |
375 | 416 |
376 *findPropsSelectors(styles, prefix, regexp) | 417 *findPropsSelectors(styles, prefix, regexp) |
377 { | 418 { |
378 for (let style of styles) | 419 for (let style of styles) |
379 if (regexp.test(style.style)) | 420 if (regexp.test(style.style)) |
380 for (let subSelector of style.subSelectors) | 421 for (let subSelector of style.subSelectors) |
381 { | 422 { |
382 if (subSelector.startsWith("*") && | 423 if (subSelector.startsWith("*") && |
383 !incompletePrefixRegexp.test(prefix)) | 424 !incompletePrefixRegexp.test(prefix)) |
384 { | 425 { |
385 subSelector = subSelector.substr(1); | 426 subSelector = subSelector.substr(1); |
386 } | 427 } |
387 let idx = subSelector.lastIndexOf("::"); | 428 let idx = subSelector.lastIndexOf("::"); |
388 if (idx != -1) | 429 if (idx != -1) |
389 subSelector = subSelector.substr(0, idx); | 430 subSelector = subSelector.substr(0, idx); |
390 yield prefix + subSelector; | 431 yield prefix + subSelector; |
391 } | 432 } |
392 }, | 433 }, |
393 | 434 |
394 *getSelectors(prefix, subtree, styles, targets) | 435 *getSelectors(prefix, subtree, styles, targets) |
395 { | 436 { |
396 for (let selector of this.findPropsSelectors(styles, prefix, this._regexp)) | 437 for (let selector of this.findPropsSelectors(styles, prefix, this._regexp)) |
397 yield [selector, subtree]; | 438 yield [selector, subtree]; |
398 } | 439 } |
399 }; | 440 }; |
400 | 441 |
401 function isSelectorHidingOnlyPattern(pattern) | 442 function Pattern(selectors, text) |
402 { | 443 { |
403 return pattern.selectors.some(s => s.preferHideWithSelector) && | 444 this.selectors = selectors; |
404 !pattern.selectors.some(s => s.requiresHiding); | 445 this.text = text; |
405 } | 446 } |
406 | 447 |
407 function patternDependsOnStyles(pattern) | 448 Pattern.prototype = { |
408 { | 449 get dependsOnStyles() |
409 return pattern.selectors.some(s => s.dependsOnStyles); | 450 { |
410 } | 451 return getCachedPropertyValue( |
411 | 452 this, "_dependsOnStyles", |
412 function patternDependsOnDOM(pattern) | 453 () => this.selectors.some(selector => selector.dependsOnStyles) |
413 { | 454 ); |
414 return pattern.selectors.some(s => s.dependsOnDOM); | 455 }, |
415 } | 456 |
416 | 457 get dependsOnDOM() |
417 function patternDependsOnStylesAndDOM(pattern) | 458 { |
418 { | 459 return getCachedPropertyValue( |
419 return pattern.selectors.some(s => s.dependsOnStyles && s.dependsOnDOM); | 460 this, "_dependsOnDOM", |
420 } | 461 () => this.selectors.some(selector => selector.dependsOnDOM) |
421 | 462 ); |
422 function patternMaybeDependsOnAttributes(pattern) | 463 }, |
423 { | 464 |
424 // Observe changes to attributes if either there's a plain selector that | 465 get dependsOnStylesAndDOM() |
425 // looks like an ID selector, class selector, or attribute selector in one of | 466 { |
426 // the patterns (e.g. "a[href='https://example.com/']") | 467 return getCachedPropertyValue( |
427 // or there's a properties selector nested inside a has selector | 468 this, "_dependsOnStylesAndDOM", |
428 // (e.g. "div:-abp-has(:-abp-properties(color: blue))") | 469 () => this.selectors.some(selector => selector.dependsOnStyles && |
429 return pattern.selectors.some( | 470 selector.dependsOnDOM) |
430 selector => selector.maybeDependsOnAttributes || | 471 ); |
431 (selector instanceof HasSelector && | 472 }, |
432 selector.dependsOnStyles) | 473 |
433 ); | 474 get maybeDependsOnAttributes() |
434 } | 475 { |
435 | 476 // Observe changes to attributes if either there's a plain selector that |
436 function patternDependsOnCharacterData(pattern) | 477 // looks like an ID selector, class selector, or attribute selector in one |
437 { | 478 // of the patterns (e.g. "a[href='https://example.com/']") |
438 // Observe changes to character data only if there's a contains selector in | 479 // or there's a properties selector nested inside a has selector |
439 // one of the patterns. | 480 // (e.g. "div:-abp-has(:-abp-properties(color: blue))") |
440 return pattern.selectors.some(selector => selector.dependsOnCharacterData); | 481 return getCachedPropertyValue( |
441 } | 482 this, "_maybeDependsOnAttributes", |
442 | 483 () => this.selectors.some( |
443 function patternMatchesMutationTypes(pattern, mutationTypes) | 484 selector => selector.maybeDependsOnAttributes || |
444 { | 485 (selector instanceof HasSelector && |
445 return mutationTypes.has("childList") || | 486 selector.dependsOnStyles) |
446 (mutationTypes.has("attributes") && | 487 ) |
447 patternMaybeDependsOnAttributes(pattern)) || | 488 ); |
448 (mutationTypes.has("characterData") && | 489 }, |
449 patternDependsOnCharacterData(pattern)); | 490 |
450 } | 491 get dependsOnCharacterData() |
492 { | |
493 // Observe changes to character data only if there's a contains selector in | |
494 // one of the patterns. | |
495 return getCachedPropertyValue( | |
496 this, "_dependsOnCharacterData", | |
497 () => this.selectors.some(selector => selector.dependsOnCharacterData) | |
498 ); | |
499 }, | |
500 | |
501 get maybeContainsSiblingCombinators() | |
502 { | |
503 return getCachedPropertyValue( | |
504 this, "_maybeContainsSiblingCombinators", | |
505 () => this.selectors.some(selector => | |
506 selector.maybeContainsSiblingCombinators) | |
507 ); | |
508 }, | |
509 | |
510 matchesMutationTypes(mutationTypes) | |
511 { | |
512 let mutationTypeMatchMap = getCachedPropertyValue( | |
513 this, "_mutationTypeMatchMap", | |
514 () => new Map([ | |
515 // All types of DOM-dependent patterns are affected by mutations of | |
516 // type "childList". | |
517 ["childList", true], | |
518 ["attributes", this.maybeDependsOnAttributes], | |
519 ["characterData", this.dependsOnCharacterData] | |
520 ]) | |
521 ); | |
522 | |
523 for (let mutationType of mutationTypes) | |
524 { | |
525 if (mutationTypeMatchMap.get(mutationType)) | |
526 return true; | |
527 } | |
528 | |
529 return false; | |
530 } | |
531 }; | |
451 | 532 |
452 function extractMutationTypes(mutations) | 533 function extractMutationTypes(mutations) |
453 { | 534 { |
454 let types = new Set(); | 535 let types = new Set(); |
455 | 536 |
456 for (let mutation of mutations) | 537 for (let mutation of mutations) |
457 { | 538 { |
458 types.add(mutation.type); | 539 types.add(mutation.type); |
459 | 540 |
460 // There are only 3 types of mutations: "attributes", "characterData", and | 541 // There are only 3 types of mutations: "attributes", "characterData", and |
(...skipping 13 matching lines...) Expand all Loading... | |
474 let targets = new Set(); | 555 let targets = new Set(); |
475 | 556 |
476 for (let mutation of mutations) | 557 for (let mutation of mutations) |
477 { | 558 { |
478 if (mutation.type == "childList") | 559 if (mutation.type == "childList") |
479 { | 560 { |
480 // When new nodes are added, we're interested in the added nodes rather | 561 // When new nodes are added, we're interested in the added nodes rather |
481 // than the parent. | 562 // than the parent. |
482 for (let node of mutation.addedNodes) | 563 for (let node of mutation.addedNodes) |
483 targets.add(node); | 564 targets.add(node); |
484 | |
485 // Ideally we would also be interested in removed nodes, but since we | |
486 // never unhide an element once hidden we can simply ignore any removed | |
487 // nodes. Note that this will change once we start using CSS selectors | |
488 // for -abp-has and -abp-contains, i.e. we'll have to remove the | |
489 // selectors for any removed nodes. | |
490 } | 565 } |
491 else | 566 else |
492 { | 567 { |
493 targets.add(mutation.target); | 568 targets.add(mutation.target); |
494 } | 569 } |
495 } | 570 } |
496 | 571 |
497 return [...targets]; | 572 return [...targets]; |
498 } | 573 } |
499 | 574 |
500 function filterPatterns(patterns, {stylesheets, mutations}) | 575 function filterPatterns(patterns, {stylesheets, mutations}) |
501 { | 576 { |
502 if (!stylesheets && !mutations) | 577 if (!stylesheets && !mutations) |
503 return patterns.slice(); | 578 return patterns.slice(); |
504 | 579 |
505 let mutationTypes = mutations ? extractMutationTypes(mutations) : null; | 580 let mutationTypes = mutations ? extractMutationTypes(mutations) : null; |
506 | 581 |
507 return patterns.filter( | 582 return patterns.filter( |
508 pattern => (stylesheets && patternDependsOnStyles(pattern)) || | 583 pattern => (stylesheets && pattern.dependsOnStyles) || |
509 (mutations && patternDependsOnDOM(pattern) && | 584 (mutations && pattern.dependsOnDOM && |
510 patternMatchesMutationTypes(pattern, mutationTypes)) | 585 pattern.matchesMutationTypes(mutationTypes)) |
511 ); | 586 ); |
512 } | 587 } |
513 | 588 |
514 function shouldObserveAttributes(patterns) | 589 function shouldObserveAttributes(patterns) |
515 { | 590 { |
516 return patterns.some(patternMaybeDependsOnAttributes); | 591 return patterns.some(pattern => pattern.maybeDependsOnAttributes); |
517 } | 592 } |
518 | 593 |
519 function shouldObserveCharacterData(patterns) | 594 function shouldObserveCharacterData(patterns) |
520 { | 595 { |
521 return patterns.some(patternDependsOnCharacterData); | 596 return patterns.some(pattern => pattern.dependsOnCharacterData); |
522 } | 597 } |
523 | 598 |
524 function ElemHideEmulation(addSelectorsFunc, hideElemsFunc) | 599 function ElemHideEmulation(addSelectorsFunc, hideElemsFunc) |
525 { | 600 { |
526 this.document = document; | 601 this.document = document; |
527 this.addSelectorsFunc = addSelectorsFunc; | 602 this.addSelectorsFunc = addSelectorsFunc; |
528 this.hideElemsFunc = hideElemsFunc; | 603 this.hideElemsFunc = hideElemsFunc; |
529 this.observer = new MutationObserver(this.observe.bind(this)); | 604 this.observer = new MutationObserver(this.observe.bind(this)); |
605 this.useInlineStyles = true; | |
530 } | 606 } |
531 | 607 |
532 ElemHideEmulation.prototype = { | 608 ElemHideEmulation.prototype = { |
533 isSameOrigin(stylesheet) | 609 isSameOrigin(stylesheet) |
534 { | 610 { |
535 try | 611 try |
536 { | 612 { |
537 return new URL(stylesheet.href).origin == this.document.location.origin; | 613 return new URL(stylesheet.href).origin == this.document.location.origin; |
538 } | 614 } |
539 catch (e) | 615 catch (e) |
(...skipping 76 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
616 * @param {MutationRecord[]} [mutations] | 692 * @param {MutationRecord[]} [mutations] |
617 * The list of DOM mutations that have been applied to the document and | 693 * The list of DOM mutations that have been applied to the document and |
618 * made reprocessing necessary. This parameter shouldn't be passed in for | 694 * made reprocessing necessary. This parameter shouldn't be passed in for |
619 * the initial processing, the entire document will be considered | 695 * the initial processing, the entire document will be considered |
620 * then and all rules, including the ones not dependent on the DOM. | 696 * then and all rules, including the ones not dependent on the DOM. |
621 * @param {function} [done] | 697 * @param {function} [done] |
622 * Callback to call when done. | 698 * Callback to call when done. |
623 */ | 699 */ |
624 _addSelectors(stylesheets, mutations, done) | 700 _addSelectors(stylesheets, mutations, done) |
625 { | 701 { |
702 if (testInfo) | |
703 testInfo.lastProcessedElements.clear(); | |
704 | |
626 let patterns = filterPatterns(this.patterns, {stylesheets, mutations}); | 705 let patterns = filterPatterns(this.patterns, {stylesheets, mutations}); |
627 | 706 |
628 let selectors = []; | 707 let selectors = []; |
629 let selectorFilters = []; | 708 let selectorFilters = []; |
630 | 709 |
631 let elements = []; | 710 let elements = []; |
632 let elementFilters = []; | 711 let elementFilters = []; |
633 | 712 |
634 let cssStyles = []; | 713 let cssStyles = []; |
635 | 714 |
636 // If neither any style sheets nor any DOM mutations have been specified, | 715 // If neither any style sheets nor any DOM mutations have been specified, |
637 // do full processing. | 716 // do full processing. |
638 if (!stylesheets && !mutations) | 717 if (!stylesheets && !mutations) |
639 stylesheets = this.document.styleSheets; | 718 stylesheets = this.document.styleSheets; |
640 | 719 |
641 // If there are any DOM mutations and any of the patterns depends on both | 720 // If there are any DOM mutations and any of the patterns depends on both |
642 // style sheets and the DOM (e.g. -abp-has(-abp-properties)), find all the | 721 // style sheets and the DOM (e.g. -abp-has(-abp-properties)), find all the |
643 // rules in every style sheet in the document, because we need to run | 722 // rules in every style sheet in the document, because we need to run |
644 // querySelectorAll afterwards. On the other hand, if we only have patterns | 723 // querySelectorAll afterwards. On the other hand, if we only have patterns |
645 // that depend on either styles or DOM both not both | 724 // that depend on either styles or DOM both not both |
646 // (e.g. -abp-properties or -abp-contains), we can skip this part. | 725 // (e.g. -abp-properties or -abp-contains), we can skip this part. |
647 if (mutations && patterns.some(patternDependsOnStylesAndDOM)) | 726 if (mutations && patterns.some(pattern => pattern.dependsOnStylesAndDOM)) |
648 stylesheets = this.document.styleSheets; | 727 stylesheets = this.document.styleSheets; |
649 | 728 |
650 for (let stylesheet of stylesheets || []) | 729 for (let stylesheet of stylesheets || []) |
651 { | 730 { |
652 // Explicitly ignore third-party stylesheets to ensure consistent behavior | 731 // Explicitly ignore third-party stylesheets to ensure consistent behavior |
653 // between Firefox and Chrome. | 732 // between Firefox and Chrome. |
654 if (!this.isSameOrigin(stylesheet)) | 733 if (!this.isSameOrigin(stylesheet)) |
655 continue; | 734 continue; |
656 | 735 |
657 let rules = stylesheet.cssRules; | 736 let rules; |
737 try | |
738 { | |
739 rules = stylesheet.cssRules; | |
740 } | |
741 catch (e) | |
742 { | |
743 // On Firefox, there is a chance that an InvalidAccessError | |
744 // get thrown when accessing cssRules. Just skip the stylesheet | |
745 // in that case. | |
746 // See https://searchfox.org/mozilla-central/rev/f65d7528e34ef1a7665b4a1 a7b7cdb1388fcd3aa/layout/style/StyleSheet.cpp#699 | |
747 continue; | |
748 } | |
749 | |
658 if (!rules) | 750 if (!rules) |
659 continue; | 751 continue; |
660 | 752 |
661 for (let rule of rules) | 753 for (let rule of rules) |
662 { | 754 { |
663 if (rule.type != rule.STYLE_RULE) | 755 if (rule.type != rule.STYLE_RULE) |
664 continue; | 756 continue; |
665 | 757 |
666 cssStyles.push(stringifyStyle(rule)); | 758 cssStyles.push(stringifyStyle(rule)); |
667 } | 759 } |
668 } | 760 } |
669 | 761 |
670 let targets = extractMutationTargets(mutations); | 762 let targets = extractMutationTargets(mutations); |
671 | 763 |
672 let pattern = null; | 764 let pattern = null; |
673 let generator = null; | 765 let generator = null; |
674 | 766 |
675 let processPatterns = () => | 767 let processPatterns = () => |
676 { | 768 { |
677 let cycleStart = performance.now(); | 769 let cycleStart = performance.now(); |
678 | 770 |
679 if (!pattern) | 771 if (!pattern) |
680 { | 772 { |
681 if (!patterns.length) | 773 if (!patterns.length) |
682 { | 774 { |
683 this.addSelectorsFunc(selectors, selectorFilters); | 775 if (selectors.length > 0) |
684 this.hideElemsFunc(elements, elementFilters); | 776 this.addSelectorsFunc(selectors, selectorFilters); |
777 if (elements.length > 0) | |
778 this.hideElemsFunc(elements, elementFilters); | |
685 if (typeof done == "function") | 779 if (typeof done == "function") |
686 done(); | 780 done(); |
687 return; | 781 return; |
688 } | 782 } |
689 | 783 |
690 pattern = patterns.shift(); | 784 pattern = patterns.shift(); |
691 | 785 |
786 let evaluationTargets = targets; | |
787 | |
788 // If the pattern appears to contain any sibling combinators, we can't | |
789 // easily optimize based on the mutation targets. Since this is a | |
790 // special case, skip the optimization. By setting it to null here we | |
791 // make sure we process the entire DOM. | |
792 if (pattern.maybeContainsSiblingCombinators) | |
793 evaluationTargets = null; | |
794 | |
795 // Ignore mutation targets when using style sheets, because we may have | |
796 // to update all the CSS selectors. | |
797 if (!this.useInlineStyles) | |
798 evaluationTargets = null; | |
799 | |
692 generator = evaluate(pattern.selectors, 0, "", | 800 generator = evaluate(pattern.selectors, 0, "", |
693 this.document, cssStyles, targets); | 801 this.document, cssStyles, evaluationTargets); |
694 } | 802 } |
695 for (let selector of generator) | 803 for (let selector of generator) |
696 { | 804 { |
697 if (selector != null) | 805 if (selector != null) |
698 { | 806 { |
699 if (isSelectorHidingOnlyPattern(pattern)) | 807 if (!this.useInlineStyles) |
700 { | 808 { |
701 selectors.push(selector); | 809 selectors.push(selector); |
702 selectorFilters.push(pattern.text); | 810 selectorFilters.push(pattern.text); |
703 } | 811 } |
704 else | 812 else |
705 { | 813 { |
706 for (let element of this.document.querySelectorAll(selector)) | 814 for (let element of this.document.querySelectorAll(selector)) |
707 { | 815 { |
708 elements.push(element); | 816 elements.push(element); |
709 elementFilters.push(pattern.text); | 817 elementFilters.push(pattern.text); |
(...skipping 39 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
749 * for full reprocessing. | 857 * for full reprocessing. |
750 */ | 858 */ |
751 queueFiltering(stylesheets, mutations) | 859 queueFiltering(stylesheets, mutations) |
752 { | 860 { |
753 let completion = () => | 861 let completion = () => |
754 { | 862 { |
755 this._lastInvocation = performance.now(); | 863 this._lastInvocation = performance.now(); |
756 this._filteringInProgress = false; | 864 this._filteringInProgress = false; |
757 if (this._scheduledProcessing) | 865 if (this._scheduledProcessing) |
758 { | 866 { |
759 let {stylesheets, mutations} = this._scheduledProcessing; | 867 let params = Object.assign({}, this._scheduledProcessing); |
760 this._scheduledProcessing = null; | 868 this._scheduledProcessing = null; |
761 this.queueFiltering(stylesheets, mutations); | 869 this.queueFiltering(params.stylesheets, params.mutations); |
762 } | 870 } |
763 }; | 871 }; |
764 | 872 |
765 if (this._scheduledProcessing) | 873 if (this._scheduledProcessing) |
766 { | 874 { |
767 if (!stylesheets && !mutations) | 875 if (!stylesheets && !mutations) |
768 { | 876 { |
769 this._scheduledProcessing = {}; | 877 this._scheduledProcessing = {}; |
770 } | 878 } |
771 else | 879 else if (this._scheduledProcessing.stylesheets || |
880 this._scheduledProcessing.mutations) | |
772 { | 881 { |
773 if (stylesheets) | 882 if (stylesheets) |
774 { | 883 { |
775 if (!this._scheduledProcessing.stylesheets) | 884 if (!this._scheduledProcessing.stylesheets) |
776 this._scheduledProcessing.stylesheets = []; | 885 this._scheduledProcessing.stylesheets = []; |
777 this._scheduledProcessing.stylesheets.push(...stylesheets); | 886 this._scheduledProcessing.stylesheets.push(...stylesheets); |
778 } | 887 } |
779 if (mutations) | 888 if (mutations) |
780 { | 889 { |
781 if (!this._scheduledProcessing.mutations) | 890 if (!this._scheduledProcessing.mutations) |
782 this._scheduledProcessing.mutations = []; | 891 this._scheduledProcessing.mutations = []; |
783 this._scheduledProcessing.mutations.push(...mutations); | 892 this._scheduledProcessing.mutations.push(...mutations); |
784 } | 893 } |
785 } | 894 } |
786 } | 895 } |
787 else if (this._filteringInProgress) | 896 else if (this._filteringInProgress) |
788 { | 897 { |
789 this._scheduledProcessing = {stylesheets, mutations}; | 898 this._scheduledProcessing = {stylesheets, mutations}; |
790 } | 899 } |
791 else if (performance.now() - this._lastInvocation < MIN_INVOCATION_INTERVAL) | 900 else if (performance.now() - this._lastInvocation < MIN_INVOCATION_INTERVAL) |
792 { | 901 { |
793 this._scheduledProcessing = {stylesheets, mutations}; | 902 this._scheduledProcessing = {stylesheets, mutations}; |
794 setTimeout(() => | 903 setTimeout(() => |
795 { | 904 { |
796 let {stylesheets, mutations} = this._scheduledProcessing; | 905 let params = Object.assign({}, this._scheduledProcessing); |
797 this._filteringInProgress = true; | 906 this._filteringInProgress = true; |
798 this._scheduledProcessing = null; | 907 this._scheduledProcessing = null; |
799 this._addSelectors(stylesheets, mutations, completion); | 908 this._addSelectors(params.stylesheets, params.mutations, completion); |
800 }, | 909 }, |
801 MIN_INVOCATION_INTERVAL - (performance.now() - this._lastInvocation)); | 910 MIN_INVOCATION_INTERVAL - (performance.now() - this._lastInvocation)); |
802 } | 911 } |
803 else if (this.document.readyState == "loading") | 912 else if (this.document.readyState == "loading") |
804 { | 913 { |
805 this._scheduledProcessing = {stylesheets, mutations}; | 914 this._scheduledProcessing = {stylesheets, mutations}; |
806 let handler = () => | 915 let handler = () => |
807 { | 916 { |
808 document.removeEventListener("DOMContentLoaded", handler); | 917 this.document.removeEventListener("DOMContentLoaded", handler); |
809 let {stylesheets, mutations} = this._scheduledProcessing; | 918 let params = Object.assign({}, this._scheduledProcessing); |
810 this._filteringInProgress = true; | 919 this._filteringInProgress = true; |
811 this._scheduledProcessing = null; | 920 this._scheduledProcessing = null; |
812 this._addSelectors(stylesheets, mutations, completion); | 921 this._addSelectors(params.stylesheets, params.mutations, completion); |
813 }; | 922 }; |
814 document.addEventListener("DOMContentLoaded", handler); | 923 this.document.addEventListener("DOMContentLoaded", handler); |
815 } | 924 } |
816 else | 925 else |
817 { | 926 { |
818 this._filteringInProgress = true; | 927 this._filteringInProgress = true; |
819 this._addSelectors(stylesheets, mutations, completion); | 928 this._addSelectors(stylesheets, mutations, completion); |
820 } | 929 } |
821 }, | 930 }, |
822 | 931 |
823 onLoad(event) | 932 onLoad(event) |
824 { | 933 { |
825 let stylesheet = event.target.sheet; | 934 let stylesheet = event.target.sheet; |
826 if (stylesheet) | 935 if (stylesheet) |
827 this.queueFiltering([stylesheet]); | 936 this.queueFiltering([stylesheet]); |
828 }, | 937 }, |
829 | 938 |
830 observe(mutations) | 939 observe(mutations) |
831 { | 940 { |
941 if (testInfo) | |
942 { | |
943 // In test mode, filter out any mutations likely done by us | |
944 // (i.e. style="display: none !important"). This makes it easier to | |
945 // observe how the code responds to DOM mutations. | |
946 mutations = mutations.filter( | |
947 ({type, attributeName, target: {style: newValue}, oldValue}) => | |
948 !(type == "attributes" && attributeName == "style" && | |
949 newValue.display == "none" && oldValue.display != "none") | |
950 ); | |
951 | |
952 if (mutations.length == 0) | |
953 return; | |
954 } | |
hub
2018/05/24 19:19:09
... and this :-/
There should be a better way to
Manish Jethani
2018/05/25 07:21:25
OK, do you have any suggestions? This is only done
| |
955 | |
832 this.queueFiltering(null, mutations); | 956 this.queueFiltering(null, mutations); |
833 }, | 957 }, |
834 | 958 |
835 apply(patterns) | 959 apply(patterns) |
836 { | 960 { |
837 this.patterns = []; | 961 this.patterns = []; |
838 for (let pattern of patterns) | 962 for (let pattern of patterns) |
839 { | 963 { |
840 let selectors = this.parseSelector(pattern.selector); | 964 let selectors = this.parseSelector(pattern.selector); |
841 if (selectors != null && selectors.length > 0) | 965 if (selectors != null && selectors.length > 0) |
842 this.patterns.push({selectors, text: pattern.text}); | 966 this.patterns.push(new Pattern(selectors, pattern.text)); |
843 } | 967 } |
844 | 968 |
845 if (this.patterns.length > 0) | 969 if (this.patterns.length > 0) |
846 { | 970 { |
847 this.queueFiltering(); | 971 this.queueFiltering(); |
848 this.observer.observe( | 972 this.observer.observe( |
849 this.document, | 973 this.document, |
850 { | 974 { |
851 childList: true, | 975 childList: true, |
852 attributes: shouldObserveAttributes(this.patterns), | 976 attributes: shouldObserveAttributes(this.patterns), |
853 characterData: shouldObserveCharacterData(this.patterns), | 977 characterData: shouldObserveCharacterData(this.patterns), |
854 subtree: true | 978 subtree: true |
855 } | 979 } |
856 ); | 980 ); |
857 this.document.addEventListener("load", this.onLoad.bind(this), true); | 981 this.document.addEventListener("load", this.onLoad.bind(this), true); |
858 } | 982 } |
859 } | 983 } |
860 }; | 984 }; |
861 | 985 |
862 exports.ElemHideEmulation = ElemHideEmulation; | 986 exports.ElemHideEmulation = ElemHideEmulation; |
LEFT | RIGHT |