class BetterHtml::TestHelper::SafeErb::TagInterpolation

Constants

NO_HTML_TAGS

Public Instance Methods

validate() click to toggle source
# File lib/better_html/test_helper/safe_erb/tag_interpolation.rb, line 14
def validate
  @parser.nodes_with_type(:tag).each do |tag_node|
    tag = Tree::Tag.from_node(tag_node)
    tag.attributes.each do |attribute|
      validate_attribute(attribute)
    end
  end

  @parser.nodes_with_type(:text).each do |node|
    validate_text_node(node) unless no_html_tag?(node)
  end
end

Private Instance Methods

handle_missing_safe_wrapper(parent_node, ruby_node, attr_name) click to toggle source
# File lib/better_html/test_helper/safe_erb/tag_interpolation.rb, line 115
def handle_missing_safe_wrapper(parent_node, ruby_node, attr_name)
  return unless @config.javascript_attribute_name?(attr_name)
  method_calls = ruby_node.return_values.select(&:method_call?)
  unsafe_calls = method_calls.select { |node| !@config.javascript_safe_method?(node.method_name) }
  if method_calls.empty?
    add_error(
      "erb interpolation in javascript attribute must be wrapped in safe helper such as '(...).to_json'",
      location: nested_location(parent_node, ruby_node)
    )
    true
  elsif unsafe_calls.any?
    unsafe_calls.each do |call_node|
      add_error(
        "erb interpolation in javascript attribute must be wrapped in safe helper such as '(...).to_json'",
        location: nested_location(parent_node, call_node)
      )
    end
    true
  end
end
nested_location(parent_node, ruby_node) click to toggle source
# File lib/better_html/test_helper/safe_erb/tag_interpolation.rb, line 169
def nested_location(parent_node, ruby_node)
  Tokenizer::Location.new(
    parent_node.loc.source_buffer,
    parent_node.loc.begin_pos + ruby_node.loc.expression.begin_pos,
    parent_node.loc.begin_pos + ruby_node.loc.expression.end_pos
  )
end
no_html_tag?(node) click to toggle source
# File lib/better_html/test_helper/safe_erb/tag_interpolation.rb, line 29
def no_html_tag?(node)
  ast = @parser.ast.to_a
  index = ast.find_index(node)
  return unless (previous_node = ast[index - 1])
  return unless previous_node.type == :tag

  tag = BetterHtml::Tree::Tag.from_node(previous_node)
  NO_HTML_TAGS.include?(tag.name) && !tag.closing?
end
no_unsafe_calls(parent_node, ruby_node) click to toggle source
# File lib/better_html/test_helper/safe_erb/tag_interpolation.rb, line 153
def no_unsafe_calls(parent_node, ruby_node)
  ruby_node.descendants(:send, :csend).each do |call|
    if call.method_name?(:raw)
      add_error(
        "erb interpolation with '<%= raw(...) %>' in this context is never safe",
        location: nested_location(parent_node, ruby_node)
      )
    elsif call.method_name?(:html_safe)
      add_error(
        "erb interpolation with '<%= (...).html_safe %>' in this context is never safe",
        location: nested_location(parent_node, ruby_node)
      )
    end
  end
end
validate_attribute(attribute) click to toggle source
# File lib/better_html/test_helper/safe_erb/tag_interpolation.rb, line 39
def validate_attribute(attribute)
  erb_nodes(attribute.value_node).each do |erb_node, indicator_node, code_node|
    next if indicator_node.nil?

    indicator = indicator_node.loc.source
    source = code_node.loc.source

    if indicator == '='
      ruby_node = begin
        RubyNode.parse(source)
      rescue ::Parser::SyntaxError
        nil
      end
      if ruby_node
        no_unsafe_calls(code_node, ruby_node)
        unless ruby_node.static_return_value?
          handle_missing_safe_wrapper(code_node, ruby_node, attribute.name)
        end
      end
    elsif indicator == '=='
      add_error(
        "erb interpolation with '<%==' inside html attribute is never safe",
        location: erb_node.loc
      )
    end
  end
end
validate_ruby_helper(parent_node, ruby_node) click to toggle source
# File lib/better_html/test_helper/safe_erb/tag_interpolation.rb, line 84
def validate_ruby_helper(parent_node, ruby_node)
  ruby_node.descendants(:send, :csend).each do |send_node|
    send_node.descendants(:hash).each do |hash_node|
      hash_node.child_nodes.select(&:pair?).each do |pair_node|
        validate_ruby_helper_hash_entry(parent_node, ruby_node, nil, *pair_node.children)
      end
    end
  end
end
validate_ruby_helper_hash_entry(parent_node, ruby_node, key_prefix, key_node, value_node) click to toggle source
# File lib/better_html/test_helper/safe_erb/tag_interpolation.rb, line 94
def validate_ruby_helper_hash_entry(parent_node, ruby_node, key_prefix, key_node, value_node)
  return unless [:sym, :str].include?(key_node.type)
  key = [key_prefix, key_node.children.first.to_s].compact.join('-').dasherize
  case value_node.type
  when :dstr
    validate_ruby_helper_hash_value(parent_node, ruby_node, key, value_node)
  when :hash
    if key == 'data'
      value_node.child_nodes.select(&:pair?).each do |pair_node|
        validate_ruby_helper_hash_entry(parent_node, ruby_node, key, *pair_node.children)
      end
    end
  end
end
validate_ruby_helper_hash_value(parent_node, ruby_node, attr_name, hash_value) click to toggle source
# File lib/better_html/test_helper/safe_erb/tag_interpolation.rb, line 109
def validate_ruby_helper_hash_value(parent_node, ruby_node, attr_name, hash_value)
  hash_value.child_nodes.select(&:begin?).each do |begin_node|
    validate_tag_interpolation(parent_node, begin_node, attr_name)
  end
end
validate_tag_interpolation(parent_node, ruby_node, attr_name) click to toggle source
# File lib/better_html/test_helper/safe_erb/tag_interpolation.rb, line 136
def validate_tag_interpolation(parent_node, ruby_node, attr_name)
  return if ruby_node.static_return_value?
  return if handle_missing_safe_wrapper(parent_node, ruby_node, attr_name)

  ruby_node.return_values.each do |call_node|
    next if call_node.static_return_value?

    if @config.javascript_attribute_name?(attr_name) &&
          !@config.javascript_safe_method?(call_node.method_name)
      add_error(
        "erb interpolation in javascript attribute must be wrapped in safe helper such as '(...).to_json'",
        location: nested_location(parent_node, ruby_node)
      )
    end
  end
end
validate_text_node(text_node) click to toggle source
# File lib/better_html/test_helper/safe_erb/tag_interpolation.rb, line 67
def validate_text_node(text_node)
  erb_nodes(text_node).each do |erb_node, indicator_node, code_node|
    indicator = indicator_node&.loc&.source
    next if indicator == '#' || indicator == '%'
    source = code_node.loc.source

    ruby_node = begin
      RubyNode.parse(source)
    rescue ::Parser::SyntaxError
      nil
    end
    next unless ruby_node
    no_unsafe_calls(code_node, ruby_node)
    validate_ruby_helper(code_node, ruby_node)
  end
end