views:

130

answers:

3

I need a bunch of links on a page each of which makes a POST to a different controller. But when I use normal links, I get a ActionController::InvalidAuthenticityToken error. I understand this is because of the missing authenticity_token value. But I don't want to use forms to do the POST because I want them to be links and not buttons. Fact is, I want complete control over the styling of the links and buttons just don't do it for me. What's the standard way of doing such things?

+1  A: 

You have lots of options.

  1. Disable AT check: protect_from_forgery :only => []
  2. Assign onclick handler to the link and submit hidden form with javascript.
  3. Grab AT while generating view and add it as request parameter.

BTW, how exactly do you make 'post' requests using only 'href' attribute? I thought form is a must for that.

Nikita Rybak
Rails lets you specify a `_method=(PUT|POST|DELETE)` query parameter and it will interpret the request as though it were made with that method. It's primarily to support `PUT`/`DELETE` which don't have full browser support.
Chris Heald
I don't really know the security implications of disabling AT check. I could try adding it as a request parameter though. But the POST is done to a different controller than what created this view. Should that matter? What is the scope of this token?
manu1001
@manu1001 As far as I understand, AT is created for particular session. So, it should work. Anyway, it's easy to try: 'my_url?authenticity_token=<%= form_authenticity_token %>'
Nikita Rybak
@Chris thanks, good to know
Nikita Rybak
A: 

Technically speaking, you should be using buttons and forms for anything that isn't a GET; hyperlinks intentionally don't allow for methods other than GET (without hacks like the _method parameter). One very practical reason is that sometimes, "web accelerator" browser add-ons prefetch links in the page; if a GET link kicks off a mutative action, the user or resource state may be erroneously modified.

That said, you can style buttons to behave like links; I use something like the following to do it quite nicely. It assumes a proper CSS reset with margins and padding and all that good stuff being nilled.

input.restlink {
  border: 0;
  background: #fff;
  color: #236cb0;
  cursor: pointer;
}
input.restlink:hover {
  text-decoration: underline;
}

With a rule like that, you can use <%=button_to "Yay button", something_path, :method => :post %> and it'll look and behave like a link just fine.

Chris Heald
I considered this but it didn't work out because there's still that annoying push effect when you mouse-down on a button; along with a dotted-line around it on some browsers.
manu1001
With a proper CSS reset, neither of those is the case. You can set `outline: none` for the browsers that display the dirty rectangle.
Chris Heald
A: 

If you aren't opposed to using jQuery and some ajax'n, I have a blog post that covers this.

Here's some basic info if you'd like a high level overview.

Add this one your layout:

<%= javascript_tag "var AUTH_TOKEN = #{form_authenticity_token.inspect};" if protect_against_forgery? %>

This code adds the auth token to the response. This way the JS can pick it up and submit it to the server.

Next we intercept any ajax call in the application.js:

function isPost(requestType) {
  return requestType.toLowerCase() == 'post';
}

$(document).ajaxSend(function(event, xhr, settings) {
  if (isPost(settings.type)) {
    settings.data = (settings.data ? settings.data + "&" : "") + "authenticity_token=" + encodeURIComponent( AUTH_TOKEN );
  }
  xhr.setRequestHeader("Accept", "text/javascript, application/javascript");     
});

Add this to your application controller:

before_filter :correct_safari_and_ie_accept_headers
after_filter :set_xhr_flash

protected
    def set_xhr_flash
      flash.discard if request.xhr?
    end

    def correct_safari_and_ie_accept_headers
      ajax_request_types = ['text/javascript', 'application/json', 'text/xml']
      request.accepts.sort!{ |x, y| ajax_request_types.include?(y.to_s) ? 1 : -1 } if request.xhr?
    end

And in your view:

<%= link_to "Delete", delete_product_path(product), :class => 'delete' %>

Back over to application.js:

$('a.delete').live('click', function(event) {
  if(event.button != 0) { return true; }

  $.post(link.attr('href').substring(0, link.attr('href').indexOf('/delete')), { _method: "delete" });

  return false;
});

This example does a delete but it's really the same process to handle posts or puts. The blog post has a full sample application that demonstrates this.

Andy Gaskell