Lost In Binding - Adventures In Ruby Metaprogramming
Posted by Guy Naor Wed, 28 Mar 2007 07:00:00 GMT
I've been using the security_extensions plugin to secure forms in Famundo and some other projects. It's a very simple plugin that adds protection against CSRF.
When upgrading one of my projects to Rails 1.2, I got a deprecation warnings from Rails, as this plugin requires start_form_tag and end_form to work. Thinking it was all easy to change, I replaced all calls to the new form_tag ... do format. This change resulted in lots of errors, all complains from erb on missing the _erbout variable.
After a lot of digging, I realized this error is caused by the way variables bindings work in Ruby, and the fact that erb uses it to pass along the output string it creates.
What are bindings? Bindings are (put very simply) the context for the variables in an execution block. It's what's used in Ruby to bind a variable to a block and have the block access it even after the variable went out of scope in the original code block, or the variable is re-defined in a new block. Here is some code to make it clear:
# Define the a var
a = 5
# Create a proc that prints a
level_a = lambda { puts a }
level_a.call # => 5
# Create a method that accepts a proc, redefines a and calls the proc
def level_b(blk)
a = 10
blk.call
end
level_b(level_a) # => 5 The best place I found to learn about bindings is this page.
How is this affecting the security_extensions plugin? The plugin needs to wrap the block given to the form, and inject into it the hidden field used to validate the form when it's posted back. When using the non-block accepting start_form_tag, it just appends a new field at the end:
def secure_form_tag(*args)
return start_form_tag(*args) + "\n" +
hidden_field_tag('session_id_validation', security_token)
endThe simple solution I thought will work, is very simple:
def secure_form_tag(url_for_options = {}, options = {}, *parameters_for_url, &block)
if block_given?
form_tag(url_for_options, options, *parameters_for_url) do
yield
end
hidden_field_tag("session_id_validation", security_token)
else
"#{form_tag(*args)} \n #{hidden_field_tag('session_id_validation', security_token)}"
end
endIt failed. I decided to try and call the external block directly:
def secure_form_tag(url_for_options = {}, options = {}, *parameters_for_url, &block)
if block_given?
form_tag(url_for_options, options, *parameters_for_url) do
block.call
end
hidden_field_tag("session_id_validation", security_token)
else
"#{form_tag(*args)} \n #{hidden_field_tag('session_id_validation', security_token)}"
end
endFailed again! The problem is that the context of the internal block is completely different from the context of the block passed to the function, and so the _erbout variable isn't bound to the internal block, only to the external one.
The solution I used was to copy the binding from the passed block into the internal block, call the passed block, and then copy it back from the internal block to the passed block. Here is the code to do it:
def secure_form_tag(url_for_options = {}, options = {}, *parameters_for_url, &block)
if block_given?
_erbout = eval('_erbout', block)
form_tag(url_for_options, options, *parameters_for_url) do
concat(hidden_field_tag("session_id_validation", security_token), block.binding)
eval "_erbout = %q[#{_erbout}]"
yield
end
eval "_erbout = %q[#{_erbout}]", block
else
"#{form_tag(*args)} \n #{hidden_field_tag('session_id_validation', security_token)}"
end
endThis code does a lot of copying of strings, but as it's in very specific places that aren't performance sensitive, I rather get the nice way to use secured forms, and incur the performance penalty in this case.
The same trick can be use to extended other erb related methods that use blocks and need the _erbout bindings.
If there are better solutions, let me know. I'd love a simpler solution for this.


















What if you just did a concat on the hidden tag and the block's binding and then pass that block into the form_for method.
http://pastie.caboo.se/50396
Not tested, but an idea.
I've not tried it, but what's wrong with:
If you don't create a new block, you don't have to create a new binding.
Piers,
Unfortunately this can't work. I wish it was that simple :-).
The reason it doesn't, is that when using
the form and /form tags wrap the form elements around the block. And so our added hidden field will be outside the form. We need a way to inject the hidden field into the block.Ah yes, of course.
Further inspection of the docs suggests that:
Should do the trick. All that ugliness with
string_evalstill happens, but it's hidden behind the abstraction wall.I may have got a little bit Higher Order Function happy on
concatenate's ass.Dang, forgot to replace both calls to
hidden_field_tagin that rewrittensecure_form_tag. L'esprit d'escalier strikes again.Piers,
This is nice, though like you said, not much different with regard to all the copying of the strings, which I suspect can't be prevented.
Thanks for the idea!
Piers, unfortunately the code you posted still doesn't work. The reason it doesn't is that we still have an internal block in the call to form_tag that goes into the concatenator.
But I did find a way of using capture which simplifies the code (though still makes it look a bit hackish...):
BTW, notice the need to use @res to make it work, or the var is hidden from the block bindings.
Thanks again for the capture trick, didn't see it before :-)