Video icon 64
Learning to code? Skill up faster with our practical video courses. Start your free trial today.
Advertisement

Singing with Sinatra - The Encore

by
Student iconAre you a student? Get a yearly Tuts+ subscription for $45 →
This post is part of a series called Singing with Sinatra.
Singing with Sinatra - The Recall App

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:

		&lt;/article&gt;woops &lt;script&gt;alert(&quot;zomg haxz&quot;);&lt;/script&gt;

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.

Advertisement