In this chapter we'll be talking about forms and form helpers. The first one we'll be introducing is form_tag
, but before we talk about the form_tag
helper, let's first talk about the various reasons to use this helper.
CSRF or Cross-Site Request Forgery is a type of attack on an application. CSRF utilizes malicious code hidden in one application to affect the data in another application. For instance, consider a link written by a hacker, and in that link some sort of call to our application API is hidden within. Malicious code like this can sometimes be found within the attributes of image or link tags.
Here's an example what was mentioned above:
<a href="http://example.com" name="example" onclick="www.our_app/posts/1/delete">...</a>
If such a link was clicked, that html event of onclick
would fire off. The value in onclick
would get added to our cookies. If we were still logged into our app when we clicked the above link, then that cookie would be sent to our application, and verified with our current session id. Clicking the link above could then delete a post on our application without the user even realizing it.
So, how do we prevent such an attack on our application. Rails does this with the use of an authenticity_token
. This token is a random string stored in our session. Every time a javascript or html based request is made, it is checked for an authenticity token. If the token sent with the request from a form does not match the one stored in our application's session, then an exception will be thrown.
We can add this authenticity token to our session with one simple line, added to our application controller.
protect_from_forgery with: :exception
Let's start by enabling CSRF protection. We'll add the line of ruby code listed above to our Application Controller.
class ApplicationController < ActionController::Base
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception
# ...
end
To test out this CSRF protection, we'll use our form for creating a new post. If this works as expected then we shouldn't be able to make a new post without that authenticity token within our form.
First we'll need to make a new RESTful view for creating a new post; we can move the code from our new_post
view to this file. Don't forget to change the local variable post
to the instance variable that we are passing in from our PostsController
, @post
.
<!-- app/views/posts/new.html.erb -->
<html>
<body>
<!-- ... -->
<form method="post" action="/posts">
<label for="title">Title</label>
<input id="title" name="title" type="text" value="<%= @post.title %>"/>
<br /> <br />
<label for="body">Body</label>
<textarea id="body" name="body"><%= @post.body %></textarea>
<br /> <br />
<label for="author">Author</label>
<input id="author" name="author" type="text" value="<%= @post.author %>"/>
<br /> <br />
<input type="submit" value="Create Post" />
</form>
<!-- ... -->
</body>
</html>
Let's see what happens when we try to create a post:
Started POST "/posts" for ::1 at 2016-04-13 15:13:37 -0700
Processing by PostsController#create as HTML
Parameters: {"title"=>"Another Post", "body"=>"Test", "author"=>"Missy"}
Can't verify CSRF token authenticity
Completed 422 Unprocessable Entity in 1ms (ActiveRecord: 0.0ms)
This occurs because we've turned on checks for CSRF authenticity tokens, but we haven't included one in our form. Let's do that now:
<!-- app/views/posts/new.html.erb -->
<html>
<body>
<!-- ... -->
<form method="post" action="/posts">
<label for="title">Title</label>
<input name="authenticity_token" type="hidden" value="<%= form_authenticity_token %>">
<input id="title" name="title" type="text" value="<%= @post.title %>"/>
<br /> <br />
<label for="body">Body</label>
<textarea id="body" name="body"><%= @post.body %></textarea>
<br /> <br />
<label for="author">Author</label>
<input id="author" name="author" type="text" value="<%= @post.author %>"/>
<br /> <br />
<input type="submit" value="Create Post" />
</form>
<!-- ... -->
</body>
</html>
We set a hidden input element with a name of authenticity_token
and a value that is set by the form_authenticity_token
method. This method looks at the token saved in our session and returns it for use here.
With the code above, our form works, and this part of our code is now protected from a CSRF attack.
One problem with our method above is that we'll have to remember to include this hidden input tag in all of our forms. Rails provides a form helper that gives us a bit of a shortcut. The form helper, form_tag
generates an HTML form for us. If CSRF protection is enabled, then that hidden input is automatically added to our form. form_tag
helper also gives us a few options for customizing our form in an effective and intuitive way.
Here is the code we would need to make the form used above with a form_tag
helper:
<html>
<body>
<!-- ... -->
<%= form_tag url_for(action: 'create'), method: "post" do %>
<%= label_tag 'Title' %>
<%= text_field_tag 'title', @post.title %>
<br /> <br />
<%= label_tag 'Body' %>
<%= text_area_tag 'body', @post.body %>
<br /> <br />
<%= label_tag 'Author' %>
<%= text_field_tag 'author', @post.author %>
<br /> <br />
<%= submit_tag "Create Post" %>
<% end %>
<!-- ... -->
</body>
</html>
There are a lot of changes here, so let's go through them one by one. First, notice that we start with the syntax:
<%= form_tag url_for(action: 'create'), method: "post" do %>
We have form_tag
, this is a form helper method that creates a form tag for us
and allows us to specify various options for our form. To make sure that our form is submitted
to the correct action, we can specify a url and the HTTP method(verb) used to submit this form.
We're using a Rails method to build up the URL we want the form to submit to. This
method is url_for
. It gives us several options to create a URL. One option is to specify
only the action, as we do above: by doing so any other missing values from our URL
are filled in with values from the current request.
The current request for making a new post would have a URL of /posts
.
This lets Rails know which controller this URL will be for. From there, specifying the
action in url_for
lets us further refine the URL based on our routes.
In this case, url_for(action: 'create') #=> '/posts'
. This URL is pretty short, we could write it manually. But you could imagine that using url_for
would be necessary for submitting forms to resources nested deep within our application.
We've previously also used URL path helpers to generate URLs. We'll be using these for our forms going forward, along with url_for
.
The next part of our form_tag
utilizes the option method
. This option shows us one other situation where using form_tag
versus writing out a form manually can be quite helpful. HTML forms can only use the GET
and POST
HTTP verbs. For some actions, we don't want to use GET or POST
. If we want to delete a post we would want to use the DELETE
HTTP verb.
Recall, that to do this we used a Rails convention where we had to set a hidden input field in our form. Then, we made sure to have a method
attribute for that input tag of "delete"
; this allowed us to submit a form with the DELETE
HTTP verb. The method
option does just that for us, depending on what value we set for the method
option. If we use method="delete"
in the form_tag
above, then a hidden input field with method attribute delete
will be automatically added to our form.
Let's continue examining what makes up our form_tag
and its helpers.
For each tag we want to create, we have a helper method to dynamically make it.
label_tag
→ <label></label>
text_field_tag
→ <input type='text'>
text_area_tag
→ <textarea><textarea>
submit_tag
→ <input type='submit'>
Here is a list of the arguments we've used with these helpers and what each argument is used for:
text_field_tag
and text_area_tag
helpers sets the name
and id
attributes for the tag we are creating. The second argument sets the content/value for that tag.
label_tag
takes one to three arguments. If only one argument is passed, then that value is used to set the for
attribute and the content for that label. If a second argument is passed in, then that second argument is used to set the content of the label instead. A third argument, as a hash, may be passed in to set additional HTML attributes.
submit_tag
takes one to two arguments. The first argument sets the value of the submit tag, the second is a list of options for configuring the submit tag.
All of the form helpers listed above as well as many other form helpers can also be passed a list of options. We don't need those options at the moment though, so if you're curious about that information, you can find more on it in the relevant documentation.
We've gotten a look at form_tag
and its field helper methods as well, the tag
methods. Let's put that knowledge to good use. In this section, we'll convert the rest of our views that use plain HTML forms to use Rails form_tag
. Here are some things to keep in mind when making the conversion.
Make sure that views:
name="_method"
to specify the correct HTTP verb when necessary
Recall that so far we have adjusted the view templates for:
<!-- app/views/posts/index.html.erb -->
<%= form_tag post_path(post.id), method: "delete", style: "display: inline" do %>
<%= submit_tag "Delete" %>
<% end %>
<!-- app/views/posts/show.html.erb -->
<%= form_tag post_comment_path(@post.id, comment.id), method: "delete" do %>
<%= submit_tag "Delete Comment" %>
<% end %>
<%= form_tag post_comments_path(@post.id) do %>
<%= label_tag 'Comment' %>
<%= text_area_tag 'body', @comment.body %>
<br /> <br />
<%= label_tag 'Name' %>
<%= text_field_tag 'author', @comment.author %>
<br /> <br />
<%= submit_tag "Add Comment" %>
<% end %>
<!-- app/views/posts/edit.html.erb -->
<%= form_tag url_for(action: 'update'), method: "patch" do %>
<%= label_tag 'Title' %>
<%= text_field_tag 'title', @post.title %>
<br /> <br />
<%= label_tag 'Body' %>
<%= text_area_tag 'body', @post.body %>
<br /> <br />
<%= label_tag 'Author' %>
<%= text_field_tag 'author', @post.author %>
<br /> <br />
<%= submit_tag 'Update Post' %>
<% end %>
Let's go over what parameters from our form look like when we receive them in the controller.
For this step, we'll put a binding.pry
in our create
action for the PostsController
.
def create
binding.pry
@post = Post.new('author' => params[:author],
'title' => params[:title],
'body' => params[:body])
if @post.save
redirect_to posts_path
else
render 'new'
end
end
Next, we'll fill out our new post form and submit it. If we look at our params
hash, we'll see that author, title, and body each show up as individual keys at the highest level of this hash, along with the controller and action keys.
[1] pry(#<PostsController>)> params
=> {"utf8"=>"✓",
"authenticity_token"=>"wYCWFeQFIDOLk4tkFd+gHUhLHlw5od3UL+RKcjKrXInSNDPZ+SfzYQrO3KW6TU0zwGuh36q4db1rfJzu+k/Kuw==",
"title"=>"A New Post",
"body"=>"Hello World",
"author"=>"Alice Cooper",
"commit"=>"Create Post",
"controller"=>"posts",
"action"=>"create"}
This may be fine with our current application. But if our form was more complicated or if we were passing more information to our controller, then we would maybe want further organization of the data related to our new post. Rails provides a way to organize related parameters into a sub-hash. To do this we need to change the name attribute for the form data that we want grouped together.
Here is what our form currently looks like:
<!-- app/views/posts/new.html.erb -->
<html>
<body>
<!-- ... -->
<%= form_tag url_for(action: 'create'), method: "post" do %>
<%= label_tag 'Title' %>
<%= text_field_tag 'title', @post.title %>
<br /> <br />
<%= label_tag 'Body' %>
<%= text_area_tag 'body', @post.body %>
<br /> <br />
<%= label_tag 'Author' %>
<%= text_field_tag 'author', @post.author %>
<br /> <br />
<%= submit_tag "Create Post" %>
<% end %>
<!-- ... -->
</body>
</html>
Now, let's change the form so that parameters related to our post are grouped together in our params hash.
<!-- app/views/posts/new.html.erb -->
<html>
<body>
<!-- ... -->
<%= form_tag url_for(action: 'create'), method: "post" do %>
<%= label_tag 'Title' %>
<%= text_field_tag 'post[title]', @post.title %>
<br /> <br />
<%= label_tag 'Body' %>
<%= text_area_tag 'post[body]', @post.body %>
<br /> <br />
<%= label_tag 'Author' %>
<%= text_field_tag 'post[author]', @post.author %>
<br /> <br />
<%= submit_tag "Create Post" %>
<% end %>
<!-- ... -->
</body>
</html>
Our params hash now looks like:
[1] pry(#<PostsController>)> params
=> {"utf8"=>"✓",
"authenticity_token"=>"PSdbhGz0Cb2ic0PO2GwmNs9+3rSxLp0wNAVR6OHvGdIuk/5Icdba7yMuFA93/ssYR15hNyI3NVlwnYd0KQuP4A==",
"post"=>{"title"=>"A New Post", "body"=>"Hello World", "author"=>"Alice Cooper"},
"commit"=>"Create Post",
"controller"=>"posts",
"action"=>"create"}
Notice that everything is a bit easier to read. Any fields related to our post are in a nested hash. While we're at it, lets also make this change to our form used to create a new comment. Make sure that the file for showing a post and creating a comment follow Rails conventions.
views/application/show_post.html.erb
-> views/posts/show.html.erb
comment
-> @comment
post
-> @post
comments
-> @post.commments
<!-- app/views/posts/show.html.erb -->
<html>
<body>
<!-- ... -->
<!-- We're using a url helper to post data to the correct action. -->
<%= form_tag post_comments_path(@post.id) do %>
<%= label_tag 'Comment' %>
<%= text_area_tag 'comment[body]', @comment.body %>
<br /> <br />
<%= label_tag 'Author' %>
<%= text_field_tag 'comment[author]', @comment.author %>
<br /> <br />
<%= submit_tag 'Add Comment' %>
<% end %>
<!-- ... -->
</body>
</html>
Here is what our params hash looks like when we apply this rails parameter convention for creating a new comment:
[1] pry(#<CommentsController>)> params
=> {"utf8"=>"✓",
"authenticity_token"=>"cz2+kiA91zb8sEdxotc6U7JCPTQ40ITM2M26oRQ2TYlgiRtePR8EZH3tELANRdd9OmKCt6vJLKWcVWw93NLbuw==",
"comment"=>{"body"=>"New Comment", "author"=>"Number 5"},
"commit"=>"Add Comment",
"controller"=>"comments",
"action"=>"create",
"post_id"=>"23"}
If you are following along, there is something that you should have noticed after these changes. Our create actions don't seem to work. If we take a closer look at our actions we may notice the issue:
# /app/controllers/posts_controller.rb
# ...
def create
@post = Post.new('author' => params[:author],
'title' => params[:title],
'body' => params[:body])
if @post.save
redirect_to posts_path
else
render 'new'
end
end
# ...
# /app/controllers/comments_controller.rb
# ...
def create
@comment = @post.build_comment(
'body' => params[:body], 'author' => params[:author]
)
if comment.save
# redirect for success
redirect_to posts_path(@post.id)
else
# render form again with errors for failure
render 'posts/show'
end
end
Recall, that by changing the name
attributes in our forms, we changed the structure of our parameters hash. So, if we want to access values related to a post, we use params[:post]
, and if we want to access values related to a comment, we'll have to use params[:comment]
.
Using the examples above we have:
params[:post] #=> {"title"=>"A New Post", "body"=>"Hello World", "author"=>"Alice Cooper"}
params[:comment] #=> {"body"=>"New Comment", "author"=>"Number 5"}
This structure for our parameters gives us a shorter, more concise syntax for object creation. Let's fix those create actions by using the correct calls to params
.
# /app/controllers/posts_controller.rb
# ...
def create
@post = Post.new(params[:post])
if @post.save
redirect_to posts_path
else
render 'new'
end
end
# ...
# /app/controllers/comments_controller.rb
# ...
def create
@comment = @post.build_comment(params[:comment])
if comment.save
# redirect for success
redirect_to posts_path(@post.id)
else
# render form again with errors for failure
render 'posts/show'
end
end
Make sure to also apply these changes to the forms we've been using in our app. Also, make sure you update their corresponding controller actions as well.
There is a name for when we use a group of values to update an object. It's called mass assignment. By accessing a single k-v pair from our params
hash, we're able to assign many attributes at once. While convenient, mass assignment also opens up our application to a security risk. How do we know that only the parameters we want assigned will be updated for our object?
By using mass assignment, we're blindly allowing any number of parameters for assignment, as long as they are included in our form. Rails does give us a way to safeguard our data when using mass assignment, which we'll learn about later on in this book.