ruby-on-railsactiveview

Why does ActionView's "select_tag" escape the HTML in its "options" argument when called directly?


So I've been going through the very poor documentation that exists for ActionView, particularly a method called select_tag that just exists when called from view files. Supposedly, it's called like this:

select_tag name, option_tags, options

The documentation says nothing at all about the name, very little about the options, and describes the option_tags only through examples that treat it as an opaque value that must be obtained from other functions.

As always, the only way to learn anything about Rails is to reverse-engineer it.

So I tried running it directly from a Rails console, which is tricky because Ruby doesn't let you call methods that are defined in modules unless you create a class and an object first:

class H
  include ActionView::Helpers::FormOptionsHelper
  include ActionView::Helpers::FormTagHelper
end

H.new.options_for_select ["foo","bar"]

The above usage of options_for_select comes from actual code that somebody else wrote. The return value is a string:

"<option value=\"foo\">foo</option>\n<option value=\"bar\">bar</option>"

So apparently, you're supposed to pass the return value from option_for_select (or one of the many other related functions that introduce complications I don't want to talk about such as generating HTML tags from ActiveRecord objects) as the option_tag parameter of select_tag. Except if you copy that string to your clipboard and paste it directly into a function call, it doesn't do what you'd expect:

H.new.select_tag :my_name, "<option value=\"foo\">foo</option>\n<option value=\"bar\">bar</option>"

Return value:

"<select name=\"my_name\" id=\"my_name\">&lt;option value=&quot;foo&quot;&gt;foo&lt;/option&gt;\n&lt;option value=&quot;bar&quot;&gt;bar&lt;/option&gt;</select>"

At least this reveals what the name parameter is for.

Even weirder, the text is not escaped if you pass the return value directly to select_tag without letting it print on the console:

H.new.select_tag :name, H.new.options_for_select(["foo","bar"])

Return value:

"<select name=\"name\" id=\"name\"><option value=\"foo\">foo</option>\n<option value=\"bar\">bar</option></select>"

WTF is going on here?


Solution

  • In the course of writing this question, I stumbled on its answer: Ruby has been lying to me (like it always does).

    When you evaluate:

    H.new.options_for_select ["foo","bar"]
    

    Ruby tells you that the result was a String. But that's only because Pry and Irb both silently call .to_s on everything, and the thing that gets returned from options_for_select has a to_s. The truth:

    (H.new.options_for_select ["foo","bar"]).class
    => ActiveSupport::SafeBuffer
    ActiveSupport::SafeBuffer.new("<foo>")
    => "<foo>"
    

    So whoever wrote these methods assumed that you want to incorporate raw, user-provided strings into your <select> tags, and those strings could contain attempts at HTML/JavaScript injection, so they must be escaped.

    ActiveView treats all strings as suspect, but it is possible to mark certain strings as "safe" by wrapping them in an ActiveSupport::SafeBuffer.