# encoding: UTF-8

class WpscanOptions
  ACCESSOR_OPTIONS = [
    :batch,
    :enumerate_plugins,
    :enumerate_only_vulnerable_plugins,
    :enumerate_all_plugins,
    :enumerate_themes,
    :enumerate_only_vulnerable_themes,
    :enumerate_all_themes,
    :enumerate_timthumbs,
    :enumerate_usernames,
    :enumerate_usernames_range,
    :no_color,
    :log,
    :proxy,
    :proxy_auth,
    :threads,
    :url,
    :vhost,
    :wordlist,
    :force,
    :update,
    :verbose,
    :username,
    :usernames,
    :password,
    :follow_redirection,
    :wp_content_dir,
    :wp_plugins_dir,
    :help,
    :config_file,
    :cookie,
    :exclude_content_based,
    :basic_auth,
    :debug_output,
    :version,
    :user_agent,
    :random_agent,
    :cache_ttl,
    :request_timeout,
    :connect_timeout,
    :max_threads,
    :no_banner,
    :throttle,
    :disable_accept_header,
    :disable_referer,
    :cache_dir,
    :disable_tls_checks
  ]

  attr_accessor *ACCESSOR_OPTIONS

  def initialize
    ACCESSOR_OPTIONS.each do |option|
      instance_variable_set("@#{option}", nil)
    end
  end

  def url=(url)
    raise  Exception.new('Empty URL given') if url.nil? || url == ''

    url = Addressable::URI.parse(url).normalize.to_s unless url.ascii_only?

    @url = URI.parse(add_http_protocol(url)).to_s
  end

  def vhost=(vhost)
    @vhost = vhost
  end

  def threads=(threads)
    @threads = threads.is_a?(Integer) ? threads : threads.to_i
  end

  def wordlist=(wordlist)
    if File.exists?(wordlist)
      @wordlist = wordlist
    else
      raise "The file #{wordlist} does not exist"
    end
  end

  def usernames=(file)
    fail "The file #{file} does not exist" unless File.exists?(file)

    @usernames = file
  end

  def proxy=(proxy)
    if proxy.index(':') == nil
      raise 'Invalid proxy format. Should be host:port.'
    else
      @proxy = proxy
    end
  end

  def proxy_auth=(auth)
    if auth.index(':') == nil
      raise 'Invalid proxy auth format, username:password expected'
    else
      @proxy_auth = auth
    end
  end

  def enumerate_plugins=(enumerate_plugins)
    if enumerate_plugins === true and (@enumerate_all_plugins === true or @enumerate_only_vulnerable_plugins === true)
      raise 'Please choose only one plugin enumeration option'
    else
      @enumerate_plugins = enumerate_plugins
    end
  end

  def enumerate_only_vulnerable_plugins=(enumerate_only_vulnerable_plugins)
    if enumerate_only_vulnerable_plugins === true and (@enumerate_all_plugins === true or @enumerate_plugins === true)
      raise 'Please choose only one plugin enumeration option'
    else
      @enumerate_only_vulnerable_plugins = enumerate_only_vulnerable_plugins
    end
  end

  def enumerate_all_plugins=(enumerate_all_plugins)
    if enumerate_all_plugins === true and (@enumerate_plugins === true or @enumerate_only_vulnerable_plugins === true)
      raise 'Please choose only one plugin enumeration option'
    else
      @enumerate_all_plugins = enumerate_all_plugins
    end
  end

  def enumerate_themes=(enumerate_themes)
    if enumerate_themes === true and (@enumerate_all_themes === true or @enumerate_only_vulnerable_themes === true)
      raise 'Please choose only one theme enumeration option'
    else
      @enumerate_themes = enumerate_themes
    end
  end

  def enumerate_only_vulnerable_themes=(enumerate_only_vulnerable_themes)
    if enumerate_only_vulnerable_themes === true and (@enumerate_all_themes === true or @enumerate_themes === true)
      raise 'Please choose only one theme enumeration option'
    else
      @enumerate_only_vulnerable_themes = enumerate_only_vulnerable_themes
    end
  end

  def enumerate_all_themes=(enumerate_all_themes)
    if enumerate_all_themes === true and (@enumerate_themes === true or @enumerate_only_vulnerable_themes === true)
      raise 'Please choose only one theme enumeration option'
    else
      @enumerate_all_themes = enumerate_all_themes
    end
  end

  def basic_auth=(basic_auth)
    raise 'Invalid basic authentication format, login:password expected' if basic_auth.index(':').nil?
    @basic_auth = "Basic #{Base64.encode64(basic_auth).chomp}"
  end

  def debug_output=(debug_output)
    Typhoeus::Config.verbose = debug_output
  end

  def has_options?
    !to_h.empty?
  end

  def random_agent=(useless)
    @user_agent = get_random_user_agent
  end

  # return Hash
  def to_h
    options = {}

    ACCESSOR_OPTIONS.each do |option|
      instance_variable = instance_variable_get("@#{option}")

      unless instance_variable.nil?
        options[:"#{option}"] = instance_variable
      end
    end
    options
  end

  # Will load the options from ARGV
  # return WpscanOptions
  def self.load_from_arguments
    wpscan_options = WpscanOptions.new

    if ARGV.length > 0
      WpscanOptions.get_opt_long.each do |opt, arg|
        wpscan_options.set_option_from_cli(opt, arg)
      end
    end

    wpscan_options
  end

  # string cli_option : --url, -u, --proxy etc
  # string cli_value : the option value
  def set_option_from_cli(cli_option, cli_value)

    if WpscanOptions.is_long_option?(cli_option)
      self.send(
          WpscanOptions.option_to_instance_variable_setter(cli_option),
          cli_value
      )
    elsif cli_option === '--enumerate' # Special cases
      # Default value if no argument is given
      cli_value = 'vt,tt,u,vp' if cli_value.length == 0

      enumerate_options_from_string(cli_value)
    else
      text = "Unknown option : #{cli_option}"
      text << " with value #{cli_value}" if (cli_value && !cli_value.empty?)
      raise text
    end
  end

  # Will set enumerate_* from the string value
  # IE : if value = vp => :enumerate_only_vulnerable_plugins will be set to true
  # multiple enumeration are possible : 'u,p' => :enumerate_usernames and :enumerate_plugins
  # Special case for usernames, a range is possible : u[1-10] will enumerate usernames from 1 to 10
  def enumerate_options_from_string(value)
    # Usage of self is mandatory because there are overridden setters

    value = value.split(',').map { |c| c.downcase }

    self.enumerate_only_vulnerable_plugins = true if value.include?('vp')

    self.enumerate_plugins = true if value.include?('p')

    self.enumerate_all_plugins = true if value.include?('ap')

    @enumerate_timthumbs = true if value.include?('tt')

    self.enumerate_only_vulnerable_themes = true if value.include?('vt')

    self.enumerate_themes = true if value.include?('t')

    self.enumerate_all_themes = true if value.include?('at')

    value.grep(/^u/) do |username_enum_value|
      @enumerate_usernames = true
      # Check for usernames range
      matches = %r{\[([\d]+)-([\d]+)\]}.match(username_enum_value)
      if matches
        @enumerate_usernames_range = (matches[1].to_i..matches[2].to_i)
      end
    end

  end

  protected
  # Even if a short option is given (IE : -u), the long one will be returned (IE : --url)
  def self.get_opt_long
    GetoptLong.new(
      ['--url', '-u', GetoptLong::REQUIRED_ARGUMENT],
      ['--vhost',GetoptLong::OPTIONAL_ARGUMENT],
      ['--enumerate', '-e', GetoptLong::OPTIONAL_ARGUMENT],
      ['--username', '-U', GetoptLong::REQUIRED_ARGUMENT],
      ['--usernames', GetoptLong::REQUIRED_ARGUMENT],
      ['--wordlist', '-w', GetoptLong::REQUIRED_ARGUMENT],
      ['--threads', '-t', GetoptLong::REQUIRED_ARGUMENT],
      ['--force', '-f', GetoptLong::NO_ARGUMENT],
      ['--user-agent', '-a', GetoptLong::REQUIRED_ARGUMENT],
      ['--random-agent', '-r', GetoptLong::NO_ARGUMENT],
      ['--help', '-h', GetoptLong::NO_ARGUMENT],
      ['--verbose', '-v', GetoptLong::NO_ARGUMENT],
      ['--proxy', GetoptLong::REQUIRED_ARGUMENT],
      ['--proxy-auth', GetoptLong::REQUIRED_ARGUMENT],
      ['--update', GetoptLong::NO_ARGUMENT],
      ['--follow-redirection', GetoptLong::NO_ARGUMENT],
      ['--wp-content-dir', GetoptLong::REQUIRED_ARGUMENT],
      ['--wp-plugins-dir', GetoptLong::REQUIRED_ARGUMENT],
      ['--config-file', '-c', GetoptLong::REQUIRED_ARGUMENT],
      ['--exclude-content-based', GetoptLong::REQUIRED_ARGUMENT],
      ['--basic-auth', GetoptLong::REQUIRED_ARGUMENT],
      ['--debug-output', GetoptLong::NO_ARGUMENT],
      ['--version', GetoptLong::NO_ARGUMENT],
      ['--cache-ttl', GetoptLong::REQUIRED_ARGUMENT],
      ['--request-timeout', GetoptLong::REQUIRED_ARGUMENT],
      ['--connect-timeout', GetoptLong::REQUIRED_ARGUMENT],
      ['--max-threads', GetoptLong::REQUIRED_ARGUMENT],
      ['--batch', GetoptLong::NO_ARGUMENT],
      ['--no-color', GetoptLong::NO_ARGUMENT],
      ['--cookie', GetoptLong::REQUIRED_ARGUMENT],
      ['--log', GetoptLong::NO_ARGUMENT],
      ['--no-banner', GetoptLong::NO_ARGUMENT],
      ['--throttle', GetoptLong::REQUIRED_ARGUMENT],
      ['--disable-accept-header', GetoptLong::NO_ARGUMENT],
      ['--disable-referer', GetoptLong::NO_ARGUMENT],
      ['--cache-dir', GetoptLong::REQUIRED_ARGUMENT],
      ['--disable-tls-checks', GetoptLong::NO_ARGUMENT],
    )
  end

  def self.is_long_option?(option)
    ACCESSOR_OPTIONS.include?(:"#{WpscanOptions.clean_option(option)}")
  end

  # Will removed the '-' or '--' chars at the beginning of option
  # and replace any remaining '-' by '_'
  #
  # param string option
  # return string
  def self.clean_option(option)
    cleaned_option = option.gsub(/^--?/, '')
    cleaned_option.gsub(/-/, '_')
  end

  def self.option_to_instance_variable_setter(option)
    cleaned_option = WpscanOptions.clean_option(option)
    option_syms = ACCESSOR_OPTIONS.grep(%r{^#{cleaned_option}$})

    option_syms.length == 1 ? :"#{option_syms.at(0)}=" : nil
  end

end
