I just implemented a new way of uploading assets such as photos and PDF-files to Lokalebasen.dk. There is nothing revolutionary about it, but I hit a few snags on the way, and I thought I’d share my choices here.
Lokalebasen (a Danish website for rental and sale of business property) is (of course) based on Ruby on Rails and uses the notoriously brilliant jQuery as Javascript framework. When the customer asked for a progress bar while uploading assets, I knew there was basically two choices: Polling the server for the progress of the upload, or uploading through Flash. I choose the last option because I believe it is easier to implement, and it gives the added bonus of being able to start the upload in an “ajaxy” way without refreshing the page.
Several ready-made solutions exists, and chose one that was built as a jQuery plugin, was updated recently and was easy to use while being highly configurable: Uploadify. This article is not an Uploadify tutorial – you’ll have to work the out from the documentation and the examples. Rather, it’s about the last piece of the puzzle, how to make Rails play nicely with Uploadify.
Lokalebasen uses attachment_fu for handling assets. Since traditional file upload was already implemented, I had fallback functionality for users without Javascript or Flash – and so should you. All I had to add the controller handling the upload was a detection of the correct content type. Browsers will usually provide this, but Flash does not. And so the controller ended up looking something like this:
The mime-types gem is used to detect the content type – installation and usage is explained here.
I needed the functionality several places, so I wrote a partial I could reuse:
There is a lot going here, so let’s take it from the top. The content_for :head section is the code that will be placed inside the <head> part of the page – my layout takes care of that with a yield :head call. So first the Uploadify jQuery plugin javascript file is included, followed by some javascript that applies Uploadify to the file_uploader div seen later in the partial. This includes a lot options, some of which uses variables supplied to the partial. Here is an example of how I call the partial:
I won’t walk you through all the options I supply for Uploadify, but let’s take a look at the important ones: script is where Uploadify will post the uploaded file to. This should be the create action of your asset controller. scriptData is the most tricky one. The option specifies what parameters should be posted to the controller along with the file. 'format': 'json' ensures that the wants.json block is invoked in the controller, instead of the default wants.html. This helps the controller to separate Flash uploads from ordinary uploads. The two other parameters in scriptData will be explained later in the article, is they are the key to get the uploading past security and authentication measures taken. fileDataName extracts the name to use for the uploaded file (e.g. asset[:uploaded_data] for attachment_fu) directly from the fallback form. onComplete makes an ajax get request through jQuery to the same url we are currently on. I use this to render some javascript that updates the page in a wants.js in the controller.
There are several gotchas when you upload files through Flash. The most common one, which also apply to ajax, is the infamous ActionController::InvalidAuthenticityToken exception. You will get this exception on any default Rails installation with authenticity checking enabled. Rails expects any post to an action the include the authenticity_token parameter. It is used to verify the post actually came from the same application, and Rails automatically adds to all the forms and ajax requests it generates. In this case, we have to apply it manually, and this what happens with 'authenticity_token': encodeURIComponent('<%= form_authenticity_token if protect_against_forgery? %>'). The form_authenticity_token returns a valid token, but first we check if forgery is enabled. If it is disabled (which it usually is in tests), we will get an error if we invoke form_authenticity_token. encodeURIComponent makes sure that any characters in the token is encoded correctly. With this parameter, we should be able to make an authentic post through Flash – or maybe not …
Rails use data in the user session to authenticate requests, and requests directly from Flash does not include the session cookie that Rails use to find the session. Thus we still get the ActionController::InvalidAuthenticityToken exception, even with the authenticity_token parameter added. We have to make a slight hack into how Rails handles sessions to make this work. Place this code in a file in config/initializers to apply the hack, which tells Rails to try to read the session id from a parameter, if it can’t find in a cookie. This will only happen, however, if we add the line session :cookie_only => false, :only => :create to the asset controller. Also, we must supply the session in a parameter, which is what '<%= Rails.configuration.action_controller.session[:session_key]%>': '<%= u session.session_id %>' } do. The unique session key of the application is taken from the Rails configuration, and the session id is taken from session.
With these measures in place, Rails can now properly authenticate our Flash upload request as a legal, secure post. As an added bonus, actions protected behind session-based logins now also just works. And I would guess that most applications require their users to register and login before they can upload files.
Finally, here is a trick if you use http basic authentication e.g. for the administration tool like we do on Lokalebasen.dk. Place an is_admin? flag in the session, as shown in the code below. This will allow even Flash uploads to be authenticated, even if they don’t supply http basic authentication information:
Hello, I'm Casper Fabricius. I have developed for the web for 9 years, and have been enjoying Ruby on Rails for the past 4.
My experience covers communities, shopping solutions, multi-language sites, heavy back-end lifting and a wide selection of more traditional websites. Radiant is my preferred CMS, and I am an expert Radiant extension developer. I am passionate about test- and behavior-driven development, but at the same time I am pragmatic and believe in getting things done.
I am based in Copenhagen, Denmark, but I take assignments from across the globe. Feel free to study my resumé, featured projects and - of course - to hire me.
Rasmus Greve
March 26th, 2009 at 4:51 pm
Great post.
Good insight into possibilities concerning user experience.
Uploading and contributing is all the rave :-) – easy, cheap and beautiful implementation is the key for being able to apply uloading solutions over a wide array of applications..
And here is the reason I commented:
You gotta a love the language :-)
”
end
end
end
end
end
“
Casper Fabricius
March 27th, 2009 at 1:24 pm
Are you being ironic, Rasmus? :)
Btw, you can write } } } } } instead of all the ends if you prefer – Ruby allows for both notation forms. But you’d probably rather not have to write anything at all – but where would that leave the poor compiler? ;)
Rasmus Greve
March 31st, 2009 at 12:01 pm
Not ironic when commenting on UX possibilities and as you pointed out, I’m not in a position to discuss syntax or semantics of a high level programming language. :D
I just liked the:
end
end
end
end
end
keep up the good work.
Lars O. Overskeid
March 31st, 2009 at 3:42 pm
For en example of how to resolve the InvalidAuthenticityToken problem without patching, check out http://thewebfellas.com/blog/2008/12/22/flash-uploaders-rails-cookie-based-sessions-and-csrf-rack-middleware-to-the-rescue
The blogpost explains a middleware-approach with rails 2.3.
Casper Fabricius
April 2nd, 2009 at 1:10 pm
Thanks Lars – this looks really cool. Looks like we are going to have to update Lokalebasen.dk to Rails 2.3 soon ;)
Narendra
May 21st, 2009 at 2:55 pm
how to upload multiple files at a time.
Did
June 2nd, 2009 at 3:13 pm
Nice article, thanks for your work !
Unfortunately, I came through a weird bug with “special” characters (such as ‘+’) in the form authenticity token. On the server side, when I get the token, ‘+’ characters for instance are removed. I’m pretty sure it’s caused by the flash component since I use encodeURIComponent.
Have you met this bug before ?
Casper Fabricius
June 2nd, 2009 at 8:14 pm
I actually think I have – I just don’t remember what I did about it, to be honest. It works for me with the above solution, though.
Did
June 3rd, 2009 at 9:53 am
Thanks Casper, I took a look at the action script code and I saw a call to the “unescape” method on the scriptData variable (used by the uploadify query). Perhaps, they changed it for the current reelase. So, next step: remove this call and re-compile the flash component.
pn
June 8th, 2009 at 9:27 am
@Did
i have the same problem.
i use CGI.escape for convert authenticity_token
it’s work very well when you have a ‘+’ in your token
Brandon Mechtley
July 24th, 2009 at 9:59 pm
Thanks for this! Your method, combined with Rob Anderton’s post using a custom Rack middleware worked out great.
Unfortunately, I’m not exactly sure why, but I had a couple problems (Rails 2.3.2), the first being that I needed to replace Rails.configuration.action_controller.session[:session_key] with ActionController::Base.session_options[:key] (Rails complained that the first session variable was nil), and the second being that I have some discrepancies in my session IDs from session.session_id.
session.session_id gives me an ID something like “2d1c06c075ecd8d7e3d8e2adf2371f06,” which seems reasonable, since as far as I know session IDs are supposed to be 32-byte hashes, but the actual session IDs I get from working form posts are more like, “BAh7CToMdXNlcl9pZGkGOg9zZXNzaW9uX2lkIiUyZDFjMDZjMDc1ZWNkOGQ3ZTNkOGUyYWRmMjM3MWYwNjoQX2NzcmZfdG9rZW4iMWdYRGQ0U29SU1k2NGJuOEY1MWZ6S1U3ZW1ZL0hPR09lVU0wTVUvQWQwbzQ9IgpmbGFzaElDOidBY3Rpb25Db250cm9sbGVyOjpGbGFzaDo6Rmxhc2hIYXNoewAGOgpAdXNlZHsA–e96c3614fb16e404a2e1cbcbaf7a401d97883bbf”
I really don’t have any clue why this happens, as I’m pretty new to Rails, but I was able to fix the problem by using request.cookies[ActionController::Base.session_options[:key]] instead of session.session_id.
Hope this helps anyone having a similar problem.
Brandon Mechtley
July 24th, 2009 at 10:01 pm
Woops. Sorry for destroying the format of your pristine blog with that string :)
Brandon Mechtley
July 24th, 2009 at 10:22 pm
I probably should have thought about this before posting–that’s my entire session, and as I’m storing a great deal extra information in it, I guess it’s important to send the whole thing.
Casper Fabricius
July 25th, 2009 at 9:32 pm
Hi Brandon,
Thank you for your comments – and don’t worry about the design of the blog ;)
I don’t mention it in the article, but that code was made a Rails 2.1.2-based project. There has been made significant changes to how sessions work in Rails 2.3.2, so that’s probably what gave you trouble. I’m glad to hear you still got it working, though – I’ll soon need to use this technique on a 2.3.2 project myself, and I’ll post a followup if I run into the same trouble.