# frozen_string_literal: true

require "active_support/core_ext/erb/util"
require "active_support/multibyte/unicode"

class Object
  def html_safe?
    false
  end
end

class Numeric
  def html_safe?
    true
  end
end

module ActiveSupport # :nodoc:
  class SafeBuffer < String
    UNSAFE_STRING_METHODS = %w(
      capitalize chomp chop delete delete_prefix delete_suffix
      downcase lstrip next reverse rstrip scrub squeeze strip
      succ swapcase tr tr_s unicode_normalize upcase
    ).freeze

    UNSAFE_STRING_METHODS_WITH_BACKREF = %w(gsub sub).freeze

    alias_method :original_concat, :concat
    private :original_concat

    # Raised when ActiveSupport::SafeBuffer#safe_concat is called on unsafe buffers.
    class SafeConcatError < StandardError
      def initialize
        super "Could not concatenate to the buffer because it is not HTML safe."
      end
    end

    def [](*)
      if html_safe?
        new_string = super

        return unless new_string

        string_into_safe_buffer(new_string, true)
      else
        to_str[*]
      end
    end
    alias_method :slice, :[]

    def slice!(...)
      new_string = super

      return new_string if !html_safe? || new_string.nil?

      string_into_safe_buffer(new_string, true)
    end

    def chr
      return super unless html_safe?

      string_into_safe_buffer(super, true)
    end

    def safe_concat(value)
      raise SafeConcatError unless html_safe?
      original_concat(value)
    end

    def initialize(_str = "")
      super
    end

    def initialize_copy(other)
      super
      @html_unsafe = true unless other.html_safe?
    end

    def concat(value)
      unless value.nil?
        super(implicit_html_escape_interpolated_argument(value))
      end
      self
    end
    alias << concat

    def bytesplice(*args, value)
      super(*args, implicit_html_escape_interpolated_argument(value))
    end

    def insert(index, value)
      super(index, implicit_html_escape_interpolated_argument(value))
    end

    def prepend(value)
      super(implicit_html_escape_interpolated_argument(value))
    end

    def replace(value)
      super(implicit_html_escape_interpolated_argument(value))
    end

    def []=(arg1, arg2, arg3 = nil)
      if arg3
        super(arg1, arg2, implicit_html_escape_interpolated_argument(arg3))
      else
        super(arg1, implicit_html_escape_interpolated_argument(arg2))
      end
    end

    def +(other)
      dup.concat(other)
    end

    def *(_)
      new_string = super
      new_safe_buffer = new_string.is_a?(SafeBuffer) ? new_string : SafeBuffer.new(new_string)
      if @html_unsafe
        new_safe_buffer.mark_unsafe!
      end
      new_safe_buffer
    end

    def %(args)
      case args
      when Hash
        escaped_args = args.transform_values { |arg| explicit_html_escape_interpolated_argument(arg) }
      else
        escaped_args = Array(args).map { |arg| explicit_html_escape_interpolated_argument(arg) }
      end

      new_safe_buffer = self.class.new(super(escaped_args))
      if @html_unsafe
        new_safe_buffer.mark_unsafe!
      end
      new_safe_buffer
    end

    def html_safe?
      @html_unsafe.nil?
    end

    def to_s
      self
    end

    def as_json(*)
      to_str
    end

    def to_param
      to_str
    end

    def encode_with(coder)
      coder.represent_object nil, to_str
    end

    UNSAFE_STRING_METHODS.each do |unsafe_method|
      if unsafe_method.respond_to?(unsafe_method)
        class_eval <<~RUBY, __FILE__, __LINE__ + 1
          def #{unsafe_method}(...)                 # def capitalize(...)
            to_str.#{unsafe_method}(...)            #   to_str.capitalize(...)
          end                                       # end

          def #{unsafe_method}!(...)                # def capitalize!(...)
            @html_unsafe = true                     #   @html_unsafe = true
            super                                   #   super
          end                                       # end
        RUBY
      end
    end

    UNSAFE_STRING_METHODS_WITH_BACKREF.each do |unsafe_method|
      class_eval <<~RUBY, __FILE__, __LINE__ + 1
        def #{unsafe_method}(*, **, &block)             # def gsub(*, **, &block)
          if block                                      #   if block
            to_str.#{unsafe_method}(*, **) { |*params|  #     to_str.gsub(*, **) { |*params|
              set_block_back_references(block, $~)      #       set_block_back_references(block, $~)
              block.call(*params)                       #       block.call(*params)
            }                                           #     }
          else                                          #   else
            to_str.#{unsafe_method}(*, **)              #     to_str.gsub(*, **)
          end                                           #   end
        end                                             # end

        def #{unsafe_method}!(*, **, &block)            # def gsub!(*, **, &block)
          @html_unsafe = true                           #   @html_unsafe = true
          if block                                      #   if block
            super(*, **) { |*params|                    #     super(*, **) { |*params|
              set_block_back_references(block, $~)      #       set_block_back_references(block, $~)
              block.call(*params)                       #       block.call(*params)
            }                                           #     }
          else                                          #   else
            super                                       #     super
          end                                           #   end
        end                                             # end
      RUBY
    end

    protected
      def mark_unsafe!
        @html_unsafe = true
      end

    private
      def explicit_html_escape_interpolated_argument(arg)
        (!html_safe? || arg.html_safe?) ? arg : ERB::Util.unwrapped_html_escape(arg)
      end

      def implicit_html_escape_interpolated_argument(arg)
        if !html_safe? || arg.html_safe?
          arg
        else
          ERB::Util.unwrapped_html_escape(arg.to_str)
        end
      end

      def set_block_back_references(block, match_data)
        block.binding.eval("proc { |m| $~ = m }").call(match_data)
      rescue ArgumentError
        # Can't create binding from C level Proc
      end

      def string_into_safe_buffer(new_string, is_html_safe)
        new_safe_buffer = new_string.is_a?(SafeBuffer) ? new_string : SafeBuffer.new(new_string)
        unless is_html_safe
          new_safe_buffer.instance_variable_set :@html_unsafe, true
        end
        new_safe_buffer
      end
  end
end

class String
  # Marks a string as trusted safe. It will be inserted into HTML with no
  # additional escaping performed. It is your responsibility to ensure that the
  # string contains no malicious content. This method is equivalent to the
  # +raw+ helper in views. It is recommended that you use +sanitize+ instead of
  # this method. It should never be called on user input.
  def html_safe
    ActiveSupport::SafeBuffer.new(self)
  end
end
