Nested content_tags escape inner html .. why?

So, if I scroll and create a collection of li / a tags, I get as expected .. an array of these tags:

(1..5).to_a.map do content_tag(:li) do link_to("boo", "www.boohoo.com") end end => ["<li><a href=\"www.boohoo.com\">boo</a></li>", "<li><a href=\"www.boohoo.com\">boo</a></li>", "<li><a href=\"www.boohoo.com\">boo</a></li>", "<li><a href=\"www.boohoo.com\">boo</a></li>", "<li><a href=\"www.boohoo.com\">boo</a></li>"] 

I call the connection on them, and get the expected string ...

 (1..5).to_a.map do content_tag(:li) do link_to("boo", "www.boohoo.com") end end.join => "<li><a href=\"www.boohoo.com\">boo</a></li><li><a href=\"www.boohoo.com\">boo</a></li><li><a href=\"www.boohoo.com\">boo</a></li><li><a href=\"www.boohoo.com\">boo</a></li><li><a href=\"www.boohoo.com\">boo</a></li>" 

However, if I put this level in depth in the ol tag ...

 content_tag(:ol) do (1..5).to_a.map do content_tag(:li) { link_to("boo", "www.boohoo.com") } end.join end => "<ol>&lt;li&gt;&lt;a href=&quot;www.boohoo.com&quot;&gt;boo&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;www.boohoo.com&quot;&gt;boo&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;www.boohoo.com&quot;&gt;boo&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;www.boohoo.com&quot;&gt;boo&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;www.boohoo.com&quot;&gt;boo&lt;/a&gt;&lt;/li&gt;</ol>" 

I get a runaway inner html frenzy !!!

When looking at the source code of the rails:

  def content_tag(name, content_or_options_with_block = nil, options = nil, escape = true, &block) if block_given? options = content_or_options_with_block if content_or_options_with_block.is_a?(Hash) content_tag_string(name, capture(&block), options, escape) else content_tag_string(name, content_or_options_with_block, options, escape) end end private def content_tag_string(name, content, options, escape = true) tag_options = tag_options(options, escape) if options "<#{name}#{tag_options}>#{escape ? ERB::Util.h(content) : content}</#{name}>".html_safe end 

It looks deceiving the way I can: content_tag (: li, nil, nil, false) and not escape from the content. But:

 content_tag(:ol, nil, nil, false) do (1..5).to_a.map do content_tag(:li, nil, nil, false) do link_to("boo", "www.boohoo.com") end end.join end => "<ol>&lt;li&gt;&lt;a href=&quot;www.boohoo.com&quot;&gt;boo&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;www.boohoo.com&quot;&gt;boo&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;www.boohoo.com&quot;&gt;boo&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;www.boohoo.com&quot;&gt;boo&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;www.boohoo.com&quot;&gt;boo&lt;/a&gt;&lt;/li&gt;</ol>" 

I still suffer from unwanted html_escape syndrome ...

So the only way I know is to do this:

 content_tag(:ol) do (1..5).to_a.map do content_tag(:li) do link_to("boo", "www.boohoo.com") end end.join.html_safe end => "<ol><li><a href=\"www.boohoo.com\">boo</a></li><li><a href=\"www.boohoo.com\">boo</a></li><li><a href=\"www.boohoo.com\">boo</a></li><li><a href=\"www.boohoo.com\">boo</a></li><li><a href=\"www.boohoo.com\">boo</a></li></ol>" 

But .. Why is this happening?

+8
ruby-on-rails ruby-on-rails-3
source share
3 answers

This is because Rails 3 introduced the SafeBuffer class, which wraps the String class and overrides the default sketch that would otherwise have occurred when calling concat.

In your case, content_tag (: li) outputs the correct SafeBuffer, but the Array # union does not understand SafeBuffers and just outputs String. Then content_tag (: ol) is called with a String value as the value instead of SafeBuffer and escapes it. Thus, this is not so much related to nesting as to a connection returning String, not SafeBuffer.

Calling html_safe on a String, passing a String to raw, or passing an array to safe_join will return the proper SafeBuffer and prevent it from leaking from the external content_tag.

Now, in the case of passing false to the escape argument, this does not work when you pass the block to the content tag, because it calls capture(&block) ActionView :: Helpers :: CaptureHelper, which is used to pull the template, or your case is the output value of the union, which then calls it to call html_escape on the line before it enters the content_tag_string method.

  # action_view/helpers/tag_helper.rb def content_tag(name, content_or_options_with_block = nil, options = nil, escape = true, &block) if block_given? options = content_or_options_with_block if content_or_options_with_block.is_a?(Hash) # capture(&block) escapes the string from join before being passed content_tag_string(name, capture(&block), options, escape) else content_tag_string(name, content_or_options_with_block, options, escape) end end # action_view/helpers/capture_helper.rb def capture(*args) value = nil buffer = with_output_buffer { value = yield(*args) } if string = buffer.presence || value and string.is_a?(String) ERB::Util.html_escape string end end 

Since the value here is the return value from join, and join returns String, it calls html_escape before the content_tag code even gets to it with its own escape.

Some links for those interested

https://github.com/rails/rails/blob/v3.1.0/actionpack/lib/action_view/helpers/capture_helper.rb

https://github.com/rails/rails/blob/v3.1.0/actionpack/lib/action_view/helpers/tag_helper.rb

http://yehudakatz.com/2010/02/01/safebuffers-and-rails-3-0/

http://railsdispatch.com/posts/security

Edit

Another way to deal with this is to make a map / reduction instead of map / join, because if reduce is not passed as an argument, it will use the first element and start this operation using this object, which in the case of a map content_tag will trigger an operation on SafeBuffer.

 content_tag(:ol) do (1..5).to_a.map do content_tag(:li) do link_to(...) end end.reduce(:<<) # Will concat using the SafeBuffer instead of String with join end 

As single line

 content_tag(:ul) { collection.map {|item| content_tag(:li) { link_to(...) }}.reduce(:<<) } 

Add some meta spices to clean things.

 ul_tag { collection.map_reduce(:<<) {|item| li_link_to(...) } } 

Who needs html_safe ... it's Ruby!

+15
source share

What happens if you use safe_join ?

 content_tag(:ol) do safe_join (1..5).to_a.map { content_tag(:li) { link_to("boo", "www.boohoo.com") } }, '' end 

Or just use raw?

 content_tag(ol) do 1.upto(5) { raw content_tag(:li) { link_to 'boo', 'www.boohoo.com' } # or maybe # raw content_tag(:li) { raw link_to('boo', 'www.boohoo.com') } } end 
+4
source share

Not bad, but I think that html escaping happens on every "layer" (due to the lack of a better term, each iteration) - which I mean at the level of the inner block (1..5) .... and then at the level external block (content_tag (: ol) do ...

0
source share

All Articles