# -*- coding: binary -*-
require 'rex/ui'
require 'pp'

module Rex
module Ui
module Text

###
#
# The dispatcher shell class is designed to provide a generic means
# of processing various shell commands that may be located in
# different modules or chunks of codes.  These chunks are referred
# to as command dispatchers.  The only requirement for command dispatchers is
# that they prefix every method that they wish to be mirrored as a command
# with the cmd_ prefix.
#
###
module DispatcherShell

  ###
  #
  # Empty template base class for command dispatchers.
  #
  ###
  module CommandDispatcher

    #
    # Initializes the command dispatcher mixin.
    #
    def initialize(shell)
      self.shell = shell
      self.tab_complete_items = []
    end

    #
    # Returns nil for an empty set of commands.
    #
    # This method should be overridden to return a Hash with command
    # names for keys and brief help text for values.
    #
    def commands
    end

    #
    # Returns an empty set of commands.
    #
    # This method should be overridden if the dispatcher has commands that
    # should be treated as deprecated. Deprecated commands will not show up in
    # help and will not tab-complete, but will still be callable.
    #
    def deprecated_commands
      []
    end

    #
    # Wraps shell.print_error
    #
    def print_error(msg = '')
      shell.print_error(msg)
    end

    #
    # Wraps shell.print_status
    #
    def print_status(msg = '')
      shell.print_status(msg)
    end

    #
    # Wraps shell.print_line
    #
    def print_line(msg = '')
      shell.print_line(msg)
    end

    #
    # Wraps shell.print_good
    #
    def print_good(msg = '')
      shell.print_good(msg)
    end

    #
    # Wraps shell.print_warning
    #
    def print_warning(msg = '')
      shell.print_warning(msg)
    end

    #
    # Wraps shell.print
    #
    def print(msg = '')
      shell.print(msg)
    end

    #
    # Print a warning that the called command is deprecated and optionally
    # forward to the replacement +method+ (useful for when commands are
    # renamed).
    #
    def deprecated_cmd(method=nil, *args)
      cmd = caller[0].match(/`cmd_(.*)'/)[1]
      print_error "The #{cmd} command is DEPRECATED"
      if cmd == "db_autopwn"
        print_error "See http://r-7.co/xY65Zr instead"
      elsif method and self.respond_to?("cmd_#{method}")
        print_error "Use #{method} instead"
        self.send("cmd_#{method}", *args)
      end
    end

    def deprecated_help(method=nil)
      cmd = caller[0].match(/`cmd_(.*)_help'/)[1]
      print_error "The #{cmd} command is DEPRECATED"
      if cmd == "db_autopwn"
        print_error "See http://r-7.co/xY65Zr instead"
      elsif method and self.respond_to?("cmd_#{method}_help")
        print_error "Use 'help #{method}' instead"
        self.send("cmd_#{method}_help")
      end
    end

    #
    # Wraps shell.update_prompt
    #
    def update_prompt(prompt=nil, prompt_char = nil, mode = false)
      shell.update_prompt(prompt, prompt_char, mode)
    end

    def cmd_help_help
      print_line "There's only so much I can do"
    end

    #
    # Displays the help banner.  With no arguments, this is just a list of
    # all commands grouped by dispatcher.  Otherwise, tries to use a method
    # named cmd_#{+cmd+}_help for the first dispatcher that has a command
    # named +cmd+.  If no such method exists, uses +cmd+ as a regex to
    # compare against each enstacked dispatcher's name and dumps commands
    # of any that match.
    #
    def cmd_help(cmd=nil, *ignored)
      if cmd
        help_found = false
        cmd_found = false
        shell.dispatcher_stack.each do |dispatcher|
          next unless dispatcher.respond_to?(:commands)
          next if (dispatcher.commands.nil?)
          next if (dispatcher.commands.length == 0)

          if dispatcher.respond_to?("cmd_#{cmd}")
            cmd_found = true
            break unless dispatcher.respond_to? "cmd_#{cmd}_help"
            dispatcher.send("cmd_#{cmd}_help")
            help_found = true
            break
          end
        end

        unless cmd_found
          # We didn't find a cmd, try it as a dispatcher name
          shell.dispatcher_stack.each do |dispatcher|
            if dispatcher.name =~ /#{cmd}/i
              print_line(dispatcher.help_to_s)
              cmd_found = help_found = true
            end
          end
        end
        print_error("No help for #{cmd}, try -h") if cmd_found and not help_found
        print_error("No such command") if not cmd_found
      else
        print(shell.help_to_s)
      end
    end

    #
    # Tab completion for the help command
    #
    # By default just returns a list of all commands in all dispatchers.
    #
    def cmd_help_tabs(str, words)
      return [] if words.length > 1

      tabs = []
      shell.dispatcher_stack.each { |dispatcher|
        tabs += dispatcher.commands.keys
      }
      return tabs
    end

    alias cmd_? cmd_help

    #
    # Return a pretty, user-readable table of commands provided by this
    # dispatcher.
    #
    def help_to_s(opts={})
      # If this dispatcher has no commands, we can't do anything useful.
      return "" if commands.nil? or commands.length == 0

      # Display the commands
      tbl = Table.new(
        'Header'  => "#{self.name} Commands",
        'Indent'  => opts['Indent'] || 4,
        'Columns' =>
          [
            'Command',
            'Description'
          ],
        'ColProps' =>
          {
            'Command' =>
              {
                'MaxWidth' => 12
              }
          })

      commands.sort.each { |c|
        tbl << c
      }

      return "\n" + tbl.to_s + "\n"
    end

    #
    # No tab completion items by default
    #
    attr_accessor :shell, :tab_complete_items

    #
    # Provide a generic tab completion for file names.
    #
    # If the only completion is a directory, this descends into that directory
    # and continues completions with filenames contained within.
    #
    def tab_complete_filenames(str, words)
      matches = ::Readline::FILENAME_COMPLETION_PROC.call(str)
      if matches and matches.length == 1 and File.directory?(matches[0])
        dir = matches[0]
        dir += File::SEPARATOR if dir[-1,1] != File::SEPARATOR
        matches = ::Readline::FILENAME_COMPLETION_PROC.call(dir)
      end
      matches
    end

  end

  #
  # DispatcherShell derives from shell.
  #
  include Shell

  #
  # Initialize the dispatcher shell.
  #
  def initialize(prompt, prompt_char = '>', histfile = nil, framework = nil)
    super

    # Initialze the dispatcher array
    self.dispatcher_stack = []

    # Initialize the tab completion array
    self.tab_words = []
    self.on_command_proc = nil
  end

  #
  # This method accepts the entire line of text from the Readline
  # routine, stores all completed words, and passes the partial
  # word to the real tab completion function. This works around
  # a design problem in the Readline module and depends on the
  # Readline.basic_word_break_characters variable being set to \x00
  #
  def tab_complete(str)
    # Check trailing whitespace so we can tell 'x' from 'x '
    str_match = str.match(/\s+$/)
    str_trail = (str_match.nil?) ? '' : str_match[0]

    # Split the line up by whitespace into words
    str_words = str.split(/[\s\t\n]+/)

    # Append an empty word if we had trailing whitespace
    str_words << '' if str_trail.length > 0

    # Place the word list into an instance variable
    self.tab_words = str_words

    # Pop the last word and pass it to the real method
    tab_complete_stub(self.tab_words.pop)
  end

  # Performs tab completion of a command, if supported
  # Current words can be found in self.tab_words
  #
  def tab_complete_stub(str)
    items = []

    return nil if not str

    # puts "Words(#{tab_words.join(", ")}) Partial='#{str}'"

    # Next, try to match internal command or value completion
    # Enumerate each entry in the dispatcher stack
    dispatcher_stack.each { |dispatcher|

      # If no command is set and it supports commands, add them all
      if (tab_words.empty? and dispatcher.respond_to?('commands'))
        items.concat(dispatcher.commands.keys)
      end

      # If the dispatcher exports a tab completion function, use it
      if(dispatcher.respond_to?('tab_complete_helper'))
        res = dispatcher.tab_complete_helper(str, tab_words)
      else
        res = tab_complete_helper(dispatcher, str, tab_words)
      end

      if (res.nil?)
        # A nil response indicates no optional arguments
        return [''] if items.empty?
      else
        # Otherwise we add the completion items to the list
        items.concat(res)
      end
    }

    # Verify that our search string is a valid regex
    begin
      Regexp.compile(str)
    rescue RegexpError
      str = Regexp.escape(str)
    end

    # @todo - This still doesn't fix some Regexp warnings:
    # ./lib/rex/ui/text/dispatcher_shell.rb:171: warning: regexp has `]' without escape

    # Match based on the partial word
    items.find_all { |e|
      e =~ /^#{str}/
    # Prepend the rest of the command (or it all gets replaced!)
    }.map { |e|
      tab_words.dup.push(e).join(' ')
    }
  end

  #
  # Provide command-specific tab completion
  #
  def tab_complete_helper(dispatcher, str, words)
    items = []

    tabs_meth = "cmd_#{words[0]}_tabs"
    # Is the user trying to tab complete one of our commands?
    if (dispatcher.commands.include?(words[0]) and dispatcher.respond_to?(tabs_meth))
      res = dispatcher.send(tabs_meth, str, words)
      return [] if res.nil?
      items.concat(res)
    else
      # Avoid the default completion list for known commands
      return []
    end

    return items
  end

  #
  # Run a single command line.
  #
  def run_single(line)
    arguments = parse_line(line)
    method    = arguments.shift
    found     = false
    error     = false

    # If output is disabled output will be nil
    output.reset_color if (output)

    if (method)
      entries = dispatcher_stack.length

      dispatcher_stack.each { |dispatcher|
        next if not dispatcher.respond_to?('commands')

        begin
          if (dispatcher.commands.has_key?(method) or dispatcher.deprecated_commands.include?(method))
            self.on_command_proc.call(line.strip) if self.on_command_proc
            run_command(dispatcher, method, arguments)
            found = true
          end
        rescue
          error = $!

          print_error(
            "Error while running command #{method}: #{$!}" +
            "\n\nCall stack:\n#{$@.join("\n")}")
        rescue ::Exception
          error = $!

          print_error(
            "Error while running command #{method}: #{$!}")
        end

        # If the dispatcher stack changed as a result of this command,
        # break out
        break if (dispatcher_stack.length != entries)
      }

      if (found == false and error == false)
        unknown_command(method, line)
      end
    end

    return found
  end

  #
  # Runs the supplied command on the given dispatcher.
  #
  def run_command(dispatcher, method, arguments)
    self.busy = true

    if(blocked_command?(method))
      print_error("The #{method} command has been disabled.")
    else
      dispatcher.send('cmd_' + method, *arguments)
    end
    self.busy = false
  end

  #
  # If the command is unknown...
  #
  def unknown_command(method, line)
    print_error("Unknown command: #{method}.")
  end

  #
  # Push a dispatcher to the front of the stack.
  #
  def enstack_dispatcher(dispatcher)
    self.dispatcher_stack.unshift(inst = dispatcher.new(self))

    inst
  end

  #
  # Pop a dispatcher from the front of the stacker.
  #
  def destack_dispatcher
    self.dispatcher_stack.shift
  end

  #
  # Adds the supplied dispatcher to the end of the dispatcher stack so that
  # it doesn't affect any enstack'd dispatchers.
  #
  def append_dispatcher(dispatcher)
    inst = dispatcher.new(self)
    self.dispatcher_stack.each { |disp|
      if (disp.name == inst.name)
        raise RuntimeError.new("Attempting to load already loaded dispatcher #{disp.name}")
      end
    }
    self.dispatcher_stack.push(inst)

    inst
  end

  #
  # Removes the supplied dispatcher instance.
  #
  def remove_dispatcher(name)
    self.dispatcher_stack.delete_if { |inst|
      (inst.name == name)
    }
  end

  #
  # Returns the current active dispatcher
  #
  def current_dispatcher
    self.dispatcher_stack[0]
  end

  #
  # Return a readable version of a help banner for all of the enstacked
  # dispatchers.
  #
  # See +CommandDispatcher#help_to_s+
  #
  def help_to_s(opts = {})
    str = ''

    dispatcher_stack.reverse.each { |dispatcher|
      str << dispatcher.help_to_s
    }

    return str
  end


  #
  # Returns nil for an empty set of blocked commands.
  #
  def blocked_command?(cmd)
    return false if not self.blocked
    self.blocked.has_key?(cmd)
  end

  #
  # Block a specific command
  #
  def block_command(cmd)
    self.blocked ||= {}
    self.blocked[cmd] = true
  end

  #
  # Unblock a specific command
  #
  def unblock_command(cmd)
    self.blocked || return
    self.blocked.delete(cmd)
  end


  attr_accessor :dispatcher_stack # :nodoc:
  attr_accessor :tab_words # :nodoc:
  attr_accessor :busy # :nodoc:
  attr_accessor :blocked # :nodoc:

end

end
end
end
