Section 8.5. The Use and Abuse of HTTP Methods


8.5. The Use and Abuse of HTTP Methods

In the spring of 2005, Google introduced a browser plug-in called Google Web Accelerator (GWA), which set off heated discussions in the Rails community. The reason is that GWA worked by pre-fetching links. Upon loading a page, GWA would scan it for links and load them before they were even clickedso when the user did click, the next page would already be cached and load much faster.

The problem was that many Rails applications (including Basecamp, the original Rails application) used regular links for destructive actions, such as "delete this post." So if you installed GWA and then visited your Basecamp account, the plug-in triggered a wave of data loss. Users and developers alike were understandably quite upset by the unintended consequences.

Google quickly cancelled the product in response to the uproar. But technically, the plug-in wasn't doing anything wrong (besides being wasteful with bandwidth). GWA was only creating HTTP GET requests, which, according the spec, are supposed to be safe for intermediaries like GWA to use. The real problem was that Rails developers had adopted the bad habit of using GET to trigger deletes.

The lesson was hard-learned, but important. Today, Rails is leading the charge among web frameworks to support the full vocabulary of HTTP methods, beyond just GET and POST. With most helpers, the fix is as simple as providing a :method option. For example, to create a proper delete link:

<%= link_to 'Delete Contact',             contact_url(:id => contact),             :method => :delete %>

Instead of creating a standard link, this helper will create a JavaScript linkone that looks just the same, but has a script in the onclick attribute. The script jumps through the necessary hoops to send the right request. Because browsers generally don't support the DELETE method, Rails piggybacks on the POST method by sending an extra parameter (_method) along with the request. It's not ideal, but it's an acceptable stopgap solution until browsers support more methods. The output of the above helper is this:

<a href="/contacts/1"     onclick="var f = document.createElement('form');             f.style.display = 'none';             this.parentNode.appendChild(f);             f.method = 'POST';             f.action = this.href;             var m = document.createElement('input');             m.setAttribute('type', 'hidden');             m.setAttribute('name', '_method');             m.setAttribute('value', 'delete');             f.appendChild(m);             f.submit(  );             return false;">Delete Contact</a>

Upon clicking the link, the JavaScript actually creates a new hidden form and input field and submits it. The effect is totally transparent to the end user, but as far as Rails is concerned, the incoming request is a full-fledged HTTP DELETE request.

This brings us to the second half of the equation, the server side. Employing JavaScript to use the correct request method is nice, but if your destroy action still responds to GET, you're still vulnerable. There are several ways to tackle the problem.

From within an action, the request object represents all that's known about the current request. So to find out the request method, you'd use (shockingly) request.method. The value will be one of five symbols: :get, :post, :put, :delete, and :head. The request object also provides corresponding Boolean "question-mark" methods, such as request.get? and request.post?.

For example, consider account confirmation, a common feature of web applications. In order to deter spammers, new users are emailed a confirmation link, which they're supposed to click before the account is activated. Most implementations of this pattern are flawed, because they use GET requests to change state on the server. A better approach is to check the request method and show a confirmation form if the incoming request is a GET. That kind of conditional processing is made easy by request.post? and friends:

def confirm   @user = User.find_by_token params[:id]   if request.post?     @user.update_attributes :confirmed => true     redirect_to home_url   else     render :inline => %Q(<%= start_form_tag %>                          <%= submit_tag "Confirm Account" %>                          <%= end_form_tag %>)   end end

Alternatively, verify, a specialized kind of before_filter, can be used to limit which request methods are allowed for each action. Options provided to verify will determine what happens if the conditions aren't met, such as redirecting and adding a flash. For example:

class UsersController < ApplicationController   verify :only        => :confirm,          :method      => :post,          :add_flash   => { "notice" => "Please confirm your account." },          :redirect_to => :confirm_form   def confirm_form     render :inline => %Q(<%= start_form_tag %>                          <%= submit_tag "Confirm" %>                          <%= end_form_tag %>)   end   # only POSTS will be able to reach this action   def confirm     @user = User.find_by_token params[:id]     @user.update_attributes :confirmed => true     redirect_to home_url   end end

Another solution is to use routes. For example:

# only matches if the request method is GET map.connect "/confirm/:id", :controller => "users",                             :action => "confirm_form",                             :conditions => { :method => :get } # only matches if the request method is POST map.connect "/confirm/:id", :controller => "users",                             :action => "confirm",                             :conditions => { :method => :post }

In many cases, you can automatically get the benefits of the :conditions option by using map.resources. For example:

ActionController::Routing::Routes.draw do |map|   map.resources :products   map.connect  ':controller/:action/:id' end

The resources method generates a whole slew of named routes, and it uses :conditions to direct the same path to multiple actions, depending on the HTTP method. Table 8-1 shows all of the routes generated by map.resources :products.

Table 8-1. Routes generated by map.resources :products
Route nameRouteActionMethod
products

/products/

index, create

GET, POST

formatted_products

/products.:format/

index, create

GET, POST

new_product

/products/new/

new

GET

formatted_new_product

/products/new.:format

new

GET

product

/products/:id/

show, update, destroy

GET, PUT, DELETE

formatted_product

/products/:id.:format/

show

GET

edit_product

/products/:id;edit/

edit

GET

formatted_edit_product

/products/:id.:format;edit

edit

GET





Ajax on Rails
Ajax on Rails
ISBN: 0596527446
EAN: 2147483647
Year: 2006
Pages: 103
Authors: Scott Raymond

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net