Revisited: Facebook, Safari and External iFrames that need cookies

Just thought I’d pass on the better solution I’ve come up with for dealing with the Facebook/iFrame/Cookie issue. The major problem with the last solution is, obviously, that it’s annoying to the user – they have to click something just because of some stupid technical issue! There’s also a more minor problem which is we are doing an AJAX request to test for cookies when it can be done for free on the browser. Anyway, I came up with a solution that fixes both of these issues, using javascript. Its a pretty good solution: no peformance drag on the server, no difference in user experience, and no clutter in your application code (its all in the view, baby (well, except for the tiny nessesary bit that carries over the session info into the new cookie)). Basicly, the script tries to store a cookie using the following function:

function Get_Cookie( name ) {

		var start = document.cookie.indexOf( name + "=" );
		var len = start + name.length + 1;
		if ( ( !start ) && ( name != document.cookie.substring( 0, name.length ) ) )
		{
			return null;
		}
		if ( start == -1 ) return null;
		var end = document.cookie.indexOf( ";", len );
		if ( end == -1 ) end = document.cookie.length;
		return unescape( document.cookie.substring( len, end ) );
	}

Now the fun part. If the above function fails, we want to attatch the session data to the links, but in our case we want to do it as the links load (rather than waiting until the page loads and risking the user clicking prematurely or forcing them to wait). This was achieved using the extremely useful onDOMLoad script, by Aaron Barker (make sure you javascript_include_tag ‘ondomload’ !)
:

var sess_id = "#{session.session_id}";
	 if(!Get_Cookie('_session_id')) {
		zelph_onDOMload('a','AddSessionGrabberToLink(theTarget);');
	 }

Note that we are storing the session id in a variable: we’ll be using this later to pass the session along in the links. Now to be safe, we don’t want to just stick the session the url – that could be secure data in there. Far better to attatch an event handler to the onclick of the link that creates a form with an invisible text field containing the sess_id and then submits it. That is done in the following function, which is the meat of the javascript:

function AddSessionGrabberToLink(link){
		//grab the link and stick it as a param.  make the new link submit a form that posts the session_id
		var url = link.href;
		var url_with_redirect = "#{url_for :action => :grab_session_and_redirect}" + "?redirect_to=" + url;

		link.onclick = function() {
				var f = document.createElement('form'); f.style.display = 'none'; 				this.parentNode.appendChild(f); f.method = 'POST'; f.action = url_with_redirect; var m = document.createElement('input'); m.setAttribute('type', 'hidden'); m.setAttribute('name', '_session_id'); m.setAttribute('value', sess_id); f.appendChild(m); f.submit(); return false;
			};      }

Finally, we’ve got to actually deal with that session id. We’ll already have a new cookie and session id by the time the user has clicked the link, so rather than try to use the old session, we just find the old session, copy over what we need, and then redirect:

def grab_session_and_redirect
	    oldsession = CGI::Session::ActiveRecordStore::Session.find_by_session_id(params[:_session_id])
	    session[:facebook_session] = oldsession.data[:facebook_session]
	    session[:user_id] = oldsession.data[:user_id]
	    session[:username] = oldsession.data[:username]
	    session[:ip] = oldsession.data[:ip]
	    redirect_to params[:redirect_to] and return
	  end

The above is the only code that makes it into your controllers. Not too bad, and the user experience is much better!

11 Responses to “Revisited: Facebook, Safari and External iFrames that need cookies”

  1. Bailey Cross Says:

    i’m trying to embed a google map into a facebook application. majority of google maps use call to onload in body tag. Facebook does not support the body tag. Any ideas how to get it to work without using the body tag.

    thanks,
    Bailey

  2. digidigo Says:

    Baily — I made the decision to use an IFrame for google map integration. Works well and you can’t even tell for my application.

  3. Facebook, Safari and External iFrames that need cookies « Will Henderson Says:

    [...] I came up with a better solution that requires no user [...]

  4. jeremy Says:

    Thanks for the initial insight on how to fix the problem. I think we’ve come up with a slightly more elegant solution:

    So we check for our session cookie from the layout we use for Facebook. We defined a constant in ApplicationController. If the cookie doesn’t exist then do your form posting trick automatically. (note we don’t check for links and alter them on the page, and instead do this immediately when needed… should be only once per session. when we post to the controller it has sessions off since we already passed in a session_id and set the cookie there and then redirect to the initial page. Voila Safari iframes, cookie problem fixed.

    Again thanks for figuring out the problem!

    Inside your layout do:

    if (document.cookie.indexOf(“”) == -1) safariFixSession(“”, “‘fb/cookie’) %>”);

    Here’s the js function:
    function safariFixSession(sess_id, controller) {
    alert(‘no cookie found’);
    var url = location.href;
    var url_with_redirect = controller + “?redirect_to=” + url;

    $$(‘body’)[0].insert(”);
    var f = $(’safariFixSession’)
    f.method = ‘POST’;
    f.action = url_with_redirect;
    var m = document.createElement(‘input’);
    m.setAttribute(‘type’, ‘hidden’);
    m.setAttribute(‘name’, ‘_session_id’);
    m.setAttribute(‘value’, sess_id);
    f.appendChild(m);
    f.submit();
    }

    And here’s the controller:
    class Fb::CookieController < ApplicationController
    session :o ff
    def index
    cookies[COOKIE_NAME] = params[:_session_id]
    redirect_to params[:redirect_to] and return
    end
    end

  5. jeremy Says:

    Shoot sorry, that first JS line needs to be:
    if (document.cookie.indexOf(”COOKIE_NAME”) == -1) safariFixSession(”SESSION_ID”, “‘fb/cookie’) %>”);

    and remove the alert(”) line ;-)

  6. Tyson Says:

    I have tried this solution but I could not make it work. Do you have some sample code to share?

  7. ajay Says:

    This post is almost a year old. Is there now a standard way of dealing with the facebook-iframe-safari-cookie issue? Esp. for Rails?

  8. Sai Emrys Says:

    I’ve written this up in more readable form on my blog, here:

    http://saizai.livejournal.com/897522.html

    Thanks, Jeremy & Will!

  9. On the Pain of Developing for Facebook : Light Year Blog Says:

    [...] your Rails sessions to carry over from page #1 of your iframe application to page #2 and beyond. Will Henderson has a solution when your Rails application uses the ActiveRecord session store, but new Rails [...]

  10. Steve Madsen Says:

    That’s my trackback just above, but the summary doesn’t say enough.

    Will’s solution here is good if you’re using ActiveRecord-backed sessions, but newly generated Rails apps prefer using the cookie store. His trick won’t work for that, as there is no database row, so no row ID, so nothing to pass between the landing page and the rest of the iframe app.

    I describe a way to preserve Facebook’s fb_sig parameters through links to the next page, where now Safari will accept cookies since the user has interacted with the iframe.

    There is some other stuff in my post that may be of interest to Rails Facebook developers, too.

    http://lightyearsoftware.com/blog/2009/11/on-the-pain-of-developing-for-facebook/

  11. Mafia Wars Money Hack Says:

    Forget slaving away completing jobs on mafia wars for cash. With my mafia wars money cheats, all that becomes childs play. Literally, in 5 minutes, you can have as much cash as you want.

Leave a Reply