# Copyright (c) 2023 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
# frozen_string_literal: true

require 'rack'
require 'contrast/utils/hash_digest'
require 'contrast/utils/duck_utils'
require 'contrast/utils/string_utils'
require 'contrast/agent/assess/rule/response/base_rule'

module Contrast
  module Agent
    module Assess
      module Rule
        module Response
          # These rules check the content of the HTTP Response to determine if something was set incorrectly or
          # insecurely in it.
          module BodyRule
            protected

            HTML_PROP = 'html'.cs__freeze
            START_PROP = 'start'.cs__freeze
            END_PROP = 'end'.cs__freeze
            FORM_START_REGEXP = /<form/i.cs__freeze
            META_TYPE = 'META tag'
            PRAGMA = 'pragma'

            # Rules discern which responses they can/should analyze.
            #
            # @param response [Contrast::Agent::Response] the response of the application
            def analyze_response? response
              super && body?(response)
            end

            # Determine if a response has a body or not.
            #
            # @param response [Contrast::Agent::Response] the response of the application
            # @return [Boolean]
            def body? response
              Contrast::Utils::StringUtils.present?(response.body)
            end

            # Find the elements in this section, if any, so as to determine if they violate this rule.
            #
            # @param section [String,nil] html section to find element
            # @param element_start_str [String] element to find in html section
            # @return [Array<Hash>] the found elements of this section, as well as their start and end indexes.
            def html_elements section, element_start_str = '', capture_overflow: false
              return [] unless section
              return [] unless (potentials = potential_elements(section, element_start_str).flatten).any?

              elements = []
              section_start = 0

              potentials.each do |potential_element|
                next unless potential_element
                next unless element_openings.any? { |opening| potential_element.start_with?(opening) }

                start = section&.index(element_start_str, section_start)
                next if Contrast::Utils::DuckUtils.empty_duck?(start)

                stop = potential_element.index('>').to_i
                next if Contrast::Utils::DuckUtils.empty_duck?(stop)

                section_close = start + 6 + stop
                # Now we have valid tag section with start and stop.
                # Save new boundaries. This is to make sure that If
                # on previous iteration there were non valid section,
                # the start_section will be assigned to nil, thus making
                # the detection of new section not possible, and throwing
                # an error. To that end old values are kept safe.
                #
                # Assign new start index.
                section_start = start
                # Assign new end index.
                element_stop = stop

                elements << capture(section, section_start, section_close, element_stop, overflow: capture_overflow)
                section_start = section_close
              end
              elements
            end

            def potential_elements section, element_start
              section.split(element_start)
            end

            def element_openings
              [' ', "\n", "\r", "\t", '>']
            end

            # Capture the information needed to build the properties of this finding by parsing out from the body
            #
            # @param body [String] the entire HTTP Response body
            # @param body_start [Integer] the start of the range to take from the body
            # @param body_close [Integer] the end of the range to take from the body
            # @param tag_stop [Integer] the index of the end of the html tag from its start
            # @return [Hash]
            def capture body, body_start, body_close, tag_stop, overflow: false
              tag = {}
              # Capture the 50 characters in front of the form, or up to the start if the form starts before 50.
              if overflow
                capture_start = body_start < 50 ? 0 : body_start - 50
                # Start is where the '<form' is in the body
                # 6 accounts for the characters in the form and the opening char
                # potential_form.index('>') accounts for finding the rest of the form
                # 50 accounts for the context to capture beyond
                capture_close = body_close + 50
                tag[HTML_PROP] = body[capture_start...capture_close]
                tag[START_PROP] = body_start < 50 ? body_start : 50
              else
                tag[HTML_PROP] = body[body_start...body_close]
                tag[START_PROP] = body_start
              end
              tag[END_PROP] = tag[START_PROP] + 6 + tag_stop
              tag
            end
          end
        end
      end
    end
  end
end