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.
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.
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
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.
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
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, nly => :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: