diff --git a/lib/haml.rb b/lib/haml.rb index 7b715e5d..8d4a4d39 100644 --- a/lib/haml.rb +++ b/lib/haml.rb @@ -696,8 +696,8 @@ $LOAD_PATH << dir unless $LOAD_PATH.include?(dir) # # ==== &= # -# An ampersand followed by an equals character -# evaluates Ruby code just like the single equals, +# An ampersand followed by one or two equals characters +# evaluates Ruby code just like the equals without the ampersand, # but sanitizes any HTML-sensitive characters in the result of the code. # For example: # @@ -712,8 +712,8 @@ $LOAD_PATH << dir unless $LOAD_PATH.include?(dir) # # ==== != # -# An exclamation mark followed by an equals character -# evaluates Ruby code just like the single equals, +# An exclamation mark followed by one or two equals characters +# evaluates Ruby code just like the equals would, # but never sanitizes the HTML. # # By default, the single equals doesn't sanitize HTML either. @@ -835,7 +835,7 @@ $LOAD_PATH << dir unless $LOAD_PATH.include?(dir) # [:escape_html] Sets whether or not to escape HTML-sensitive characters in script. # If this is true, = behaves like &=; # otherwise, it behaves like !=. -# Note that this doesn't affect attributes or == interpolation. +# Note that this escapes tag attributes. # Defaults to false. # # [:suppress_eval] Whether or not attribute hashes and Ruby scripts diff --git a/lib/haml/buffer.rb b/lib/haml/buffer.rb index 77d94814..fa9d6454 100644 --- a/lib/haml/buffer.rb +++ b/lib/haml/buffer.rb @@ -117,7 +117,7 @@ module Haml # Takes the various information about the opening tag for an # element, formats it, and adds it to the buffer. - def open_tag(name, atomic, try_one_line, preserve_tag, class_id, obj_ref, content, *attributes_hashes) + def open_tag(name, atomic, try_one_line, preserve_tag, escape_html, class_id, obj_ref, content, *attributes_hashes) tabulation = @real_tabs attributes = class_id @@ -135,7 +135,7 @@ module Haml str = ">\n" end - attributes = Precompiler.build_attributes(html?, @options[:attr_wrapper], attributes) + attributes = Precompiler.build_attributes(html?, @options[:attr_wrapper], escape_html, attributes) @buffer << "#{@options[:ugly] ? '' : tabs(tabulation)}<#{name}#{attributes}#{str}" if content diff --git a/lib/haml/helpers.rb b/lib/haml/helpers.rb index 7f602e7d..d35ec0aa 100644 --- a/lib/haml/helpers.rb +++ b/lib/haml/helpers.rb @@ -293,7 +293,9 @@ module Haml end attributes = Haml::Precompiler.build_attributes(haml_buffer.html?, - haml_buffer.options[:attr_wrapper], attributes) + haml_buffer.options[:attr_wrapper], + haml_buffer.options[:escape_html], + attributes) if text.nil? && block.nil? puts "<#{name}#{attributes} />" return nil diff --git a/lib/haml/precompiler.rb b/lib/haml/precompiler.rb index 8d21dca2..0df1225c 100644 --- a/lib/haml/precompiler.rb +++ b/lib/haml/precompiler.rb @@ -202,6 +202,7 @@ END when ELEMENT; render_tag(text) when COMMENT; render_comment(text) when SANITIZE + return push_script(unescape_interpolation(text[3..-1].strip), false, nil, false, true) if text[1..2] == "==" return push_script(text[2..-1].strip, false, nil, false, true) if text[1] == SCRIPT push_plain text when SCRIPT @@ -220,6 +221,7 @@ END when FILTER; start_filtered(text[1..-1].downcase) when DOCTYPE return render_doctype(text) if text[0...3] == '!!!' + return push_script(unescape_interpolation(text[3..-1].strip), false) if text[1..2] == "==" return push_script(text[2..-1].strip, false) if text[1] == SCRIPT push_plain text when ESCAPE; push_plain text[1..-1] @@ -461,7 +463,7 @@ END end # This is a class method so it can be accessed from Buffer. - def self.build_attributes(is_html, attr_wrapper, attributes = {}) + def self.build_attributes(is_html, attr_wrapper, escape_html, attributes = {}) quote_escape = attr_wrapper == '"' ? """ : "'" other_quote_char = attr_wrapper == '"' ? "'" : '"' @@ -485,12 +487,13 @@ END end end " #{attr}=#{this_attr_wrapper}#{value}#{this_attr_wrapper}" - end - result.compact.sort.join + end.compact.sort.join + + escape_html ? Haml::Helpers.html_escape(result) : result end - def prerender_tag(name, self_close, attributes) - attributes_string = Precompiler.build_attributes(html?, @options[:attr_wrapper], attributes) + def prerender_tag(name, self_close, escape_html, attributes) + attributes_string = Precompiler.build_attributes(html?, @options[:attr_wrapper], escape_html, attributes) "<#{name}#{attributes_string}#{self_close && xhtml? ? ' /' : ''}>" end @@ -529,10 +532,10 @@ END when '&', '!' if value[0] == ?= parse = true - value = value[1..-1].strip + value = (value[1] == ?= ? unescape_interpolation(value[2..-1].strip) : value[1..-1].strip) end end - + if parse && @options[:suppress_eval] parse = false value = '' @@ -559,7 +562,7 @@ END # This means that we can render the tag directly to text and not process it in the buffer tag_closed = !value.empty? && one_liner && !parse - open_tag = prerender_tag(tag_name, atomic, attributes) + open_tag = prerender_tag(tag_name, atomic, escape_html, attributes) open_tag << "#{value}" if tag_closed open_tag << "\n" unless parse @@ -569,7 +572,7 @@ END flush_merged_text content = value.empty? || parse ? 'nil' : value.dump attributes_hash = ', ' + attributes_hash if attributes_hash - push_silent "_hamlout.open_tag(#{tag_name.inspect}, #{atomic.inspect}, #{(!value.empty?).inspect}, #{preserve_tag.inspect}, #{attributes.inspect}, #{object_ref}, #{content}#{attributes_hash})" + push_silent "_hamlout.open_tag(#{tag_name.inspect}, #{atomic.inspect}, #{(!value.empty?).inspect}, #{preserve_tag.inspect}, #{escape_html.inspect}, #{attributes.inspect}, #{object_ref}, #{content}#{attributes_hash})" end return if atomic diff --git a/test/haml/engine_test.rb b/test/haml/engine_test.rb index 42416e50..214c5fde 100644 --- a/test/haml/engine_test.rb +++ b/test/haml/engine_test.rb @@ -118,36 +118,79 @@ class EngineTest < Test::Unit::TestCase # HTML escaping tests - def test_script_ending_in_comment_should_render_when_html_is_escaped - assert_equal("foo&bar\n", render("= 'foo&bar' #comment", :escape_html => true)) - end - - def test_ampersand_equals + def test_ampersand_equals_should_escape assert_equal("

\n foo & bar\n

\n", render("%p\n &= 'foo & bar'", :escape_html => false)) end - def test_ampersand_equals_inline + def test_ampersand_equals_inline_should_escape assert_equal("

foo & bar

\n", render("%p&= 'foo & bar'", :escape_html => false)) end - def test_bang_equals + def test_bang_equals_should_not_escape assert_equal("

\n foo & bar\n

\n", render("%p\n != 'foo & bar'", :escape_html => true)) end - def test_bang_equals_inline + def test_bang_equals_inline_should_not_escape assert_equal("

foo & bar

\n", render("%p!= 'foo & bar'", :escape_html => true)) end + + def test_static_attributes_should_be_escaped + assert_equal("\n", + render("%img.atlantis{:style => 'ugly&stupid'}", :escape_html => true)) + assert_equal("
foo
\n", + render(".atlantis{:style => 'ugly&stupid'} foo", :escape_html => true)) + assert_equal("

foo

\n", + render("%p.atlantis{:style => 'ugly&stupid'}= 'foo'", :escape_html => true)) + end - def test_escape_html_option_for_scripts + def test_dynamic_attributes_should_be_escaped + assert_equal("\n", + render("%img{:width => nil, :src => '/foo.png', :alt => String.new}", :escape_html => true)) + assert_equal("

foo

\n", + render("%p{:width => nil, :src => '/foo.png', :alt => String.new} foo", :escape_html => true)) + assert_equal("
foo
\n", + render("%div{:width => nil, :src => '/foo.png', :alt => String.new}= 'foo'", :escape_html => true)) + end + + def test_string_interpolation_should_be_esaped + assert_equal("

4&3

\n", render("%p== #{2+2}&#{2+1}", :escape_html => true)) + assert_equal("

4&3

\n", render("%p== #{2+2}&#{2+1}", :escape_html => false)) + end + + def test_escaped_inline_string_interpolation + assert_equal("

4&3

\n", render("%p&== #{2+2}&#{2+1}", :escape_html => true)) + assert_equal("

4&3

\n", render("%p&== #{2+2}&#{2+1}", :escape_html => false)) + end + + def test_unescaped_inline_string_interpolation + assert_equal("

4&3

\n", render("%p!== #{2+2}&#{2+1}", :escape_html => true)) + assert_equal("

4&3

\n", render("%p!== #{2+2}&#{2+1}", :escape_html => false)) + end + + def test_escaped_string_interpolation + assert_equal("

\n 4&3\n

\n", render("%p\n &== #{2+2}&#{2+1}", :escape_html => true)) + assert_equal("

\n 4&3\n

\n", render("%p\n &== #{2+2}&#{2+1}", :escape_html => false)) + end + + def test_unescaped_string_interpolation + assert_equal("

\n 4&3\n

\n", render("%p\n !== #{2+2}&#{2+1}", :escape_html => true)) + assert_equal("

\n 4&3\n

\n", render("%p\n !== #{2+2}&#{2+1}", :escape_html => false)) + end + + def test_scripts_should_respect_escape_html_option assert_equal("

\n foo & bar\n

\n", render("%p\n = 'foo & bar'", :escape_html => true)) assert_equal("

\n foo & bar\n

\n", render("%p\n = 'foo & bar'", :escape_html => false)) end - def test_escape_html_option_for_inline_scripts + def test_inline_scripts_should_respect_escape_html_option assert_equal("

foo & bar

\n", render("%p= 'foo & bar'", :escape_html => true)) assert_equal("

foo & bar

\n", render("%p= 'foo & bar'", :escape_html => false)) end + def test_script_ending_in_comment_should_render_when_html_is_escaped + assert_equal("foo&bar\n", render("= 'foo&bar' #comment", :escape_html => true)) + end + # Options tests def test_stop_eval diff --git a/test/haml/helper_test.rb b/test/haml/helper_test.rb index d548018f..5b620ff7 100644 --- a/test/haml/helper_test.rb +++ b/test/haml/helper_test.rb @@ -95,6 +95,10 @@ class HelperTest < Test::Unit::TestCase def test_capture_haml assert_equal("\"

13

\\n\"\n", render("- foo = capture_haml(13) do |a|\n %p= a\n= foo.dump")) end + + def test_haml_tag_attribute_html_escaping + assert_equal("

baz

\n", render("%p{:id => 'foo&bar'} baz", :escape_html => true)) + end def test_is_haml assert(!ActionView::Base.new.is_haml?)