Welcome back to Singing with Sinatra! In this third and final part we'll be extending the "Recall" app we built in the previous lesson. We're going to add an RSS feed to the app with the incredibly useful Builder gem, which makes creating XML files in Ruby a piece of cake. We'll learn just how easy Sinatra makes escaping HTML from user input to prevent XSS attacks, and we'll improve on some of the error handling code.
Users Are Bad, m'kay
The general rule when building web apps is to be paranoid. Paranoid that every one of your users is out to get you by destroying your site or attacking other users through it. In your app, try adding a new Note with the following content:
</article>woops <script>alert("zomg haxz");</script>
Currently our users are free to enter whatever HTML they like. This leaves the app open to XSS attacks where a user may enter malicous JavaScript to attack or misdirect other users of the site. So the first thing we need to do is escpe all user-submitted content so that the above code will be converted into HTML entities, like so:
</article>woops <script>alert("zomg haxz");</script>
To do this, add the following block of code to your recall.rb
file, for example under the DataMapper.auto_upgrade!
line:
helpers do include Rack::Utils alias_method :h, :escape_html end
This includes a set of methods provided by Rack. We now have access to a h()
method to escape HTML.
To escape HTML on the home page, open the views/home.erb
view file, and change the <%= note.content %>
line (around line 11) to:
<%=h note.content %>
Alternatively we could have written this as <%= h(note.content) %>
, but the style above is much more common in the Ruby community. Refresh the page and the submitted HTML should now be escaped, and not executed by the browser:
XSS on the Other Pages
Click the "edit" link for the note with the XSS code, and you may think it's safe - it's all sitting inside a textarea, and so not executing. But what if we added a new note with the following content:
</textarea> <script>alert("haha")</script>
Take a look at its edit page, and you can see that we've closed off the textarea and so the JavaScript alert is executed. So clearly we need to escape the note's content on every page where it's displayed.
Inside your views/edit.erb
view file, escape the content inside the textarea
by running it through the h
method (line 4):
<textarea name="content"><%=h @note.content %></textarea>
And do the same in your views/delete.erb
file on line 2:
<p>Are you sure you want to delete the following note: <em>"<%=h @note.content %>"</em>?</p>
There you have it - we're now safe from XSS. Just remember to escape all user-submitted data when creating other web apps in the future!
You may be wondering "what about SQL injections?" Well, DataMapper handles that for us just as long as we use DataMapper's methods for getting data from the database (ie. not executing raw SQL).
RSS Feed the Masses
An important part of any dynamic website is some form of RSS feed, and our Recall app is going to be no exception! Thankfully it's incredibly easy to create feeds thanks to the Builder gem. Install it with:
gem install builder
Depending on how you have RubyGems set up on your system, you may need to prefix gem install
with sudo
.
Now add a new route to your recall.rb
application file for a GET request to /rss.xml
:
get '/rss.xml' do @notes = Note.all :order => :id.desc builder :rss end
Make sure you add this route somewhere above the get '/:id'
route, otherwise a request for rss.xml
would be mistaken for a post ID!
In the route we're simply requesting all notes from the database, and loading a rss.builder
view file. Note how previously we were using the ERB engine to display a .erb
file, now we're using Builder to process a file. A Builder file is mostly a normal Ruby file with a special xml
object for creating XML tags.
Start your views/rss.builder
view file off with the following:
xml.instruct! :.xml, :version => "1.0" xml.rss :version => "2.0" do xml.channel do end end
Very Important Note: On the first second of the code block above, remove the period (.
) in the text :.xml
. WordPress is interfering with code snippets.
Builder will parse this out to be:
<?xml version="1.0" encoding="UTF-8"?> <rss version="2.0"> <channel> </channel> </rss>
So we've started off by creating the structure for a valid XML file. Now let's add tags for the feed title, description and a link back to the main site. Add the following inside the xml.channel do
block:
xml.title "Recall" xml.description "'cause you're too busy to remember" xml.link request.url
Notice how we're getting the current URL from the request
object. We could code this in manually, but the idea is that you could upload the app anywhere without having to change obscure pieces of code.
There is one problem though, the link is now set to (for example) http://localhost:9393/rss.xml
. Ideally we'd want the link to be to the home page, and not back to the feed. The request
object also has a path_info
method which is set to the current route string; so in our case, /rss.xml
.
Knowing this, we can now use Ruby's chomp
method to remove the path from the end of the URL. Change the xml.link request.url
line to:
xml.link request.url.chomp request.path_info
The link in our XML file is now set to http://localhost:9393
. We can now loop through each note and create a new XML item for it:
@notes.each do |note| xml.item do xml.title h note.content xml.link "#{request.url.chomp request.path_info}/#{note.id}" xml.guid "#{request.url.chomp request.path_info}/#{note.id}" xml.pubDate Time.parse(note.created_at.to_s).rfc822 xml.description h note.content end end
Note that on lines 3 and 7 we escape the note's content using h
, just as we did in the main views. It's a little odd to be displaying the same content for both the title
and the description
tags, but we're following Twitter's lead here, and there's no other data we can put there.
On line 6 we're converting the note's created_at
time to RFC822, the required format for times in RSS feeds.
Now try it out in a browser! Go to /rss.xml
and your notes should be displaying correctly.
DRY Don't Repeat Yourself
There is one minor problem with our implementation. In our RSS view we've got the site title and description. We've also got them in the views/layout.erb
file for the main part of the site. But now if we wanted to change the name or description of the site, there are two different places we need to update. A better solution would be to set the title and description in one place, then reference them from there.
Inside the recall.rb
application file, add the following two lines to the top of the file, directly after the require
statements, to define two constants:
SITE_TITLE = "Recall" SITE_DESCRIPTION = "'cause you're too busy to remember"
Now back inside views/rss.builder
change lines 4 and 5 to:
xml.title SITE_TITLE xml.description SITE_DESCRIPTION
And inside views/layout.erb
change the <title>
tag on line 5 to:
<title><%= "#{@title} | #{SITE_TITLE}" %></title>
And change the h1
and h2
title tags on lines 12 and 13 to:
<h1><a href="/"><%= SITE_TITLE %></a></h1> <h2><%= SITE_DESCRIPTION %></h2>
We should also include a link to the RSS feed in the head
of the page so that browsers can display an RSS button in address bar. Add the following directly before the </head>
tag:
<link href="/rss.xml" rel="alternate" type="application/rss+xml">
Flash Messages Errors and Successes
We need some way to inform the user when something went wrong - or right, such as a confirmation message when a new note is added, a note removed etc.
The most common and logical way to achieve this is through "flash messages" - a short message added into the user's browser session, which is displayed and cleared on the next page they view. And there just so happens to be a couple of RubyGems to help achieve this! Enter the following into the Terminal to install the Rack Flash and Sinatra Redirect with Flash gems:
gem install rack-flash sinatra-redirect-with-flash
Depending on how you have RubyGems set up on your system, you may need to prefix gem install
with sudo
.
Require the gems and activate their functionality by adding the following near the top of your recall.rb
application file:
require 'rack-flash' require 'sinatra/redirect_with_flash' enable :sessions use Rack::Flash, :sweep => true
Adding a new flash message is as simple as flash[:error] = "Something went wrong!"
. Let's display an error on the home page when no notes exist in the database.
Change your get '/'
route to:
get '/' do @notes = Note.all :order => :id.desc @title = 'All Notes' if @notes.empty? flash[:error] = 'No notes found. Add your first below.' end erb :home end
Very simple. If the @notes
instance variable is empty, create a new flash error. To display these flash messages on the page, add the following to your views/layout.erb
file, before the <%= yield %>
:
<% if flash[:notice] %> <p class="notice"><%= flash[:notice] %> <% end %> <% if flash[:error] %> <p class="error"><%= flash[:error] %> <% end %>
And add the following styles to your public/style.css
file to display notices in green and errors in red:
.notice { color: green; } .error { color: red; }
Now your home page should display the "no notes found" message when the database is empty:
Now let's display either an error or success message depending on whether a new note could be added to the database. Change your post '/'
route to:
post '/' do n = Note.new n.content = params[:content] n.created_at = Time.now n.updated_at = Time.now if n.save redirect '/', :notice => 'Note created successfully.' else redirect '/', :error => 'Failed to save note.' end end
The code is pretty logical. If the note could be saved, redirect to the home page, with a 'notice' flash message, otherwise redirect home with an error flash message. Here you can see the alternative syntax for setting a flash message and redirecting the page offered by the Sinatra-Redirect-With-Flash gem.
It would also be ideal to also display an error on the 'edit note' page if the requested note doesn't exist. Change the get '/:id'
route to:
get '/:id' do @note = Note.get params[:id] @title = "Edit note ##{params[:id]}" if @note erb :edit else redirect '/', :error => "Can't find that note." end end
And also on the PUT request page for when updating a note. Change put '/:id'
to:
put '/:id' do n = Note.get params[:id] unless n redirect '/', :error => "Can't find that note." end n.content = params[:content] n.complete = params[:complete] ? 1 : 0 n.updated_at = Time.now if n.save redirect '/', :notice => 'Note updated successfully.' else redirect '/', :error => 'Error updating note.' end end
Change the get '/:id/delete'
route to:
get '/:id/delete' do @note = Note.get params[:id] @title = "Confirm deletion of note ##{params[:id]}" if @note erb :edit else redirect '/', :error => "Can't find that note." end end
And its corresponding DELETE request, delete '/:id'
to:
delete '/:id' do n = Note.get params[:id] if n.destroy redirect '/', :notice => 'Note deleted successfully.' else redirect '/', :error => 'Error deleting note.' end end
Finally, change the get '/:id/complete'
route to the following:
get '/:id/complete' do n = Note.get params[:id] unless n redirect '/', :error => "Can't find that note." end n.complete = n.complete ? 0 : 1 # flip it n.updated_at = Time.now if n.save redirect '/', :notice => 'Note marked as complete.' else redirect '/', :error => 'Error marking note as complete.' end end
And There You Have It!
A working, secure and error-responsive web app written in a surprisingly small amount of code! Over this short mini-series we've learnt how to process various HTTP requests with a RESTful interface, handle form submissions, escape potentially dangerous content, connect with a database, work with user Sessions to display flash messages, generate a dynamic RSS feed and how to gracefully handle application errors.
If you wanted to take the app further, you may want to look into dealing with user authentication, such as with the Sinatra Authentication gem.
If you want to deploy the app on a web server, as Sinatra is built with Rake you can very easily host your Sinatra applications on Apache and Nginx servers by installing Passenger.
Alternatively, check out Heroku, a Git-powered hosting platform which makes deploying your Ruby web apps as simple as git push heroku
(free accounts are available!)
If you want to learn more about Sinatra, check out the very in-depth Readme, the Documentation pages and the free Sinatra Book.
Note: the source files for each part of this mini-series are available on GitHub, along with the finished app.
Comments