Singing with Sinatra – The Encore

Singing with Sinatra – The Encore

Tutorial Details
  • Topic: Sinatra / Ruby
  • Difficulty: Intermediate
This entry is part 3 of 3 in the Singing with Sinatra Session
« Previous

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.

Dan Harper is danharper on Themeforest
Note: Want to add some source code? Type <pre><code> before it and </code></pre> after it. Find out more
  • moosc

    Great tutorial

    Please, next do a login/password method (create the sign form, login…)

  • http://mrzepinski.pl/ mrzepinski

    Great tutorial, but there are few mistakes..

    @notes = Note.all rder => :id.desc

    should be: @notes = Note.all order => :id.desc

    and couple more like this.. [use other WP plugin for syntax]

    also when loop through each note and create a new XML item don’t know where
    should be this code in our application.. i’ve discovered that it should be in a rss.builder

    • http://www.danharper.me Dan Harper
      Author

      Damn it. WordPress keeps trying to insert emoticons on Ruby symbols.

      • http://mrzepinski.pl/ mrzepinski

        So if it possible.. use ‘Syntax Highlighter for WordPress’ plugin in your WP.

  • satlavida

    Great tutorial but can you make a tutorial showing how to allow login for many users…

    Thank you.

    • http://www.iammakingprogress.com Patrick

      I also would love to see a tutorial on logins with sinatra/rack! Thanks, these really helped a lot!

    • http://www.iammakingprogress.com Patrick

      I also would love to see a tutorial on logins with sinatra/rack! Thanks, these really helped a lot!

  • Ahto

    Nice article.

    One thing that came to mind is people should really use secure by default stuff. For example erubis has auto escaping (sanitizing) support, it means that ” can be escaped by default. Haml has this also, “set :haml, :escape_html => true”.

    • http://www.danharper.me Dan Harper
      Author

      Ah, thanks, I didn’t know erubis did it automatically. Personally, I find HAML incredibly ugly and using it a waste of time (but I’m sure a lot of the Ruby community will disagree with me!)

      • http://www.jeffrey-way.com Jeffrey Way

        Actually, I agree with you. I’ve given it a chance, but just don’t like it.

  • http://www.seanmccambridge.com Sean

    I could have a crossed wire in my tired brain and remembering another tutorial, but didn’t you say you were going to cover deployment to Heroku?

    • http://www.danharper.me Dan Harper
      Author

      That was the original plan, but to cover Heroku deployment would also require covering Rack, Git, SSH & SSH keys. It’ll work better as a “quick-tip” video.

  • Andy

    How come the notice flash does not work. I even moved the notice message from

    redirect ‘/’, :notice => “message” to

    flash[:notice] = “message”
    redirect

    it seems when redirecting it killed off the flash session. any insight?

    great tutorial though – thanks for sharing.

  • Julian Nicholls

    Like Andy, the flash messages don’t appear for me.

    The explicit flash message for no notes on the home page works fine, but none of the others appear.

    As Andy says, even making the flash messages explicit for the others doesn’t fix it. Is it a session problem, are we both missing a Gem?

    • Julian Nicholls

      It turns out that my flash problem comes from using shotgun, which makes Sinatra run very slowly too.

      Running ruby recall.rb instead makes the flash work perfectly, and the whole shebang runs at a sensible speed too.

      • http://www.danharper.me Dan Harper
        Author

        Yeah Shotgun can run slowly because it’s restarting the application on every refresh. I guess that also causes problems with flash redirects.

  • aromaron

    When changing the get ‘/:id/delete’ route line 5 instead of

    erb :edit

    it should be

    erb :delete

    XD

  • http://www.christianfleschhut.de/ Christian Fleschhut

    Great introduction to Sinatra, thanks!
    For those getting stuck with trying to deploy their Sinatra apps on Heroku these two posts provide some helpful information: http://yamilurbina.com/post/4854924459/deploying-a-sinatra-datamapper-sqlite-app-to-heroku + https://gist.github.com/68277
    Kudos to the original authors!

  • Scott Semple

    Has anyone had issues with the redirect after deploying to Heroku?

    I have a site on Heroku that was working fine before adding recall.db. Once added, the live site appears normal until trying to publish a note. After clicking Take Note, the page redirects to a blank page with the root URL. View Source is blank.

    The weird thing is that the site works perfectly on my local install. It’s only after pushing to Heroku that the redirects break. Any ideas?

    • Scott Semple

      Seems like switching to a Postgres database on Heroku (but continuing with a Sqlite database locally) fixed the blank redirects. File changes as follows:

      *In Gemfile*

      source :rubygems
      gem ‘sinatra’
      gem ‘dm-postgres-adapter’
      gem ‘data_mapper’
      gem ‘dm-timestamps’
      gem ‘rack-flash’
      gem ‘sinatra-redirect-with-flash’

      group :development do
      gem ‘dm-sqlite-adapter’
      gem ‘heroku’
      end

      *In main.rb*

      require ‘rubygems’
      require ‘sinatra’
      require ‘data_mapper’
      require ‘dm-timestamps’
      require ‘dm-postgres-adapter’
      require ‘rack-flash’
      require ‘sinatra/redirect_with_flash’

      enable :sessions
      use Rack::Flash, :sweep => true

      DataMapper.setup(:default, ENV['DATABASE_URL'] || “sqlite3://#{Dir.pwd}/supportLog.db”)

  • http://webnius.com/en/ David

    There is another gem that seems to work better for sinatra: sinatra-flash

    I was trying to use rack-flash and it was giving me errors for ‘<<' or something like that. I just switched to sinatra-flash and used flash.now[:error], and it worked fine.

    • chad

      Turns out rack-flash doesn’t work with rack 1.4 (which is current). So you either have to downgrade rack, or like you said, use sinatra-flash instead. I like your way better!

      gem install sinatra-flash

      add “require ‘sinatra/flash’” and remove “require ‘rack-flash’” up top
      remove the “use Rack::Flash, :sweep => true” line

      i think that is all I did to get it working.

      PS – Thanks for this Sinatra tutorial, Dan, it was very helpful.

      • http://twitter.com/PaulAdamDavis Paul Adam Davis

        Thanks for this, Chad. Saved me from giving Ruby/Sinatra a miss once again!

      • Chase Pursley

        Yep. And require ‘sinatra/redirect_with_flash’ is not necessary. Just change redirect notices like this: redirect ‘/’, flash[:error] = “Can’t find that note.”

    • baiki

      Sweet, thanks for that!

      Cheers

    • John Jorgensen

      For anyone who’s using sinatra-flash and having trouble getting the “No new messages” notification to display, try changing:

      flash[:notice]

      to

      flash.sweep[:notice]

  • http://craftybits.de Jennifer

    Thanks a lot! This was very very helpful to me.

  • Floyd

    Great tutorial – thank you kindly.

  • MalachaiTheGreat (@captcussa)

    For some reason, when I add this:

    Time.now.strftime(“%m/%d/%Y %I:%M%p”)

    My Notes fail… every time. Ideas?

  • rubyenthusiast

    great tutorial :)

  • jmaclabs

    Dan Harper

    In the RSS/builder section of the tutorial, after modifying the xml.link to use chomp, you have this loop instruction:

    “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:”"

    …followed by a new @notes code snippet.

    You never actually mention which file these changes go into. Maybe that should be obvious but the syntax itself doesn’t convey which file to introduce these changes.

    Can you update this line to make where the changes should be made explicit? Thanks in advance!

    • jmaclabs

      Oh, and, that code goes into the rss.builder file.

  • jmaclabs

    Note for Google Chrome users: Chrome does not display xml files natively. You must install an extension. Otherwise, the /rss.xml route will cause Chrome to attempt to download the rss.xml as a file.

  • jj

    I almost never comment articles, but this series on Sinatra really shines! Well done man! *****

  • http://www.heemels.com yggdrasil

    Very nice and clear tutorial. Well done.

  • http://www.facebook.com/talkeinan Tal Keinan

    Great tutorial… couple of pointers:
    in the layout flash update, you didn’t close the p.
    The flash notice for complete should state status switch rather than “complete” since you can switch it back and forth.