require 'redcarpet'
require 'erb'

module Redcarpet
  module Render
    class MsfMdHTML < Redcarpet::Render::HTML

      def block_code(code, language)
        code = $1 if code =~ /^<ruby>(.+)<\/ruby>/m

        "<pre>" \
          "<code>#{CGI.escape_html(code)}</code>" \
        "</pre>"
      end


      def list(content, list_type)
        if list_type == :unordered && content.scan(/<li>/).flatten.length > 15
          %Q|<p><div id=\"long_list\"><ul>#{content}<ul></div></p>|
        elsif list_type == :unordered
          %Q|<ul>#{content}</ul>|
        elsif list_type == :ordered
          %Q|<ol>#{content}</ol>|
        else
          content
        end
      end

      def header(text, header_level)
        %Q|<h#{header_level}>#{text}</h#{header_level}><hr>|
      end

      def table(header, body)
        %Q|<table class="kb_table" cellpadding="5" cellspacing="2" border="1">#{header}#{body}</table><br>|
      end

    end
  end
end


module Msf
  module Util
    module DocumentGenerator
      class DocumentNormalizer

        #
        # Markdown templates
        #

        CSS_BASE_PATH                   = 'markdown.css'
        HTML_TEMPLATE                   = 'html_template.erb'
        TEMPLATE_PATH                   = 'default_template.erb'

        #
        # Demo templates
        #

        REMOTE_EXPLOIT_DEMO_TEMPLATE    = 'remote_exploit_demo_template.erb'
        BES_DEMO_TEMPLATE               = 'bes_demo_template.erb'
        HTTPSERVER_DEMO_TEMPLATE        = 'httpserver_demo_template.erb'
        GENERIC_DEMO_TEMPLATE           = 'generic_demo_template.erb'
        LOCALEXPLOIT_DEMO_TEMPLATE      = 'localexploit_demo_template.erb'
        POST_DEMO_TEMPLATE              = 'post_demo_template.erb'
        AUXILIARY_SCANNER_DEMO_TEMPLATE = 'auxiliary_scanner_template.erb'
        PAYLOAD_DEMO_TEMPLATE           = 'payload_demo_template.erb'
        EVASION_DEMO_TEMPLATE           = 'evasion_demo_template.erb'

        # Special messages
        NO_CVE_MESSAGE = %Q|CVE: [Not available](https://docs.metasploit.com/docs/using-metasploit/other/why-cve-is-not-available.html)|


        # Returns the module document in HTML form.
        #
        # @param items [Hash] Items to be documented.
        # @param kb [String] Additional information to be added in the doc.
        # @return [String] HTML.
        def get_md_content(items, kb)
          @md_template ||= lambda {
            template = ''
            path = File.expand_path(File.join(Msf::Config.data_directory, 'markdown_doc', TEMPLATE_PATH))
            File.open(path, 'rb') { |f| template = f.read }
            return template
          }.call
          md_to_html(ERB.new(@md_template).result(binding()), kb)
        end


        private


        # Returns the CSS code for the HTML document.
        #
        # @return [String]
        def load_css
          @css ||= lambda {
            data = ''
            path = File.expand_path(File.join(Msf::Config.data_directory, 'markdown_doc', CSS_BASE_PATH))
            File.open(path, 'rb') { |f| data = f.read }
            return data
          }.call
        end


        # Returns the HTML document.
        #
        # @param md [String] Markdown document.
        # @param kb [String] Additional information to add.
        # @return [String] HTML document.
        def md_to_html(md, kb)
          extensions = {
              escape_html: true
          }

          render_options = {
            fenced_code_blocks: true,
            no_intra_emphasis: true,
            tables: true
          }

          html_renderer = Redcarpet::Render::MsfMdHTML.new(extensions)
          r = Redcarpet::Markdown.new(html_renderer, render_options)
          ERB.new(@html_template ||= lambda {
            html_template = ''
            path = File.expand_path(File.join(Msf::Config.data_directory, 'markdown_doc', HTML_TEMPLATE))
            File.open(path, 'rb') { |f| html_template = f.read }
            return html_template
          }.call).result(binding())
        end


        # Returns the markdown format for pull requests.
        #
        # @param pull_requests [Hash] Pull requests
        # @return [String]
        def normalize_pull_requests(pull_requests)
          if pull_requests.kind_of?(PullRequestFinder::Exception)
            error = pull_requests.message
            case error
            when /GITHUB_OAUTH_TOKEN/i
              error << " [See how]("
              error << "https://help.github.com/articles/creating-an-access-token-for-command-line-use/"
              error << ")"
            end
            return error
          end

          formatted_pr = []

          pull_requests.each_pair do |number, pr|
            formatted_pr << "* [##{number} #{pr[:title]}](https://github.com/rapid7/metasploit-framework/pull/#{number})"
          end

          formatted_pr * "\n"
        end


        # Returns the markdown format for module description.
        #
        # @param description [String] Module description.
        # @return [String]
        def normalize_description(description)
          Rex::Text.wordwrap(Rex::Text.compress(description))
        end


        # Returns the markdown format for module authors.
        #
        # @param authors [Array, String] Module Authors
        # @return [String]
        def normalize_authors(authors)
          if authors.kind_of?(Array)
            authors.collect { |a| "* #{CGI::escapeHTML(a)}" } * "\n"
          else
            authors
          end
        end


        # Returns the markdown format for module targets.
        #
        # @param targets [Array] Module targets.
        # @return [String]
        def normalize_targets(targets)
          targets.collect { |c| "* #{c.name}" } * "\n"
        end

        # Returns the markdown format for module AttackerKB references.
        #
        # @param refs [Array] AttackerKB references , with the CVE as the key and the link as the value.
        # @return [String]
        def normalize_attackerkb_references(refs)
          normalized = ''

          # Grabs module CVE's if available and adds each AttackerKB link to the hash
          refs.each do |ref|
            next unless ref.ctx_id == 'CVE'

            cve = "#{ref.ctx_id}-#{ref.ctx_val}"
            link = "https://attackerkb.com/topics/#{ref.ctx_id}-#{ref.ctx_val}?referrer=msfconsole"
            normalized << "* [#{cve}](#{link})\n"
          end
          normalized
        end

        # Returns the markdown format for module references.
        #
        # @param refs [Array] Module references.
        # @return [String]
        def normalize_references(refs)
          normalized = ''
          cve_collection = refs.select { |r| r.ctx_id.match(/^cve$/i) }
          if cve_collection.empty?
            normalized << "* #{NO_CVE_MESSAGE}\n"
          end

          refs.each do |ref|
            case ref.ctx_id
            when 'MSB'
              normalized << "* [#{ref.ctx_val}](#{ref.site})"
            when 'URL'
              normalized << "* [#{ref.site}](#{ref.site})"
            when 'OSVDB'
              normalized << "* #{ref.site.to_s}"
            when 'US-CERT-VU'
              normalized << "* [VU##{ref.ctx_val}](#{ref.site})"
            when 'CVE', 'cve'
              if !cve_collection.empty? && ref.ctx_val.blank?
                normalized << "* #{NO_CVE_MESSAGE}"
              else
                normalized << "* [#{ref.ctx_id}-#{ref.ctx_val}](#{ref.site})"
              end
            else
              normalized << "* [#{ref.ctx_id}-#{ref.ctx_val}](#{ref.site})"
            end
            normalized << "\n"
          end
          normalized
        end


        # Returns the markdown format for module platforms.
        #
        # @param platforms [Array, String] Module platforms.
        # @return [String]
        def normalize_platforms(platforms)
          if platforms.kind_of?(Array)
            platforms.collect { |p| "* #{p}" } * "\n"
          else
            platforms
          end
        end


        # Returns a parsed demo ERB template.
        #
        # @param mod [Msf::Module] Metasploit module.
        # @param path [String] Template path.
        # @return [String]
        def load_demo_template(mod, path)
          data = ''
          path = File.expand_path(File.join(Msf::Config.data_directory, 'markdown_doc', path))
          File.open(path, 'rb') { |f| data = f.read }
          ERB.new(data).result(binding())
        end


        # Returns whether the module is a remote exploit or not.
        #
        # @param mod [Msf::Module] Metasploit module.
        # @return [TrueClass] Module is a remote exploit.
        # @return [FalseClass] Module is not really a remote exploit.
        def is_remote_exploit?(mod)
          # It's actually a little tricky to determine this, so we'll try to be as
          # specific as possible. Rather have false negatives than false positives,
          # because the worst case would be using the generic demo template.
          mod.type == 'exploit' &&                          # Must be an exploit
          mod.kind_of?(Msf::Exploit::Remote) &&             # Should always have this
          !mod.kind_of?(Msf::Exploit::FILEFORMAT) &&        # Definitely not a file format
          !mod.kind_of?(Msf::Exploit::Remote::TcpServer) && # If there is a server mixin, things might get complicated
          mod.options['DisablePayloadHandler']              # Must allow this option
        end


        # Returns a demo template suitable for the module. Currently supported templates:
        # BrowserExploitServer modules, HttpServer modules, local exploit modules, post
        # modules, payloads, auxiliary scanner modules.
        #
        # @param mod [Msf::Module] Metasploit module.
        # @return [String]
        def normalize_demo_output(mod)
          if mod.kind_of?(Msf::Exploit::Remote::BrowserExploitServer) && mod.shortname != 'browser_autopwn2'
            load_demo_template(mod, BES_DEMO_TEMPLATE)
          elsif mod.kind_of?(Msf::Exploit::Remote::HttpServer)
            load_demo_template(mod, HTTPSERVER_DEMO_TEMPLATE)
          elsif mod.kind_of?(Msf::Exploit::Local)
            load_demo_template(mod, LOCALEXPLOIT_DEMO_TEMPLATE)
          elsif mod.kind_of?(Msf::Post)
            load_demo_template(mod, POST_DEMO_TEMPLATE)
          elsif mod.kind_of?(Msf::Payload)
            load_demo_template(mod, PAYLOAD_DEMO_TEMPLATE)
          elsif mod.kind_of?(Msf::Auxiliary::Scanner)
            load_demo_template(mod, AUXILIARY_SCANNER_DEMO_TEMPLATE)
          elsif is_remote_exploit?(mod)
            load_demo_template(mod, REMOTE_EXPLOIT_DEMO_TEMPLATE)
          elsif mod.kind_of?(Msf::Evasion)
            load_demo_template(mod, EVASION_DEMO_TEMPLATE)
          else
            load_demo_template(mod, GENERIC_DEMO_TEMPLATE)
          end
        end

      end
    end
  end
end
