#
# Copyright (c) 2006-2024 Wade Alcorn - wade@bindshell.net
# Browser Exploitation Framework (BeEF) - https://beefproject.com
# See the file 'doc/COPYING' for copying permission
#

module BeEF
  module Core
    #
    # @note This module contains a list of utils functions to use when writing commands
    #
    module CommandUtils
      #
      # Format a string to support multiline in javascript.
      # @param [String] text String to convert
      #
      # @return [String] Formatted string
      #
      def format_multiline(text)
        text.gsub(/\n/, '\n')
      end
    end

    #
    # @note The Command Module Context is being used when evaluating code in eruby.
    #   In other words, we use that code to add funky functions to the
    #   javascript templates of our commands.
    #
    class CommandContext < Erubis::Context
      include BeEF::Core::CommandUtils
      #
      # Constructor
      # @param [Hash] hash
      #
      def initialize(hash = nil)
        super(hash)
      end
    end

    #
    # @note This class is the base class for all command modules in the framework.
    #       Two instances of this object are created during the execution of command module.
    #
    class Command
      attr_reader :datastore, :path, :default_command_url, :beefjs_components, :friendlyname,
                  :config
      attr_accessor :zombie, :command_id, :session_id

      include BeEF::Core::CommandUtils
      include BeEF::Core::Constants::Browsers
      include BeEF::Core::Constants::CommandModule

      #
      # Super class controller
      #
      # @param [String] key command module key
      #
      def initialize(key)
        @config = BeEF::Core::Configuration.instance

        @key = key
        @datastore = {}
        @friendlyname = @config.get("beef.module.#{key}.name")
        @output = ''
        @path = @config.get("beef.module.#{key}.path")
        @default_command_url = config.get("beef.module.#{key}.mount")
        @id = @config.get("beef.module.#{key}.db.id")
        @auto_update_zombie = false
        @results = {}
        @beefjs_components = {}
      end

      #
      # This function is called just before the instructions are sent to hooked browser.
      #
      def pre_send; end

      #
      # Callback method. This function is called when the hooked browser sends results back.
      #
      def callback; end

      #
      # If the command requires some data to be sent back, this function will process them.
      # @param [] head
      # @param [Hash] params Hash of parameters
      # @todo Determine argument "head" type
      #
      def process_zombie_response(head, params); end

      #
      # Returns true if the command needs configurations to work. False if not.
      # @deprecated This command should not be used since the implementation of the new configuration system
      #
      def needs_configuration?
        !@datastore.nil?
      end

      #
      # Returns information about the command in a JSON format.
      # @return [String] JSON formatted string
      #
      def to_json(*_args)
        {
          'Name' => @friendlyname,
          'Description' => BeEF::Core::Configuration.instance.get("beef.module.#{@key}.description"),
          'Category' => BeEF::Core::Configuration.instance.get("beef.module.#{@key}.category"),
          'Data' => BeEF::Module.get_options(@key)
        }.to_json
      end

      #
      # Builds the 'datastore' attribute of the command which is used to generate javascript code.
      # @param [Hash] data Data to be inserted into the datastore
      # @todo TODO Confirm argument "data" type
      #
      def build_datastore(data)
        @datastore = JSON.parse data
      rescue StandardError => e
        print_error "Could not build datastore: #{e.message}"
      end

      #
      # Sets the datastore for the callback function. This function is meant to be called by the CommandHandler
      # @param [Hash] http_params HTTP parameters
      # @param [Hash] http_headers HTTP headers
      #
      def build_callback_datastore(result, command_id, beefhook, http_params, http_headers)
        @datastore = { 'http_headers' => {} } # init the datastore

        if !http_params.nil? && !http_headers.nil?
          # get, check and add the http_params to the datastore
          http_params.keys.each do |http_params_key|
            unless BeEF::Filters.is_valid_command_module_datastore_key? http_params_key
              print_error 'http_params_key is invalid'
              return
            end

            http_params_value = Erubis::XmlHelper.escape_xml http_params[http_params_key]
            unless BeEF::Filters.is_valid_command_module_datastore_param?(http_params_value)
              print_error 'http_params_value is invalid'
              return
            end

            # add the checked key and value to the datastore
            @datastore[http_params_key] = http_params_value
          end

          # get, check and add the http_headers to the datastore
          http_headers.keys.each do |http_header_key|
            unless BeEF::Filters.is_valid_command_module_datastore_key? http_header_key
              print_error 'http_header_key is invalid'
              return
            end

            http_header_value = Erubis::XmlHelper.escape_xml http_headers[http_header_key][0]
            unless BeEF::Filters.is_valid_command_module_datastore_param? http_header_value
              print_error 'http_header_value is invalid'
              return
            end

            # add the checked key and value to the datastore
            @datastore['http_headers'][http_header_key] = http_header_value
          end
        end

        @datastore['results'] = result
        @datastore['cid'] = command_id
        @datastore['beefhook'] = beefhook
      end

      #
      # Returns the output of the command. These are the actual instructions sent to the browser.
      # @return [String] The command output
      #
      def output
        f = "#{@path}command.js"
        unless File.exist? f
          print_error "File does not exist: #{f}"
          return
        end

        command = BeEF::Core::Models::Command.find(@command_id)

        @eruby = Erubis::FastEruby.new(File.read(f))

        # data = BeEF::Core::Configuration.instance.get "beef.module.#{@key}"
        cc = BeEF::Core::CommandContext.new
        cc['command_url'] = @default_command_url
        cc['command_id'] = @command_id
        JSON.parse(command['data']).each do |v|
          cc[v['name']] = v['value']
        end

        execute if respond_to?(:execute)

        @output = @eruby.evaluate cc
        @output
      end

      # Saves the results received from the hooked browser
      # @param [Hash] results Results from hooked browser
      def save(results)
        @results = results
      end

      #
      # If nothing else than the file is specified,
      # the function will map the file to a random path without any extension.
      #
      # @param [String] file File to be mounted
      # @param [String] path URL path to mounted file
      # @param [String] extension URL extension
      # @param [Integer] count The amount of times this file can be accessed before being automatically unmounted
      # @deprecated This function is possibly deprecated in place of the API
      #
      def map_file_to_url(file, path = nil, extension = nil, count = 1)
        BeEF::Core::NetworkStack::Handlers::AssetHandler.instance.bind(file, path, extension, count)
      end

      #
      # Tells the framework to load a specific module of the BeEFJS library that the command will be using.
      # @param [String] component String of BeEFJS component to load
      # @note Example: use 'beef.net.local'
      #
      def use(component)
        return if @beefjs_components.include? component

        component_path = '/' + component
        component_path.gsub!(/beef./, '')
        component_path.gsub!(/\./, '/')
        component_path.replace "#{$root_dir}/core/main/client/#{component_path}.js"

        raise "Invalid beefjs component for command module #{@path}" unless File.exist? component_path

        @beefjs_components[component] = component_path
      end

      # @todo TODO Document
      def oc_value(name)
        option = BeEF::Core::Models::OptionCache.where(name: name).first
        return nil unless option

        option.value
      end

      # @todo TODO Document
      def apply_defaults
        @datastore.each do |opt|
          opt['value'] = oc_value(opt['name']) || opt['value']
        end
      end

      @use_template
      @eruby
      @update_zombie
      @results
    end
  end
end
