module Msf::DBManager::Note
  #
  # This method iterates the notes table calling the supplied block with the
  # note instance of each entry.
  #
  def each_note(wspace=framework.db.workspace, &block)
  ::ApplicationRecord.connection_pool.with_connection {
    wspace.notes.each do |note|
      block.call(note)
    end
  }
  end

  #
  # Find or create a note matching this type/data
  #
  def find_or_create_note(opts)
    report_note(opts)
  end

  #
  # This methods returns a list of all notes in the database
  #
  def notes(opts)
    ::ApplicationRecord.connection_pool.with_connection {
      # If we have the ID, there is no point in creating a complex query.
      if opts[:id] && !opts[:id].to_s.empty?
        return Array.wrap(Mdm::Note.find(opts[:id]))
      end

      wspace = Msf::Util::DBManager.process_opts_workspace(opts, framework)
      opts = opts.clone()
      opts.delete(:workspace)

      data = opts.delete(:data)
      search_term = opts.delete(:search_term)
      results = wspace.notes.includes(:host).where(opts)

      # Compare the deserialized data from the DB to the search data since the column is serialized.
      unless data.nil?
        results = results.select { |note| note.data == data }
      end

      if search_term && !search_term.empty?
        re_search_term = /#{search_term}/mi
        results = results.select { |note|
          note.attribute_names.any? { |a| note[a.intern].to_s.match(re_search_term) }
        }
      end
      results
    }
  end

  #
  # Report a Note to the database.  Notes can be tied to a ::Mdm::Workspace, Host, or Service.
  #
  # opts MUST contain
  # +:type+::  The type of note, e.g. smb_peer_os
  #
  # opts can contain
  # +:workspace+::  the workspace to associate with this Note
  # +:host+::       an IP address or a Host object to associate with this Note
  # +:service+::    a Service object to associate with this Note
  # +:data+::       whatever it is you're making a note of
  # +:port+::       along with +:host+ and +:proto+, a service to associate with this Note
  # +:proto+::      along with +:host+ and +:port+, a service to associate with this Note
  # +:update+::     what to do in case a similar Note exists, see below
  #
  # The +:update+ option can have the following values:
  # +:unique+::       allow only a single Note per +:host+/+:type+ pair
  # +:unique_data+::  like +:unique+, but also compare +:data+
  # +:insert+::       always insert a new Note even if one with identical values exists
  #
  # If the provided +:host+ is an IP address and does not exist in the
  # database, it will be created.  If +:workspace+, +:host+ and +:service+
  # are all omitted, the new Note will be associated with the current
  # workspace.
  #
  def report_note(opts)
    return if not active
  ::ApplicationRecord.connection_pool.with_connection {
    wspace = Msf::Util::DBManager.process_opts_workspace(opts, framework)
    opts = opts.clone()
    opts.delete(:workspace)
    seen = opts.delete(:seen) || false
    crit = opts.delete(:critical) || false
    host = nil
    addr = nil
    # Report the host so it's there for the Proc to use below
    if opts[:host]
      if opts[:host].kind_of? ::Mdm::Host
        host = opts[:host]
      else
        addr = Msf::Util::Host.normalize_host(opts[:host])
        host = report_host({:workspace => wspace, :host => addr})
      end
      # Do the same for a service if that's also included.
      if (opts[:port])
        proto = nil
        sname = nil
        proto_lower = opts[:proto].to_s.downcase # Catch incorrect usages
        case proto_lower
        when 'tcp','udp'
          proto = proto_lower
          sname = opts[:sname] if opts[:sname]
        # XXX: These normalizations are lazy af
        when 'http', 'smb'
          proto = 'tcp'
          sname = proto_lower
        when 'dns','snmp','dhcp'
          proto = 'udp'
          sname = proto_lower
        else
          proto = 'tcp'
          sname = proto_lower
        end
        sopts = {
          :workspace => wspace,
          :host  => host,
          :port  => opts[:port],
          :proto => proto
        }
        sopts[:name] = sname if sname
        report_service(sopts)
      end
    end
    # Update Modes can be :unique, :unique_data, :insert
    mode = opts[:update] || :unique

    ret = {}

    if addr and not host
      host = get_host(:workspace => wspace, :host => addr)
    end
    if host and (opts[:port] and proto)
      # only one result can be returned, as the +port+ field restricts potential results to a single service
      service = services(:workspace => wspace,
                         :hosts => {address: host.address},
                         :proto => proto,
                         :port => opts[:port]).first
    elsif opts[:service] and opts[:service].kind_of? ::Mdm::Service
      service = opts[:service]
    end

    ntype  = opts.delete(:type) || opts.delete(:ntype) || (raise RuntimeError, "A note :type or :ntype is required")

    has_valid_note_datatype = opts[:data].is_a?(Hash) || opts[:data].nil?
    unless has_valid_note_datatype
      stack_trace = caller.map { |line| "[-]   #{line}" }.join("\n")
      message = "\n[-] [DEPRECATION] Using #{__method__} with a non-hash data value is deprecated, please raise a Github issue with this output.\n[-] Call stack:\n#{stack_trace}"
      warn(message)
      # Additionally write to ~/.msf4/logs/framework.log - as this gets attached to GitHub issues etc
      elog(message)
    end

    data   = opts[:data]
    note   = nil

    conditions = { :ntype => ntype }
    conditions[:host_id] = host[:id] if host
    conditions[:service_id] = service[:id] if service
    conditions[:vuln_id] = opts[:vuln_id]

    case mode.to_sym
    when :unique
      note      = wspace.notes.where(conditions).first_or_initialize
      note.data = data
    when :unique_data
      notes = wspace.notes.where(conditions)

      # Don't make a new Note with the same data as one that already
      # exists for the given: type and (host or service)
      notes.each do |n|
        # Compare the deserialized data from the table to the raw
        # data we're looking for.  Because of the serialization we
        # can't do this easily or reliably in SQL.
        if n.data == data
          note = n
          break
        end
      end
      if not note
        # We didn't find one with the data we're looking for, make
        # a new one.
        note = wspace.notes.new(conditions.merge(:data => data))
      end
    else
      # Otherwise, assume :insert, which means always make a new one
      note = wspace.notes.new
      if host
        note.host_id = host[:id]
      end
      if opts[:service] and opts[:service].kind_of? ::Mdm::Service
        note.service_id = opts[:service][:id]
      end
      note.seen     = seen
      note.critical = crit
      note.ntype    = ntype
      note.data     = data
    end
    if opts[:vuln_id]
      note.vuln_id = opts[:vuln_id]
    end
    msf_assign_timestamps(opts, note)
    note.save!
    ret[:note] = note
  }
  end

  # Update the attributes of a note entry with the values in opts.
  # The values in opts should match the attributes to update.
  #
  # @param opts [Hash] Hash containing the updated values. Key should match the attribute to update. Must contain :id of record to update.
  # @return [Mdm::Note] The updated Mdm::Note object.
  def update_note(opts)
    ::ApplicationRecord.connection_pool.with_connection {
      wspace = Msf::Util::DBManager.process_opts_workspace(opts, framework, false)
      opts = opts.clone()
      opts.delete(:workspace)
      opts[:workspace] = wspace if wspace

      id = opts.delete(:id)
      note = Mdm::Note.find(id)
      note.update!(opts)
      return note
    }
  end

  # Deletes note entries based on the IDs passed in.
  #
  # @param opts[:ids] [Array] Array containing Integers corresponding to the IDs of the note entries to delete.
  # @return [Array] Array containing the Mdm::Note objects that were successfully deleted.
  def delete_note(opts)
    raise ArgumentError.new("The following options are required: :ids") if opts[:ids].nil?

    ::ApplicationRecord.connection_pool.with_connection {
      deleted = []
      opts[:ids].each do |note_id|
        note = Mdm::Note.find(note_id)
        begin
          deleted << note.destroy
        rescue # refs suck
          elog("Forcibly deleting #{note}")
          deleted << note.delete
        end
      end

      return deleted
    }
  end
end
