Rails + XMLHttpRequest#sendAsBinary() / missing file contents

48 views
Skip to first unread message

FredO

unread,
Nov 15, 2009, 1:40:12 PM11/15/09
to pdxruby

Firefox, since version 3, has supported 'sendAsBinary()', a 'new file
transfer' method on the XMLHttpRequest object. And in the latest
Firefox beta also supports drag-and-drop.

There is some lovely sample code that combines these two features
posted on the CSS.Ninja blog at:

http://www.thecssninja.com/javascript/drag-and-drop-upload

But the author's code is designed to work with a minimal PHP demo app.

I thought that I would try to get a RoR version working.

The crux of the JavaScript code are these 5 lines

var upload_file_url = // restful route to RoR application
var file = // some code to acquire the file
var xhr = new XMLHttpRequest()
xhr.open("POST", upload_file_url);
xhr.overrideMimeType('text/plain; charset=x-user-defined-
binary');
xhr.sendAsBinary(file.getAsBinary());

Well this is all very fine, except that we normally have a line in
application_controller.rb

protect_from_forgery

Which means the server is now expecting an authenticity_token in all
of the requests that it handles (unless specified otherwise)

So, when you execute ' xhr.sendAsBinary(file.getAsBinary())' the
server rejects the request because there is no 'authenticity_token'.

Well, would it work to incorporate the authenticity_token, and any
other useful file characteristics in the url? I defined a route in
routes.rb like so:

map.file_upload '/
file_upload/:authenticity_token/:user_id/:md5/:name/:suffix/
create', :controller=>'uploaded_files', :action=>'create', :method=>'post'

And in the JavaScript I extract the authenticity_token from the page
and incorporated it into the 'upload_file_url' along with the other
interesting parameters.

And it works... sort of.

If I pretty-print out the params variable this I can see all of the
attributes I incorporated into the URL

{"name"=>"les-amis2",
"method"=>"post",
"authenticity_token"=>
"8a576fa97709c92e102eb5da515481009e2bf5044caa2e014ef6004dde58f5c4",
"action"=>"create",
"user_id"=>"2",
"suffix"=>"jpg",
"md5"=>"1a8591455122cdb9ff1015d694c9c067",
"controller"=>"uploaded_files"}

What is missing however is any reference to the file contents!!!!!!

Does anyone know where it is? I am using 'mongrel' on the server
side, by the way.

Fred Obermann

Darren Hinderer

unread,
Nov 15, 2009, 7:18:09 PM11/15/09
to pdx...@googlegroups.com
I think as of Rails 2.3.3 or 2.3.4 the authenticity token isn't
necessary for ajax requests. If you look at the github comments for
that commit there are some comments as to why securing ajax didn't
make sense and there is also a snippet you can use to backport the
change with an initializer.

Hope that helps.

--
Darren

FredO

unread,
Nov 16, 2009, 8:41:48 AM11/16/09
to pdxruby
I have tested the code with 2.3.4 and I still need to include the
authenticity_token as per the comments in
'request_forgery_protection.rb':

# If you are generating an HTML form manually (without the
# use of Rails' <tt>form_for</tt>, <tt>form_tag</tt> or other
helpers), you have to include a hidden field named like that and
# set its value to what is returned by
<tt>form_authenticity_token</tt>. Same applies to manually constructed
Ajax requests.

On Nov 15, 7:18 pm, Darren Hinderer <darren.hinde...@gmail.com> wrote:
> I think as of Rails 2.3.3 or 2.3.4 the authenticity token isn't
> necessary for ajax requests. If you look at the github comments for
> that commit there are some comments as to why securing ajax didn't
> make sense and there is also a snippet you can use to backport the
> change with an initializer.
>
> Hope that helps.
>
> --
> Darren
>

Alain Bloch

unread,
Nov 16, 2009, 4:12:21 PM11/16/09
to pdx...@googlegroups.com
Apart from creating a way to insert in the auth token into the AJAX request, you can always just skip the auth token for the called method:

skip_before_filter :verify_authenticity_token, :only => [:ajax_method]

Alain Bloch

unread,
Nov 16, 2009, 6:50:49 PM11/16/09
to pdx...@googlegroups.com
By the way, Fred.. are you asking where the file content is in the request parameters?

If so, then it isn't that the extension and filename? The method it requests would use the filename and extension to construct the proper path to the file stored in your app server(s), correct?

FredO

unread,
Nov 17, 2009, 9:38:28 AM11/17/09
to pdxruby
Thanks for the advice.

I tried inserting
"skip_before_filter :verify_authenticity_token, :only =>
[:ajax_method]" in the "application_controller.rb" file, both before
and after "protect_from_forgery" and it did not seem to work.

I get the following error message:

ActionController::InvalidAuthenticityToken
(ActionController::InvalidAuthenticityToken):

But that's not so troubling since I can put the authenticity token in
the URL. Although please note I had to change the definition of
form_authenticity_token() (in /usr/local/lib/ruby/gems/1.8/gems/
actionpack-2.3.4/lib/action_controller/request_forgery_protection.rb)
to use hexadecimal encoding as opposed to base64.

What bothers me is that when I use this 'new' Mozilla JavaScript
function

xhr.sendAsBinary(file.getAsBinary())

nothing shows up in the params hash in my controller function.

Jordan Curzon

unread,
Nov 18, 2009, 12:03:56 PM11/18/09
to pdx...@googlegroups.com
I think the key is that the mime time is not "multipart/form-data"
which I think is required for ruby to interpret the POST body as
parameters instead of a raw IO stream. I think you'll find your file
data in request.body which is an IO object.

On Tue, Nov 17, 2009 at 6:38 AM, FredO <sin...@gmail.com> wrote:
>
> Thanks for the advice.

FredO

unread,
Nov 22, 2009, 10:37:05 PM11/22/09
to pdxruby
Right you are!

Getting the file contents turns out to be really quit simple.
On the server side:

contents = request.body.read()

Is all you need.


FredO

unread,
Nov 22, 2009, 11:49:47 PM11/22/09
to pdxruby
After Jordan responded to my email. I smacked myself in the head and
said "Of course dummy! What were you thinking. Or was it 'not
thinking'?

Using this technology, JavaScript and jQuery, I was able to create a
photo-layout editor that uploads the files asynchronously in the back-
ground as you mess-around with photo layout and captioning in your
browser.

Given that we usually pass an ‘authenticity_token’ back and forth in
a Rails session, the first problem I ran in to is that when I created
an XMLHttpRequest object and executed a ’sendAsBinary(contents)’
against it, no ‘authenticity_token’ was sent to the server. And the
server rejected the request.

My solution was to make use of Rails’ restful routing feature to solve
this problem.

First, I changed the server code to generate a hexadecimal
‘authenticity_token’ in lieu of the normal ‘base64′ version. Then I
included the ‘authenticity_token’ as a hidden field on the form

Then on the server, I set up a route that incorporated the
‘authenticity_token’ (along with other useful variables) in the path
specification in the ‘routes.rb’ file.

And in the JavaScript code, in ‘handleDrop()’, I used a path that
incorporates the ‘authenticity_token’ and other interesting variables.

On the server side, the variables encoded into the path are
automatically extracted into the ‘params’ hash. And to get the file
contents you just have to write something like:

contents = request.body.read()

So simple!

There was, however another wrinkle I had to deal with.

What I found (with firefox 3.6 beta at least) is that sometimes the
width and height attributes are set on the image after invoking
something like:

img.src = dropped_file.getAsDataURL()

And sometimes the are NOT. At different times the same file will
result in an image with width and height set to values from the jpeg
file, and sometimes the dimensions will both be zero.

So after the user drags and drops files onto the browser…. in order to
reliably know their dimensions, you have to upload them to the server
(where you can gather the size information with image-magick or image-
science) and then use another function (like jQuery’s ‘post()’ ) to
send a request for the width and height.

Thanks again Jordan.

Fred Obermann
Reply all
Reply to author
Forward
0 new messages