OLD | NEW |
| (Empty) |
1 /* | |
2 * This file is part of Adblock Plus <https://adblockplus.org/>, | |
3 * Copyright (C) 2006-2016 Eyeo GmbH | |
4 * | |
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 | |
7 * published by the Free Software Foundation. | |
8 * | |
9 * Adblock Plus is distributed in the hope that it will be useful, | |
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
12 * GNU General Public License for more details. | |
13 * | |
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/>. | |
16 */ | |
17 | |
18 /** | |
19 * @fileOverview Definition of Filter class and its subclasses. | |
20 */ | |
21 | |
22 let {FilterNotifier} = require("filterNotifier"); | |
23 let {Utils} = require("utils"); | |
24 | |
25 /** | |
26 * Abstract base class for filters | |
27 * | |
28 * @param {String} text string representation of the filter | |
29 * @constructor | |
30 */ | |
31 function Filter(text) | |
32 { | |
33 this.text = text; | |
34 this.subscriptions = []; | |
35 } | |
36 exports.Filter = Filter; | |
37 | |
38 Filter.prototype = | |
39 { | |
40 /** | |
41 * String representation of the filter | |
42 * @type String | |
43 */ | |
44 text: null, | |
45 | |
46 /** | |
47 * Filter subscriptions the filter belongs to | |
48 * @type Subscription[] | |
49 */ | |
50 subscriptions: null, | |
51 | |
52 /** | |
53 * Filter type as a string, e.g. "blocking". | |
54 * @type String | |
55 */ | |
56 get type() | |
57 { | |
58 throw new Error("Please define filter type in the subclass"); | |
59 }, | |
60 | |
61 /** | |
62 * Serializes the filter to an array of strings for writing out on the disk. | |
63 * @param {string[]} buffer buffer to push the serialization results into | |
64 */ | |
65 serialize: function(buffer) | |
66 { | |
67 buffer.push("[Filter]"); | |
68 buffer.push("text=" + this.text); | |
69 }, | |
70 | |
71 toString: function() | |
72 { | |
73 return this.text; | |
74 } | |
75 }; | |
76 | |
77 /** | |
78 * Cache for known filters, maps string representation to filter objects. | |
79 * @type Object | |
80 */ | |
81 Filter.knownFilters = Object.create(null); | |
82 | |
83 /** | |
84 * Regular expression that element hiding filters should match | |
85 * @type RegExp | |
86 */ | |
87 Filter.elemhideRegExp = /^([^\/\*\|\@"!]*?)#(\@)?(?:([\w\-]+|\*)((?:\([\w\-]+(?:
[$^*]?=[^\(\)"]*)?\))*)|#([^{}]+))$/; | |
88 /** | |
89 * Regular expression that RegExp filters specified as RegExps should match | |
90 * @type RegExp | |
91 */ | |
92 Filter.regexpRegExp = /^(@@)?\/.*\/(?:\$~?[\w\-]+(?:=[^,\s]+)?(?:,~?[\w\-]+(?:=[
^,\s]+)?)*)?$/; | |
93 /** | |
94 * Regular expression that options on a RegExp filter should match | |
95 * @type RegExp | |
96 */ | |
97 Filter.optionsRegExp = /\$(~?[\w\-]+(?:=[^,\s]+)?(?:,~?[\w\-]+(?:=[^,\s]+)?)*)$/
; | |
98 /** | |
99 * Regular expression that CSS property filters should match | |
100 * Properties must not contain " or ' | |
101 * @type RegExp | |
102 */ | |
103 Filter.csspropertyRegExp = /\[\-abp\-properties=(["'])([^"']+)\1\]/; | |
104 | |
105 /** | |
106 * Creates a filter of correct type from its text representation - does the basi
c parsing and | |
107 * calls the right constructor then. | |
108 * | |
109 * @param {String} text as in Filter() | |
110 * @return {Filter} | |
111 */ | |
112 Filter.fromText = function(text) | |
113 { | |
114 if (text in Filter.knownFilters) | |
115 return Filter.knownFilters[text]; | |
116 | |
117 let ret; | |
118 let match = (text.indexOf("#") >= 0 ? Filter.elemhideRegExp.exec(text) : null)
; | |
119 if (match) | |
120 ret = ElemHideBase.fromText(text, match[1], !!match[2], match[3], match[4],
match[5]); | |
121 else if (text[0] == "!") | |
122 ret = new CommentFilter(text); | |
123 else | |
124 ret = RegExpFilter.fromText(text); | |
125 | |
126 Filter.knownFilters[ret.text] = ret; | |
127 return ret; | |
128 }; | |
129 | |
130 /** | |
131 * Deserializes a filter | |
132 * | |
133 * @param {Object} obj map of serialized properties and their values | |
134 * @return {Filter} filter or null if the filter couldn't be created | |
135 */ | |
136 Filter.fromObject = function(obj) | |
137 { | |
138 let ret = Filter.fromText(obj.text); | |
139 if (ret instanceof ActiveFilter) | |
140 { | |
141 if ("disabled" in obj) | |
142 ret._disabled = (obj.disabled == "true"); | |
143 if ("hitCount" in obj) | |
144 ret._hitCount = parseInt(obj.hitCount) || 0; | |
145 if ("lastHit" in obj) | |
146 ret._lastHit = parseInt(obj.lastHit) || 0; | |
147 } | |
148 return ret; | |
149 }; | |
150 | |
151 /** | |
152 * Removes unnecessary whitespaces from filter text, will only return null if | |
153 * the input parameter is null. | |
154 */ | |
155 Filter.normalize = function(/**String*/ text) /**String*/ | |
156 { | |
157 if (!text) | |
158 return text; | |
159 | |
160 // Remove line breaks and such | |
161 text = text.replace(/[^\S ]/g, ""); | |
162 | |
163 if (/^\s*!/.test(text)) | |
164 { | |
165 // Don't remove spaces inside comments | |
166 return text.trim(); | |
167 } | |
168 else if (Filter.elemhideRegExp.test(text)) | |
169 { | |
170 // Special treatment for element hiding filters, right side is allowed to co
ntain spaces | |
171 let [, domain, separator, selector] = /^(.*?)(#\@?#?)(.*)$/.exec(text); | |
172 return domain.replace(/\s/g, "") + separator + selector.trim(); | |
173 } | |
174 else | |
175 return text.replace(/\s/g, ""); | |
176 }; | |
177 | |
178 /** | |
179 * Converts filter text into regular expression string | |
180 * @param {String} text as in Filter() | |
181 * @return {String} regular expression representation of filter text | |
182 */ | |
183 Filter.toRegExp = function(text) | |
184 { | |
185 return text | |
186 .replace(/\*+/g, "*") // remove multiple wildcards | |
187 .replace(/\^\|$/, "^") // remove anchors following separator placehold
er | |
188 .replace(/\W/g, "\\$&") // escape special symbols | |
189 .replace(/\\\*/g, ".*") // replace wildcards by .* | |
190 // process separator placeholders (all ANSI characters but alphanumeric char
acters and _%.-) | |
191 .replace(/\\\^/g, "(?:[\\x00-\\x24\\x26-\\x2C\\x2F\\x3A-\\x40\\x5B-\\x5E\\x6
0\\x7B-\\x7F]|$)") | |
192 .replace(/^\\\|\\\|/, "^[\\w\\-]+:\\/+(?!\\/)(?:[^\\/]+\\.)?") // process ex
tended anchor at expression start | |
193 .replace(/^\\\|/, "^") // process anchor at expression start | |
194 .replace(/\\\|$/, "$") // process anchor at expression end | |
195 .replace(/^(\.\*)/, "") // remove leading wildcards | |
196 .replace(/(\.\*)$/, ""); // remove trailing wildcards | |
197 } | |
198 | |
199 /** | |
200 * Class for invalid filters | |
201 * @param {String} text see Filter() | |
202 * @param {String} reason Reason why this filter is invalid | |
203 * @constructor | |
204 * @augments Filter | |
205 */ | |
206 function InvalidFilter(text, reason) | |
207 { | |
208 Filter.call(this, text); | |
209 | |
210 this.reason = reason; | |
211 } | |
212 exports.InvalidFilter = InvalidFilter; | |
213 | |
214 InvalidFilter.prototype = | |
215 { | |
216 __proto__: Filter.prototype, | |
217 | |
218 type: "invalid", | |
219 | |
220 /** | |
221 * Reason why this filter is invalid | |
222 * @type String | |
223 */ | |
224 reason: null, | |
225 | |
226 /** | |
227 * See Filter.serialize() | |
228 */ | |
229 serialize: function(buffer) {} | |
230 }; | |
231 | |
232 /** | |
233 * Class for comments | |
234 * @param {String} text see Filter() | |
235 * @constructor | |
236 * @augments Filter | |
237 */ | |
238 function CommentFilter(text) | |
239 { | |
240 Filter.call(this, text); | |
241 } | |
242 exports.CommentFilter = CommentFilter; | |
243 | |
244 CommentFilter.prototype = | |
245 { | |
246 __proto__: Filter.prototype, | |
247 | |
248 type: "comment", | |
249 | |
250 /** | |
251 * See Filter.serialize() | |
252 */ | |
253 serialize: function(buffer) {} | |
254 }; | |
255 | |
256 /** | |
257 * Abstract base class for filters that can get hits | |
258 * @param {String} text see Filter() | |
259 * @param {String} [domains] Domains that the filter is restricted to separated
by domainSeparator e.g. "foo.com|bar.com|~baz.com" | |
260 * @constructor | |
261 * @augments Filter | |
262 */ | |
263 function ActiveFilter(text, domains) | |
264 { | |
265 Filter.call(this, text); | |
266 | |
267 this.domainSource = domains; | |
268 } | |
269 exports.ActiveFilter = ActiveFilter; | |
270 | |
271 ActiveFilter.prototype = | |
272 { | |
273 __proto__: Filter.prototype, | |
274 | |
275 _disabled: false, | |
276 _hitCount: 0, | |
277 _lastHit: 0, | |
278 | |
279 /** | |
280 * Defines whether the filter is disabled | |
281 * @type Boolean | |
282 */ | |
283 get disabled() | |
284 { | |
285 return this._disabled; | |
286 }, | |
287 set disabled(value) | |
288 { | |
289 if (value != this._disabled) | |
290 { | |
291 let oldValue = this._disabled; | |
292 this._disabled = value; | |
293 FilterNotifier.triggerListeners("filter.disabled", this, value, oldValue); | |
294 } | |
295 return this._disabled; | |
296 }, | |
297 | |
298 /** | |
299 * Number of hits on the filter since the last reset | |
300 * @type Number | |
301 */ | |
302 get hitCount() | |
303 { | |
304 return this._hitCount; | |
305 }, | |
306 set hitCount(value) | |
307 { | |
308 if (value != this._hitCount) | |
309 { | |
310 let oldValue = this._hitCount; | |
311 this._hitCount = value; | |
312 FilterNotifier.triggerListeners("filter.hitCount", this, value, oldValue); | |
313 } | |
314 return this._hitCount; | |
315 }, | |
316 | |
317 /** | |
318 * Last time the filter had a hit (in milliseconds since the beginning of the
epoch) | |
319 * @type Number | |
320 */ | |
321 get lastHit() | |
322 { | |
323 return this._lastHit; | |
324 }, | |
325 set lastHit(value) | |
326 { | |
327 if (value != this._lastHit) | |
328 { | |
329 let oldValue = this._lastHit; | |
330 this._lastHit = value; | |
331 FilterNotifier.triggerListeners("filter.lastHit", this, value, oldValue); | |
332 } | |
333 return this._lastHit; | |
334 }, | |
335 | |
336 /** | |
337 * String that the domains property should be generated from | |
338 * @type String | |
339 */ | |
340 domainSource: null, | |
341 | |
342 /** | |
343 * Separator character used in domainSource property, must be overridden by su
bclasses | |
344 * @type String | |
345 */ | |
346 domainSeparator: null, | |
347 | |
348 /** | |
349 * Determines whether the trailing dot in domain names isn't important and | |
350 * should be ignored, must be overridden by subclasses. | |
351 * @type Boolean | |
352 */ | |
353 ignoreTrailingDot: true, | |
354 | |
355 /** | |
356 * Determines whether domainSource is already upper-case, | |
357 * can be overridden by subclasses. | |
358 * @type Boolean | |
359 */ | |
360 domainSourceIsUpperCase: false, | |
361 | |
362 /** | |
363 * Map containing domains that this filter should match on/not match on or nul
l if the filter should match on all domains | |
364 * @type Object | |
365 */ | |
366 get domains() | |
367 { | |
368 // Despite this property being cached, the getter is called | |
369 // several times on Safari, due to WebKit bug 132872 | |
370 let prop = Object.getOwnPropertyDescriptor(this, "domains"); | |
371 if (prop) | |
372 return prop.value; | |
373 | |
374 let domains = null; | |
375 | |
376 if (this.domainSource) | |
377 { | |
378 let source = this.domainSource; | |
379 if (!this.domainSourceIsUpperCase) { | |
380 // RegExpFilter already have uppercase domains | |
381 source = source.toUpperCase(); | |
382 } | |
383 let list = source.split(this.domainSeparator); | |
384 if (list.length == 1 && list[0][0] != "~") | |
385 { | |
386 // Fast track for the common one-domain scenario | |
387 domains = {__proto__: null, "": false}; | |
388 if (this.ignoreTrailingDot) | |
389 list[0] = list[0].replace(/\.+$/, ""); | |
390 domains[list[0]] = true; | |
391 } | |
392 else | |
393 { | |
394 let hasIncludes = false; | |
395 for (let i = 0; i < list.length; i++) | |
396 { | |
397 let domain = list[i]; | |
398 if (this.ignoreTrailingDot) | |
399 domain = domain.replace(/\.+$/, ""); | |
400 if (domain == "") | |
401 continue; | |
402 | |
403 let include; | |
404 if (domain[0] == "~") | |
405 { | |
406 include = false; | |
407 domain = domain.substr(1); | |
408 } | |
409 else | |
410 { | |
411 include = true; | |
412 hasIncludes = true; | |
413 } | |
414 | |
415 if (!domains) | |
416 domains = Object.create(null); | |
417 | |
418 domains[domain] = include; | |
419 } | |
420 domains[""] = !hasIncludes; | |
421 } | |
422 | |
423 this.domainSource = null; | |
424 } | |
425 | |
426 Object.defineProperty(this, "domains", {value: domains, enumerable: true}); | |
427 return this.domains; | |
428 }, | |
429 | |
430 /** | |
431 * Array containing public keys of websites that this filter should apply to | |
432 * @type string[] | |
433 */ | |
434 sitekeys: null, | |
435 | |
436 /** | |
437 * Checks whether this filter is active on a domain. | |
438 * @param {String} docDomain domain name of the document that loads the URL | |
439 * @param {String} [sitekey] public key provided by the document | |
440 * @return {Boolean} true in case of the filter being active | |
441 */ | |
442 isActiveOnDomain: function(docDomain, sitekey) | |
443 { | |
444 // Sitekeys are case-sensitive so we shouldn't convert them to upper-case to
avoid false | |
445 // positives here. Instead we need to change the way filter options are pars
ed. | |
446 if (this.sitekeys && (!sitekey || this.sitekeys.indexOf(sitekey.toUpperCase(
)) < 0)) | |
447 return false; | |
448 | |
449 // If no domains are set the rule matches everywhere | |
450 if (!this.domains) | |
451 return true; | |
452 | |
453 // If the document has no host name, match only if the filter isn't restrict
ed to specific domains | |
454 if (!docDomain) | |
455 return this.domains[""]; | |
456 | |
457 if (this.ignoreTrailingDot) | |
458 docDomain = docDomain.replace(/\.+$/, ""); | |
459 docDomain = docDomain.toUpperCase(); | |
460 | |
461 while (true) | |
462 { | |
463 if (docDomain in this.domains) | |
464 return this.domains[docDomain]; | |
465 | |
466 let nextDot = docDomain.indexOf("."); | |
467 if (nextDot < 0) | |
468 break; | |
469 docDomain = docDomain.substr(nextDot + 1); | |
470 } | |
471 return this.domains[""]; | |
472 }, | |
473 | |
474 /** | |
475 * Checks whether this filter is active only on a domain and its subdomains. | |
476 */ | |
477 isActiveOnlyOnDomain: function(/**String*/ docDomain) /**Boolean*/ | |
478 { | |
479 if (!docDomain || !this.domains || this.domains[""]) | |
480 return false; | |
481 | |
482 if (this.ignoreTrailingDot) | |
483 docDomain = docDomain.replace(/\.+$/, ""); | |
484 docDomain = docDomain.toUpperCase(); | |
485 | |
486 for (let domain in this.domains) | |
487 if (this.domains[domain] && domain != docDomain && (domain.length <= docDo
main.length || domain.indexOf("." + docDomain) != domain.length - docDomain.leng
th - 1)) | |
488 return false; | |
489 | |
490 return true; | |
491 }, | |
492 | |
493 /** | |
494 * Checks whether this filter is generic or specific | |
495 */ | |
496 isGeneric: function() /**Boolean*/ | |
497 { | |
498 return !(this.sitekeys && this.sitekeys.length) && | |
499 (!this.domains || this.domains[""]); | |
500 }, | |
501 | |
502 /** | |
503 * See Filter.serialize() | |
504 */ | |
505 serialize: function(buffer) | |
506 { | |
507 if (this._disabled || this._hitCount || this._lastHit) | |
508 { | |
509 Filter.prototype.serialize.call(this, buffer); | |
510 if (this._disabled) | |
511 buffer.push("disabled=true"); | |
512 if (this._hitCount) | |
513 buffer.push("hitCount=" + this._hitCount); | |
514 if (this._lastHit) | |
515 buffer.push("lastHit=" + this._lastHit); | |
516 } | |
517 } | |
518 }; | |
519 | |
520 /** | |
521 * Abstract base class for RegExp-based filters | |
522 * @param {String} text see Filter() | |
523 * @param {String} regexpSource filter part that the regular expression should b
e build from | |
524 * @param {Number} [contentType] Content types the filter applies to, combinatio
n of values from RegExpFilter.typeMap | |
525 * @param {Boolean} [matchCase] Defines whether the filter should distinguish be
tween lower and upper case letters | |
526 * @param {String} [domains] Domains that the filter is restricted to, e.g. "foo
.com|bar.com|~baz.com" | |
527 * @param {Boolean} [thirdParty] Defines whether the filter should apply to thir
d-party or first-party content only | |
528 * @param {String} [sitekeys] Public keys of websites that this filter should ap
ply to | |
529 * @constructor | |
530 * @augments ActiveFilter | |
531 */ | |
532 function RegExpFilter(text, regexpSource, contentType, matchCase, domains, third
Party, sitekeys) | |
533 { | |
534 ActiveFilter.call(this, text, domains, sitekeys); | |
535 | |
536 if (contentType != null) | |
537 this.contentType = contentType; | |
538 if (matchCase) | |
539 this.matchCase = matchCase; | |
540 if (thirdParty != null) | |
541 this.thirdParty = thirdParty; | |
542 if (sitekeys != null) | |
543 this.sitekeySource = sitekeys; | |
544 | |
545 if (regexpSource.length >= 2 && regexpSource[0] == "/" && regexpSource[regexpS
ource.length - 1] == "/") | |
546 { | |
547 // The filter is a regular expression - convert it immediately to catch synt
ax errors | |
548 let regexp = new RegExp(regexpSource.substr(1, regexpSource.length - 2), thi
s.matchCase ? "" : "i"); | |
549 Object.defineProperty(this, "regexp", {value: regexp}); | |
550 } | |
551 else | |
552 { | |
553 // No need to convert this filter to regular expression yet, do it on demand | |
554 this.regexpSource = regexpSource; | |
555 } | |
556 } | |
557 exports.RegExpFilter = RegExpFilter; | |
558 | |
559 RegExpFilter.prototype = | |
560 { | |
561 __proto__: ActiveFilter.prototype, | |
562 | |
563 /** | |
564 * @see ActiveFilter.domainSourceIsUpperCase | |
565 */ | |
566 domainSourceIsUpperCase: true, | |
567 | |
568 /** | |
569 * Number of filters contained, will always be 1 (required to optimize Matcher
). | |
570 * @type Integer | |
571 */ | |
572 length: 1, | |
573 | |
574 /** | |
575 * @see ActiveFilter.domainSeparator | |
576 */ | |
577 domainSeparator: "|", | |
578 | |
579 /** | |
580 * Expression from which a regular expression should be generated - for delaye
d creation of the regexp property | |
581 * @type String | |
582 */ | |
583 regexpSource: null, | |
584 /** | |
585 * Regular expression to be used when testing against this filter | |
586 * @type RegExp | |
587 */ | |
588 get regexp() | |
589 { | |
590 // Despite this property being cached, the getter is called | |
591 // several times on Safari, due to WebKit bug 132872 | |
592 let prop = Object.getOwnPropertyDescriptor(this, "regexp"); | |
593 if (prop) | |
594 return prop.value; | |
595 | |
596 let source = Filter.toRegExp(this.regexpSource); | |
597 let regexp = new RegExp(source, this.matchCase ? "" : "i"); | |
598 Object.defineProperty(this, "regexp", {value: regexp}); | |
599 return regexp; | |
600 }, | |
601 /** | |
602 * Content types the filter applies to, combination of values from RegExpFilte
r.typeMap | |
603 * @type Number | |
604 */ | |
605 contentType: 0x7FFFFFFF, | |
606 /** | |
607 * Defines whether the filter should distinguish between lower and upper case
letters | |
608 * @type Boolean | |
609 */ | |
610 matchCase: false, | |
611 /** | |
612 * Defines whether the filter should apply to third-party or first-party conte
nt only. Can be null (apply to all content). | |
613 * @type Boolean | |
614 */ | |
615 thirdParty: null, | |
616 | |
617 /** | |
618 * String that the sitekey property should be generated from | |
619 * @type String | |
620 */ | |
621 sitekeySource: null, | |
622 | |
623 /** | |
624 * Array containing public keys of websites that this filter should apply to | |
625 * @type string[] | |
626 */ | |
627 get sitekeys() | |
628 { | |
629 // Despite this property being cached, the getter is called | |
630 // several times on Safari, due to WebKit bug 132872 | |
631 let prop = Object.getOwnPropertyDescriptor(this, "sitekeys"); | |
632 if (prop) | |
633 return prop.value; | |
634 | |
635 let sitekeys = null; | |
636 | |
637 if (this.sitekeySource) | |
638 { | |
639 sitekeys = this.sitekeySource.split("|"); | |
640 this.sitekeySource = null; | |
641 } | |
642 | |
643 Object.defineProperty(this, "sitekeys", {value: sitekeys, enumerable: true})
; | |
644 return this.sitekeys; | |
645 }, | |
646 | |
647 /** | |
648 * Tests whether the URL matches this filter | |
649 * @param {String} location URL to be tested | |
650 * @param {String} typeMask bitmask of content / request types to match | |
651 * @param {String} docDomain domain name of the document that loads the URL | |
652 * @param {Boolean} thirdParty should be true if the URL is a third-party requ
est | |
653 * @param {String} sitekey public key provided by the document | |
654 * @return {Boolean} true in case of a match | |
655 */ | |
656 matches: function(location, typeMask, docDomain, thirdParty, sitekey) | |
657 { | |
658 if (this.contentType & typeMask && | |
659 (this.thirdParty == null || this.thirdParty == thirdParty) && | |
660 this.isActiveOnDomain(docDomain, sitekey) && this.regexp.test(location)) | |
661 { | |
662 return true; | |
663 } | |
664 | |
665 return false; | |
666 } | |
667 }; | |
668 | |
669 // Required to optimize Matcher, see also RegExpFilter.prototype.length | |
670 Object.defineProperty(RegExpFilter.prototype, "0", | |
671 { | |
672 get: function() { return this; } | |
673 }); | |
674 | |
675 /** | |
676 * Creates a RegExp filter from its text representation | |
677 * @param {String} text same as in Filter() | |
678 */ | |
679 RegExpFilter.fromText = function(text) | |
680 { | |
681 let blocking = true; | |
682 let origText = text; | |
683 if (text.indexOf("@@") == 0) | |
684 { | |
685 blocking = false; | |
686 text = text.substr(2); | |
687 } | |
688 | |
689 let contentType = null; | |
690 let matchCase = null; | |
691 let domains = null; | |
692 let sitekeys = null; | |
693 let thirdParty = null; | |
694 let collapse = null; | |
695 let options; | |
696 let match = (text.indexOf("$") >= 0 ? Filter.optionsRegExp.exec(text) : null); | |
697 if (match) | |
698 { | |
699 options = match[1].toUpperCase().split(","); | |
700 text = match.input.substr(0, match.index); | |
701 for (let option of options) | |
702 { | |
703 let value = null; | |
704 let separatorIndex = option.indexOf("="); | |
705 if (separatorIndex >= 0) | |
706 { | |
707 value = option.substr(separatorIndex + 1); | |
708 option = option.substr(0, separatorIndex); | |
709 } | |
710 option = option.replace(/-/, "_"); | |
711 if (option in RegExpFilter.typeMap) | |
712 { | |
713 if (contentType == null) | |
714 contentType = 0; | |
715 contentType |= RegExpFilter.typeMap[option]; | |
716 } | |
717 else if (option[0] == "~" && option.substr(1) in RegExpFilter.typeMap) | |
718 { | |
719 if (contentType == null) | |
720 contentType = RegExpFilter.prototype.contentType; | |
721 contentType &= ~RegExpFilter.typeMap[option.substr(1)]; | |
722 } | |
723 else if (option == "MATCH_CASE") | |
724 matchCase = true; | |
725 else if (option == "~MATCH_CASE") | |
726 matchCase = false; | |
727 else if (option == "DOMAIN" && typeof value != "undefined") | |
728 domains = value; | |
729 else if (option == "THIRD_PARTY") | |
730 thirdParty = true; | |
731 else if (option == "~THIRD_PARTY") | |
732 thirdParty = false; | |
733 else if (option == "COLLAPSE") | |
734 collapse = true; | |
735 else if (option == "~COLLAPSE") | |
736 collapse = false; | |
737 else if (option == "SITEKEY" && typeof value != "undefined") | |
738 sitekeys = value; | |
739 else | |
740 return new InvalidFilter(origText, "Unknown option " + option.toLowerCas
e()); | |
741 } | |
742 } | |
743 | |
744 try | |
745 { | |
746 if (blocking) | |
747 return new BlockingFilter(origText, text, contentType, matchCase, domains,
thirdParty, sitekeys, collapse); | |
748 else | |
749 return new WhitelistFilter(origText, text, contentType, matchCase, domains
, thirdParty, sitekeys); | |
750 } | |
751 catch (e) | |
752 { | |
753 return new InvalidFilter(origText, e); | |
754 } | |
755 }; | |
756 | |
757 /** | |
758 * Maps type strings like "SCRIPT" or "OBJECT" to bit masks | |
759 */ | |
760 RegExpFilter.typeMap = { | |
761 OTHER: 1, | |
762 SCRIPT: 2, | |
763 IMAGE: 4, | |
764 STYLESHEET: 8, | |
765 OBJECT: 16, | |
766 SUBDOCUMENT: 32, | |
767 DOCUMENT: 64, | |
768 XBL: 1, | |
769 PING: 1024, | |
770 XMLHTTPREQUEST: 2048, | |
771 OBJECT_SUBREQUEST: 4096, | |
772 DTD: 1, | |
773 MEDIA: 16384, | |
774 FONT: 32768, | |
775 | |
776 BACKGROUND: 4, // Backwards compat, same as IMAGE | |
777 | |
778 POPUP: 0x10000000, | |
779 GENERICBLOCK: 0x20000000, | |
780 ELEMHIDE: 0x40000000, | |
781 GENERICHIDE: 0x80000000 | |
782 }; | |
783 | |
784 // DOCUMENT, ELEMHIDE, POPUP, GENERICHIDE and GENERICBLOCK options shouldn't | |
785 // be there by default | |
786 RegExpFilter.prototype.contentType &= ~(RegExpFilter.typeMap.DOCUMENT | | |
787 RegExpFilter.typeMap.ELEMHIDE | | |
788 RegExpFilter.typeMap.POPUP | | |
789 RegExpFilter.typeMap.GENERICHIDE | | |
790 RegExpFilter.typeMap.GENERICBLOCK); | |
791 | |
792 /** | |
793 * Class for blocking filters | |
794 * @param {String} text see Filter() | |
795 * @param {String} regexpSource see RegExpFilter() | |
796 * @param {Number} contentType see RegExpFilter() | |
797 * @param {Boolean} matchCase see RegExpFilter() | |
798 * @param {String} domains see RegExpFilter() | |
799 * @param {Boolean} thirdParty see RegExpFilter() | |
800 * @param {String} sitekeys see RegExpFilter() | |
801 * @param {Boolean} collapse defines whether the filter should collapse blocked
content, can be null | |
802 * @constructor | |
803 * @augments RegExpFilter | |
804 */ | |
805 function BlockingFilter(text, regexpSource, contentType, matchCase, domains, thi
rdParty, sitekeys, collapse) | |
806 { | |
807 RegExpFilter.call(this, text, regexpSource, contentType, matchCase, domains, t
hirdParty, sitekeys); | |
808 | |
809 this.collapse = collapse; | |
810 } | |
811 exports.BlockingFilter = BlockingFilter; | |
812 | |
813 BlockingFilter.prototype = | |
814 { | |
815 __proto__: RegExpFilter.prototype, | |
816 | |
817 type: "blocking", | |
818 | |
819 /** | |
820 * Defines whether the filter should collapse blocked content. Can be null (us
e the global preference). | |
821 * @type Boolean | |
822 */ | |
823 collapse: null | |
824 }; | |
825 | |
826 /** | |
827 * Class for whitelist filters | |
828 * @param {String} text see Filter() | |
829 * @param {String} regexpSource see RegExpFilter() | |
830 * @param {Number} contentType see RegExpFilter() | |
831 * @param {Boolean} matchCase see RegExpFilter() | |
832 * @param {String} domains see RegExpFilter() | |
833 * @param {Boolean} thirdParty see RegExpFilter() | |
834 * @param {String} sitekeys see RegExpFilter() | |
835 * @constructor | |
836 * @augments RegExpFilter | |
837 */ | |
838 function WhitelistFilter(text, regexpSource, contentType, matchCase, domains, th
irdParty, sitekeys) | |
839 { | |
840 RegExpFilter.call(this, text, regexpSource, contentType, matchCase, domains, t
hirdParty, sitekeys); | |
841 } | |
842 exports.WhitelistFilter = WhitelistFilter; | |
843 | |
844 WhitelistFilter.prototype = | |
845 { | |
846 __proto__: RegExpFilter.prototype, | |
847 | |
848 type: "whitelist" | |
849 }; | |
850 | |
851 /** | |
852 * Base class for element hiding filters | |
853 * @param {String} text see Filter() | |
854 * @param {String} [domains] Host names or domains the filter should be restrict
ed to | |
855 * @param {String} selector CSS selector for the HTML elements that should be
hidden | |
856 * @constructor | |
857 * @augments ActiveFilter | |
858 */ | |
859 function ElemHideBase(text, domains, selector) | |
860 { | |
861 ActiveFilter.call(this, text, domains || null); | |
862 | |
863 if (domains) | |
864 this.selectorDomain = domains.replace(/,~[^,]+/g, "").replace(/^~[^,]+,?/, "
").toLowerCase(); | |
865 this.selector = selector; | |
866 } | |
867 exports.ElemHideBase = ElemHideBase; | |
868 | |
869 ElemHideBase.prototype = | |
870 { | |
871 __proto__: ActiveFilter.prototype, | |
872 | |
873 /** | |
874 * @see ActiveFilter.domainSeparator | |
875 */ | |
876 domainSeparator: ",", | |
877 | |
878 /** | |
879 * @see ActiveFilter.ignoreTrailingDot | |
880 */ | |
881 ignoreTrailingDot: false, | |
882 | |
883 /** | |
884 * Host name or domain the filter should be restricted to (can be null for no
restriction) | |
885 * @type String | |
886 */ | |
887 selectorDomain: null, | |
888 /** | |
889 * CSS selector for the HTML elements that should be hidden | |
890 * @type String | |
891 */ | |
892 selector: null | |
893 }; | |
894 | |
895 /** | |
896 * Creates an element hiding filter from a pre-parsed text representation | |
897 * | |
898 * @param {String} text same as in Filter() | |
899 * @param {String} domain domain part of the text representation (can be e
mpty) | |
900 * @param {Boolean} isException exception rule indicator | |
901 * @param {String} tagName tag name part (can be empty) | |
902 * @param {String} attrRules attribute matching rules (can be empty) | |
903 * @param {String} selector raw CSS selector (can be empty) | |
904 * @return {ElemHideFilter|ElemHideException|CSSPropertyFilter|InvalidFilter} | |
905 */ | |
906 ElemHideBase.fromText = function(text, domain, isException, tagName, attrRules,
selector) | |
907 { | |
908 if (!selector) | |
909 { | |
910 if (tagName == "*") | |
911 tagName = ""; | |
912 | |
913 let id = null; | |
914 let additional = ""; | |
915 if (attrRules) | |
916 { | |
917 attrRules = attrRules.match(/\([\w\-]+(?:[$^*]?=[^\(\)"]*)?\)/g); | |
918 for (let rule of attrRules) | |
919 { | |
920 rule = rule.substr(1, rule.length - 2); | |
921 let separatorPos = rule.indexOf("="); | |
922 if (separatorPos > 0) | |
923 { | |
924 rule = rule.replace(/=/, '="') + '"'; | |
925 additional += "[" + rule + "]"; | |
926 } | |
927 else | |
928 { | |
929 if (id) | |
930 return new InvalidFilter(text, Utils.getString("filter_elemhide_dupl
icate_id")); | |
931 | |
932 id = rule; | |
933 } | |
934 } | |
935 } | |
936 | |
937 if (id) | |
938 selector = tagName + "." + id + additional + "," + tagName + "#" + id + ad
ditional; | |
939 else if (tagName || additional) | |
940 selector = tagName + additional; | |
941 else | |
942 return new InvalidFilter(text, Utils.getString("filter_elemhide_nocriteria
")); | |
943 } | |
944 | |
945 if (isException) | |
946 return new ElemHideException(text, domain, selector); | |
947 | |
948 let match = Filter.csspropertyRegExp.exec(selector); | |
949 if (match) | |
950 { | |
951 // CSS property filters are inefficient so we need to make sure that | |
952 // they're only applied if they specify active domains | |
953 if (!/,[^~][^,.]*\.[^,]/.test("," + domain)) | |
954 return new InvalidFilter(text, Utils.getString("filter_cssproperty_nodomai
n")); | |
955 | |
956 return new CSSPropertyFilter(text, domain, selector, match[2], | |
957 selector.substr(0, match.index), | |
958 selector.substr(match.index + match[0].length)); | |
959 } | |
960 | |
961 return new ElemHideFilter(text, domain, selector); | |
962 }; | |
963 | |
964 /** | |
965 * Class for element hiding filters | |
966 * @param {String} text see Filter() | |
967 * @param {String} domains see ElemHideBase() | |
968 * @param {String} selector see ElemHideBase() | |
969 * @constructor | |
970 * @augments ElemHideBase | |
971 */ | |
972 function ElemHideFilter(text, domains, selector) | |
973 { | |
974 ElemHideBase.call(this, text, domains, selector); | |
975 } | |
976 exports.ElemHideFilter = ElemHideFilter; | |
977 | |
978 ElemHideFilter.prototype = | |
979 { | |
980 __proto__: ElemHideBase.prototype, | |
981 | |
982 type: "elemhide" | |
983 }; | |
984 | |
985 /** | |
986 * Class for element hiding exceptions | |
987 * @param {String} text see Filter() | |
988 * @param {String} domains see ElemHideBase() | |
989 * @param {String} selector see ElemHideBase() | |
990 * @constructor | |
991 * @augments ElemHideBase | |
992 */ | |
993 function ElemHideException(text, domains, selector) | |
994 { | |
995 ElemHideBase.call(this, text, domains, selector); | |
996 } | |
997 exports.ElemHideException = ElemHideException; | |
998 | |
999 ElemHideException.prototype = | |
1000 { | |
1001 __proto__: ElemHideBase.prototype, | |
1002 | |
1003 type: "elemhideexception" | |
1004 }; | |
1005 | |
1006 /** | |
1007 * Class for CSS property filters | |
1008 * @param {String} text see Filter() | |
1009 * @param {String} domains see ElemHideBase() | |
1010 * @param {String} selector see ElemHideBase() | |
1011 * @param {String} regexpSource see CSSPropertyFilter.regexpSource | |
1012 * @param {String} selectorPrefix see CSSPropertyFilter.selectorPrefix | |
1013 * @param {String} selectorSuffix see CSSPropertyFilter.selectorSuffix | |
1014 * @constructor | |
1015 * @augments ElemHideBase | |
1016 */ | |
1017 function CSSPropertyFilter(text, domains, selector, regexpSource, | |
1018 selectorPrefix, selectorSuffix) | |
1019 { | |
1020 ElemHideBase.call(this, text, domains, selector); | |
1021 | |
1022 this.regexpSource = regexpSource; | |
1023 this.selectorPrefix = selectorPrefix; | |
1024 this.selectorSuffix = selectorSuffix; | |
1025 } | |
1026 exports.CSSPropertyFilter = CSSPropertyFilter; | |
1027 | |
1028 CSSPropertyFilter.prototype = | |
1029 { | |
1030 __proto__: ElemHideBase.prototype, | |
1031 | |
1032 type: "cssproperty", | |
1033 | |
1034 /** | |
1035 * Expression from which a regular expression should be generated for matching | |
1036 * CSS properties - for delayed creation of the regexpString property | |
1037 * @type String | |
1038 */ | |
1039 regexpSource: null, | |
1040 /** | |
1041 * Substring of CSS selector before properties for the HTML elements that | |
1042 * should be hidden | |
1043 * @type String | |
1044 */ | |
1045 selectorPrefix: null, | |
1046 /** | |
1047 * Substring of CSS selector after properties for the HTML elements that | |
1048 * should be hidden | |
1049 * @type String | |
1050 */ | |
1051 selectorSuffix: null, | |
1052 | |
1053 /** | |
1054 * Raw regular expression string to be used when testing CSS properties | |
1055 * against this filter | |
1056 * @type String | |
1057 */ | |
1058 get regexpString() | |
1059 { | |
1060 // Despite this property being cached, the getter is called | |
1061 // several times on Safari, due to WebKit bug 132872 | |
1062 let prop = Object.getOwnPropertyDescriptor(this, "regexpString"); | |
1063 if (prop) | |
1064 return prop.value; | |
1065 | |
1066 let regexp = Filter.toRegExp(this.regexpSource); | |
1067 Object.defineProperty(this, "regexpString", {value: regexp}); | |
1068 return regexp; | |
1069 } | |
1070 }; | |
OLD | NEW |