xiaody/adblock-minus

View on GitHub
lib/FilterClasses.js

Summary

Maintainability
B
7 hrs
Test Coverage
import {isThirdParty, extractDomain} from './utils'

const Filter = {
  elemhideRegExp: /^([^/*|@"!]*?)##(.+)$/, // whitelist elementHide filter is not supported
  optionsRegExp: /\$(~?[\w-]+(?:=[^,\s]+)?(?:,~?[\w-]+(?:=[^,\s]+)?)*)$/,

  fromText (text = '') {
    text = text.trim()
    if (!text || text.startsWith('!') || text.includes('#@#')) return
    if (/^\[Adblock Plus/i.test(text)) return

    if (text.includes('##') && this.elemhideRegExp.test(text)) {
      let domainStr = RegExp.$1
      let selectorStr = RegExp.$2
      return new ElemHideFilter(domainStr.replace(/\s/g, ''), selectorStr.trim())
    } else {
      return RegExpFilter.fromText(text.replace(/\s/g, ''))
    }
  },
  isRegExpFilter (filter) { return filter instanceof RegExpFilter },
  isElemHideFilter (filter) { return filter instanceof ElemHideFilter },
  isWhitelistFilter (filter) { return filter instanceof WhitelistFilter },
  isBlockingFilter (filter) { return filter instanceof BlockingFilter }
}

class ActiveFilter {
  constructor (domainSource) {
    this.domains = null
    this.domainSource = domainSource
  }
  /* make it possible to lazy-calc `.domains`,
   * cuz most RegExpFilter won't even need it */
  calcDomains () {
    if (this.domainSource) {
      this.domains = new Map()
      let domains = this.domainSource.split(this.domainSeparator)
      if (domains.length === 1 && domains[0][0] !== '~') {
        this.domains.set('', false)
        this.domains.set(domains[0], true)
      } else {
        let hasIncludes = false
        let i = 0
        for (let l = domains.length, domain, include; i < l; i++) {
          domain = domains[i]
          if (domain === '') continue

          if (domain[0] === '~') {
            include = false
            domain = domain.substr(1)
          } else {
            include = true
            hasIncludes = true
          }
          this.domains.set(domain, include)
        }
        this.domains.set('', !hasIncludes)
      }
    }
    this.domainSource = ''
  }
  isActiveOnDomain (docDomain = '') {
    if (this.domainSource) this.calcDomains()
    if (!this.domains) return true
    docDomain = docDomain.toUpperCase()
    let nextDot
    do {
      if (this.domains.has(docDomain)) {
        return this.domains.get(docDomain)
      }
      nextDot = docDomain.indexOf('.')
      if (nextDot < 0) break
      docDomain = docDomain.substr(nextDot + 1)
    } while (true)

    return this.domains.get('')
  }
}

class RegExpFilter extends ActiveFilter {
  constructor (regexpSrc, contentType, domains, thirdParty) {
    super(domains)
    this.regexpSrc = regexpSrc
    this.regexp = null // lazy generate from @regexpSrc
    this.contentType = contentType || this.contentType
    this.thirdParty = thirdParty
  }
  genRegexp () {
    const regexpSrc = this.regexpSrc
    if (!regexpSrc) {
      this.regexp = /./
    } else if (regexpSrc.length > 2 && regexpSrc.startsWith('/') && regexpSrc.endsWith('/')) {
      // The filter is a regular expression - convert it immediately.
      this.regexp = new RegExp(regexpSrc.substr(1, regexpSrc.length - 2))
    } else {
      this.regexp = new RegExp(RegExpFilter.convert(regexpSrc))
    }
  }
  matches (location, contentType, docDomain) {
    if (!this.regexp) this.genRegexp()
    if (typeof contentType === 'string') {
      contentType = contentType.toUpperCase()
    }
    return !!(
      this.isActiveOnDomain(docDomain) &&
      (RegExpFilter.typeMap.get(contentType || 'OTHER') & this.contentType) &&
      this.regexp.test(location) &&
      (typeof this.thirdParty === 'undefined' ||
      this.thirdParty === isThirdParty(extractDomain(location), docDomain))
    )
  }
  /* convert abpexp to regexp */
  static convert (abpexp) {
    return abpexp
      .replace(/\*+/g, '*').replace(/^\*|\*$/g, '').replace(/\^\|$/, '^')
      .replace(/\W/g, '\\$&')
      .replace(/\\\*/g, '.*')
      .replace(/\\\^/g, '(?:[\\x00-\\x24\\x26-\\x2C\\x2F\\x3A-\\x40\\x5B-\\x5E\\x60\\x7B-\\x7F]|$)')
      .replace(/^\\\|\\\|/, '^[\\w\\-]+:\\/+(?!\\/)(?:[^.\\/]+\\.)?')
      .replace(/^\\\|/, '^').replace(/\\\|$/, '$')
  }

  static fromText (text) {
    let blocking = true

    if (text.indexOf('@@') === 0) {
      blocking = false
      text = text.substr(2)
    }

    let contentType, domains, thirdParty, options

    if (text.includes('$') && Filter.optionsRegExp.test(text)) {
      options = RegExp.$1.toUpperCase().split(',')
      text = RegExp.leftContext
      for (let i = options.length, option, value, separatorIndex; i--;) {
        option = options[i]
        value = null
        separatorIndex = option.indexOf('=')
        if (separatorIndex >= 0) {
          value = option.substr(separatorIndex + 1)
          option = option.substr(0, separatorIndex)
        }
        option = option.replace('-', '_')
        if (RegExpFilter.typeMap.has(option)) {
          contentType = contentType || 0
          contentType |= RegExpFilter.typeMap.get(option)
        } else if (option[0] === '~' && RegExpFilter.typeMap.has(option.substr(1))) {
          contentType = contentType || RegExpFilter.prototype.contentType
          contentType &= ~RegExpFilter.typeMap.get(option.substr(1))
        } else if (option === 'DOMAIN' && value) {
          domains = value
        } else if (option === 'THIRD_PARTY') {
          thirdParty = true
        } else if (option === '~THIRD_PARTY') {
          thirdParty = false
        } else {
          return
        }
      }
    }

    let Constructor = blocking ? BlockingFilter : WhitelistFilter
    return new Constructor(text, contentType, domains, thirdParty)
  }
}
Object.assign(RegExpFilter.prototype, {
  domainSeparator: '|',
  contentType: (1 << 10) - 1
})
RegExpFilter.typeMap = new Map([
  [ 'OTHER', 1 << 0 ],
  [ 'SCRIPT', 1 << 1 ],
  [ 'IMAGE', 1 << 2 ],
  [ 'STYLESHEET', 1 << 3 ],
  [ 'OBJECT', 1 << 4 ],
  [ 'OBJECT_SUBREQUEST', 1 << 4 ], // same as OBJECT
  [ 'SUBDOCUMENT', 1 << 5 ],
  [ 'DOCUMENT', 1 << 6 ],
  [ 'XMLHTTPREQUEST', 1 << 7 ]
])

class BlockingFilter extends RegExpFilter { }
class WhitelistFilter extends RegExpFilter { }

class ElemHideFilter extends ActiveFilter {
  constructor (domains, selector) {
    super(domains ? domains.toUpperCase() : null)
    this.selector = selector
    this.calcDomains()
  }
}
Object.assign(ElemHideFilter.prototype, {
  domainSeparator: ','
})

export { Filter, ActiveFilter, RegExpFilter, BlockingFilter, WhitelistFilter, ElemHideFilter }