Index: lib/matcher.js |
=================================================================== |
--- a/lib/matcher.js |
+++ b/lib/matcher.js |
@@ -18,16 +18,17 @@ |
"use strict"; |
/** |
* @fileOverview Matcher class implementing matching addresses against |
* a list of filters. |
*/ |
const {RegExpFilter, WhitelistFilter} = require("./filterClasses"); |
+const {suffixes} = require("./domain"); |
/** |
* Regular expression for matching a keyword in a filter. |
* @type {RegExp} |
*/ |
const keywordRegExp = /[^a-z0-9%*][a-z0-9%]{3,}(?=[^a-z0-9%*])/; |
/** |
@@ -58,16 +59,23 @@ |
* @type {number} |
*/ |
const WHITELIST_ONLY_TYPES = RegExpFilter.typeMap.DOCUMENT | |
RegExpFilter.typeMap.ELEMHIDE | |
RegExpFilter.typeMap.GENERICHIDE | |
RegExpFilter.typeMap.GENERICBLOCK; |
/** |
+ * Map to be used instead when a filter has a blank <code>domains</code> |
+ * property. |
+ * @type {Map.<string, boolean>} |
+ */ |
+let defaultDomains = new Map([["", true]]); |
+ |
+/** |
* Yields individual non-default types from a filter's type mask. |
* @param {number} contentType A filter's type mask. |
* @yields {number} |
*/ |
function* nonDefaultTypes(contentType) |
{ |
for (let mask = contentType & NON_DEFAULT_TYPES, bitIndex = 0; |
mask != 0; mask >>>= 1, bitIndex++) |
@@ -167,32 +175,42 @@ |
/** |
* Lookup table for complex filters by their associated keyword |
* @type {Map.<string,(RegExpFilter|Set.<RegExpFilter>)>} |
* @private |
*/ |
this._complexFiltersByKeyword = new Map(); |
/** |
+ * Lookup table of domain maps for complex filters by their associated |
+ * keyword |
+ * @type {Map.<string,Map.<string,(RegExpFilter| |
+ * Map.<RegExpFilter,boolean>)>>} |
+ * @private |
+ */ |
+ this._filterDomainMapsByKeyword = new Map(); |
+ |
+ /** |
* Lookup table of type-specific lookup tables for complex filters by their |
* associated keyword |
* @type {Map.<string,Map.<string,(RegExpFilter|Set.<RegExpFilter>)>>} |
* @private |
*/ |
this._filterMapsByType = new Map(); |
} |
/** |
* Removes all known filters |
*/ |
clear() |
{ |
this._keywordByFilter.clear(); |
this._simpleFiltersByKeyword.clear(); |
this._complexFiltersByKeyword.clear(); |
+ this._filterDomainMapsByKeyword.clear(); |
this._filterMapsByType.clear(); |
} |
/** |
* Adds a filter to the matcher |
* @param {RegExpFilter} filter |
*/ |
add(filter) |
@@ -216,16 +234,45 @@ |
for (let type of nonDefaultTypes(filter.contentType)) |
{ |
let map = this._filterMapsByType.get(type); |
if (!map) |
this._filterMapsByType.set(type, map = new Map()); |
addFilterByKeyword(filter, keyword, map); |
} |
+ |
+ let filtersByDomain = this._filterDomainMapsByKeyword.get(keyword); |
+ if (!filtersByDomain) |
+ this._filterDomainMapsByKeyword.set(keyword, filtersByDomain = new Map()); |
+ |
+ for (let [domain, include] of filter.domains || defaultDomains) |
+ { |
+ if (!include && domain == "") |
+ continue; |
+ |
+ let map = filtersByDomain.get(domain); |
+ if (!map) |
+ { |
+ filtersByDomain.set(domain, include ? filter : |
+ map = new Map([[filter, false]])); |
+ } |
+ else if (map.size == 1 && !(map instanceof Map)) |
+ { |
+ if (filter != map) |
+ { |
+ filtersByDomain.set(domain, new Map([[map, true], |
+ [filter, include]])); |
+ } |
+ } |
+ else |
+ { |
+ map.set(filter, include); |
+ } |
+ } |
} |
/** |
* Removes a filter from the matcher |
* @param {RegExpFilter} filter |
*/ |
remove(filter) |
{ |
@@ -245,16 +292,40 @@ |
return; |
for (let type of nonDefaultTypes(filter.contentType)) |
{ |
let map = this._filterMapsByType.get(type); |
if (map) |
removeFilterByKeyword(filter, keyword, map); |
} |
+ |
+ let filtersByDomain = this._filterDomainMapsByKeyword.get(keyword); |
+ if (filtersByDomain) |
+ { |
+ let domains = filter.domains || defaultDomains; |
+ for (let domain of domains.keys()) |
+ { |
+ let map = filtersByDomain.get(domain); |
+ if (map) |
+ { |
+ if (map.size > 1 || map instanceof Map) |
+ { |
+ map.delete(filter); |
+ |
+ if (map.size == 0) |
+ filtersByDomain.delete(domain); |
+ } |
+ else if (filter == map) |
+ { |
+ filtersByDomain.delete(domain); |
+ } |
+ } |
+ } |
+ } |
} |
/** |
* Chooses a keyword to be associated with the filter |
* @param {Filter} filter |
* @returns {string} keyword or an empty string if no keyword could be found |
* @protected |
*/ |
@@ -286,16 +357,119 @@ |
result = candidate; |
resultCount = count; |
resultLength = candidate.length; |
} |
} |
return result; |
} |
+ _checkEntryMatchSimple(keyword, location, typeMask, docDomain, thirdParty, |
+ sitekey, specificOnly, collection) |
+ { |
+ let filters = this._simpleFiltersByKeyword.get(keyword); |
+ if (filters) |
+ { |
+ let lowerCaseLocation = location.toLowerCase(); |
+ |
+ for (let filter of filters) |
+ { |
+ if (specificOnly && !(filter instanceof WhitelistFilter)) |
+ continue; |
+ |
+ if (filter.matchesLocation(location, lowerCaseLocation)) |
+ { |
+ if (!collection) |
+ return filter; |
+ |
+ collection.push(filter); |
+ } |
+ } |
+ } |
+ |
+ return null; |
+ } |
+ |
+ _checkEntryMatchForType(keyword, location, typeMask, docDomain, thirdParty, |
+ sitekey, specificOnly, collection) |
+ { |
+ let filtersForType = this._filterMapsByType.get(typeMask); |
+ if (filtersForType) |
+ { |
+ let filters = filtersForType.get(keyword); |
+ if (filters) |
+ { |
+ for (let filter of filters) |
+ { |
+ if (specificOnly && filter.isGeneric() && |
+ !(filter instanceof WhitelistFilter)) |
+ continue; |
+ |
+ if (filter.matches(location, typeMask, docDomain, thirdParty, |
+ sitekey)) |
+ { |
+ if (!collection) |
+ return filter; |
+ |
+ collection.push(filter); |
+ } |
+ } |
+ } |
+ } |
+ |
+ return null; |
+ } |
+ |
+ _checkEntryMatchByDomain(keyword, location, typeMask, docDomain, thirdParty, |
+ sitekey, specificOnly, collection) |
+ { |
+ let filtersByDomain = this._filterDomainMapsByKeyword.get(keyword); |
+ if (filtersByDomain) |
+ { |
+ // The code in this block is similar to the generateStyleSheetForDomain |
+ // function in lib/elemHide.js. |
+ |
+ if (docDomain) |
+ { |
+ if (docDomain[docDomain.length - 1] == ".") |
+ docDomain = docDomain.replace(/\.+$/, ""); |
+ |
+ docDomain = docDomain.toLowerCase(); |
+ } |
+ |
+ let excluded = new Set(); |
+ |
+ for (let suffix of suffixes(docDomain || "", !specificOnly)) |
+ { |
+ let filters = filtersByDomain.get(suffix); |
+ if (filters) |
+ { |
+ for (let [filter, include] of filters.entries()) |
+ { |
+ if (!include) |
+ { |
+ excluded.add(filter); |
+ } |
+ else if ((excluded.size == 0 || !excluded.has(filter)) && |
+ filter.matchesWithoutDomain(location, typeMask, |
+ thirdParty, sitekey)) |
+ { |
+ if (!collection) |
+ return filter; |
+ |
+ collection.push(filter); |
+ } |
+ } |
+ } |
+ } |
+ } |
+ |
+ return null; |
+ } |
+ |
/** |
* Checks whether the entries for a particular keyword match a URL |
* @param {string} keyword |
* @param {string} location |
* @param {number} typeMask |
* @param {string} [docDomain] |
* @param {boolean} [thirdParty] |
* @param {string} [sitekey] |
@@ -309,70 +483,38 @@ |
*/ |
checkEntryMatch(keyword, location, typeMask, docDomain, thirdParty, sitekey, |
specificOnly, collection) |
{ |
// We need to skip the simple (location-only) filters if the type mask does |
// not contain any default content types. |
if (!specificOnly && (typeMask & DEFAULT_TYPES) != 0) |
{ |
- let simpleSet = this._simpleFiltersByKeyword.get(keyword); |
- if (simpleSet) |
- { |
- let lowerCaseLocation = location.toLowerCase(); |
- |
- for (let filter of simpleSet) |
- { |
- if (filter.matchesLocation(location, lowerCaseLocation)) |
- { |
- if (!collection) |
- return filter; |
- |
- collection.push(filter); |
- } |
- } |
- } |
+ let filter = this._checkEntryMatchSimple(keyword, location, typeMask, |
+ docDomain, thirdParty, sitekey, |
+ specificOnly, collection); |
+ if (filter) |
+ return filter; |
} |
- let complexSet = null; |
- |
// If the type mask contains a non-default type (first condition) and it is |
// the only type in the mask (second condition), we can use the |
// type-specific map, which typically contains a lot fewer filters. This |
// enables faster lookups for whitelisting types like $document, $elemhide, |
// and so on, as well as other special types like $csp. |
if ((typeMask & NON_DEFAULT_TYPES) != 0 && (typeMask & typeMask - 1) == 0) |
{ |
- let map = this._filterMapsByType.get(typeMask); |
- if (map) |
- complexSet = map.get(keyword); |
- } |
- else |
- { |
- complexSet = this._complexFiltersByKeyword.get(keyword); |
+ return this._checkEntryMatchForType(keyword, location, typeMask, |
+ docDomain, thirdParty, sitekey, |
+ specificOnly, collection); |
} |
- if (complexSet) |
- { |
- for (let filter of complexSet) |
- { |
- if (specificOnly && filter.isGeneric()) |
- continue; |
- |
- if (filter.matches(location, typeMask, docDomain, thirdParty, sitekey)) |
- { |
- if (!collection) |
- return filter; |
- |
- collection.push(filter); |
- } |
- } |
- } |
- |
- return null; |
+ return this._checkEntryMatchByDomain(keyword, location, typeMask, |
+ docDomain, thirdParty, sitekey, |
+ specificOnly, collection); |
} |
/** |
* Tests whether the URL matches any of the known filters |
* @param {string} location |
* URL to be tested |
* @param {number} typeMask |
* bitmask of content / request types to match |