Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

safe_join and content_tag approach to replace html_safe

I am building a block of HTML code using Rails' content_tag helper. The challenge I face right now is joining HTML strings from an array with HTML elements generated by content_tag.

RuboCop Rails/OutputSafety reference.

For example:

options = ["<li>Three</li>", "<li>Four</li>", "<li>Five</li>"]

# This is code to generate blocks of HTML
out = []
out << content_tag(:ul,  
   content_tag(:li, "One") + 
   content_tag(:li, "Two") + 
   options.join(''),
:class => ["class_1", "class_2"])
safe_join(out)

# Expect result should be like
<ul class="class_1 class_2">
   <li>One</li>
   <li>Two</li>
   <li>Three</li>
   <li>Four</li>
   <li>Five</li>
</ul>

# Actual result
<ul class="class_1 class_2">
   <li>One</li>
   <li>Two</li>
   "<li>Three</li><li>Four</li><li>Five</li>"
</ul>

However, if I use the html_safe approach like below, it will work.

%{<ul>
   <li>One</li>
   <li>Two</li>
   #{options.join('')}
 </ul>
}.html_safe

Any suggestions on what I should change?

# New apporach
options = ["Three", "Four", "Five"]
out = []
out << content_tag(:ul,  
   content_tag(:li, "One") + 
   content_tag(:li, "Two") + 
   options.collect do |option|
      content_tag(:li, "#{option[0]}")
   end.join(""),
:class => ["class_1", "class_2"])
safe_join(out)

# New approach result
<ul class="class_1 class_2">
   <li>One</li>
   <li>Two</li>
   "<li>Three</li><li>Four</li><li>Five</li>"
</ul>
like image 643
zihaow Avatar asked Mar 09 '23 03:03

zihaow


1 Answers

The problem is that you are concatenating your output with unsafe strings coming from options array. This is the only place where you should use html_safe method for the whole output to be safe:

out << content_tag(:ul,  
   content_tag(:li, "One") + 
   content_tag(:li, "Two") + 
   options.join('').html_safe,
:class => ["class_1", "class_2"])

Edit

First of all safe_join method does not work like html_safe method, it doesn't only make the joined strings html_safe. It also makes html escaping if the joined string are not html_safe in order to avoid harmful content.

https://apidock.com/rails/ActionView/Helpers/OutputSafetyHelper/safe_join

In your case safe_join method didn't do anything at all with the strings in out array, since they had already been html_safe.

result = content_tag(:ul,  
           content_tag(:li, "One") + 
           content_tag(:li, "Two") + 
           options.join(''),
           :class => ["class_1", "class_2"])

result.html_safe? # => true

The cause of the problem is that you concatenated a safe string with unsafe one:

content_tag(:li, "Two") + options.join('')

content_tag(:li, "Two").html_safe? # => true
options.join('').html_safe?        # => false

At that moment options.join('') was html escaped since it wasn't safe. See the example:

# html tags in the second string are escaped, since it is not safe
"<li>One</li>".html_safe + "<li>Two</li>" # => "<li>One</li>&lt;li&gt;Two&lt;/li&gt;"

# nothing has been escaped, since everything is safe
"<li>One</li>".html_safe + "<li>Two</li>".html_safe # => "<li>One</li><li>Two</li>"

So, in order to get expected results 2 conditions must be met:

  1. safe_join method must take array of html_safe strings. If they are not html_safe, all html tags will be escaped.
  2. Do not concatenate a safe string with an unsafe one, otherwise the unsafe string will be escaped.

As you can see, you did not fulfill the second condition.

Suggestions about the new approach

.join("") method makes the result string unsafe, even if the array contains safe strings. Use safe_join:

   content_tag(:li, "One") + 
   content_tag(:li, "Two") + 
   safe_join(
     options.collect do |option|
       content_tag(:li, option)
     end
   )
like image 144
chumakoff Avatar answered Mar 16 '23 07:03

chumakoff