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

require 'contrast/agent/assess/policy/propagation_node'
require 'contrast/components/logger'
require 'contrast/components/scope'
require 'contrast/agent/assess/events/event_data'

module Contrast
  module Extension
    module Assess
      # This Class provides us with a way to invoke String propagation for those
      # methods which are too complex to fit into one of the standard
      # Contrast::Agent::Assess::Policy::Propagator molds without cluttering up the
      # String Class or exposing our methods there.
      class StringPropagator
        extend Contrast::Components::Logger::InstanceMethods
        extend Contrast::Components::Scope::InstanceMethods

        NODE_HASH = {
            'class_name' => 'String',
            'instance_method' => true,
            'method_name' => 'interpolate',
            'method_visibility' => 'public',
            'action' => 'CUSTOM',
            'source' => 'O,P0',
            'target' => 'R',
            'patch_class' => 'NOOP',
            'patch_method' => 'track_interpolation'
        }.cs__freeze
        INTERPOLATION_NODE = Contrast::Agent::Assess::Policy::PropagationNode.new(NODE_HASH)

        class << self
          def track_interpolation inputs, result
            return unless ::Contrast::AGENT.interpolation_enabled?
            return if in_contrast_scope?
            return unless inputs.any? { |input| Contrast::Agent::Assess::Tracker.tracked?(input) }

            with_contrast_scope do
              return unless (properties = Contrast::Agent::Assess::Tracker.properties!(result))

              parent_events = []
              offset = 0
              inputs.each do |input|
                properties.copy_from(input, result, offset)
                add_dynamic_sources_info input, result
                offset += input.length
                parent_event = Contrast::Agent::Assess::Tracker.properties(input)&.event
                parent_events << parent_event if parent_event
              end
              event_data = Contrast::Agent::Assess::Events::EventData.new(INTERPOLATION_NODE,
                                                                          result,
                                                                          inputs,
                                                                          result,
                                                                          inputs)
              properties.build_event(event_data)
              properties.event.instance_variable_set(:@_parent_events, parent_events)
            end
          rescue StandardError => e
            logger.error('Unable to track interpolation', e)
          end

          private

          # When there is a string interpolation on input coming from tainted database,
          # the Contrast::Agent::Assess::Properties::Updated.copy_from method won't copy
          # the dynamic source properties needed in the build findings from TS to display
          # the column and Table information as database source information.
          #
          # @param source [Object] the source object with the required properties.
          # @param target [Object] the result form the interpolation and the object
          # that needs to keep the source properties, in order to be reporter on
          # trigger event.
          # @return updated_properties [Hash<DynamicSourceInfo>, nil]
          def add_dynamic_sources_info source, target
            return unless (dynamic_props = Contrast::Agent::Assess::Tracker.properties(source)&.properties)

            Contrast::Agent::Assess::Tracker.properties(target)&.add_properties(dynamic_props)
          end
        end
      end
    end
  end
end