Mastering The Back Button With Javascript

Filed: Wed, Feb 28 2007 under Programming|| Tags: ajax history secure sessions

Ask how to control the back button on a forum and you'll be quickly lectured how interfering with the browser's history is evil and that you're a bad person for wanting to do it. You'll then be told it's impossible. But, guess what? Both groups are wrong!

Almost everyone who asks how to control the back button is trying to make pages accessed while a user was logged in, inaccessible once a user has logged out. A good example of this is a banking page. If you log in, transfer a few funds, then log out, no one should be able to hit the back button a few time and see what you were doing.

So wanting to control the back button isn't an evil request. There are many, MANY legitimate uses for wanting to do so.

window.onbeforeunload

Around Internet Explorer version 4.0, Microsoft decided there needed to be a way for secure banking and e-commerce sites to have some control over the session history. They did this with the javascript event onbeforeunload, a very funky little method that is called right before the browser navigates to a new URL.

There are two things that happen when you set an onbeforeunload event. One, you can give the user a chance to re-think the decision to leave the page by spawning a "yes/no" dialog box; and two, just having an onbeforeunload event will cause the browser to never cache the page: This means that the page will always be rendered exactly as if the user was hitting it for the very first time.

onbeforeunload is a method of window (the global/root javascript object). You set up the event pretty much like any other event.

window.onbeforeunload = function () {
   // stuff do do before the window is unloaded here.
}

Are you really, really sure you want to leave my glorious page?

What makes onbeforeunload quirky is that if you return ANYTHING at all in your event handler it will pop up a confirmation alert box asking the user if he or she is REALLY sure they want to leave the page. The exact text is:

Are you sure you want to navigate away from this page?

Click OK to continue, or Cancel to stay on the current page.

This dialog box will appear if you return ANYTHING in your function. return null, return false, return true, return "some text" will all bring up the confirmation dialog box.

You can insert your own explanatory text BETWEEN those two lines by including a string on the return statement. For instance:

window.onbeforeunload = function () {
   return "You have not saved your document yet.  If you continue, your work will not be saved."
}

…will change the alert box to read...

Are you sure you want to navigate away from this page?

You have not saved your document yet.  If you continue, your work will not be saved.

Click OK to continue, or Cancel to stay on the current page.

As you can see, the starting and ending lines stay the same, your text is inserted into the middle.

If the user clicks ok, the onunload event will trigger and the browser will navigate to the new page. If the user clicks cancel the user will be returned to the current page where he or she left off.

You can see an example of this by clicking this link. If you click OK this timestamp should change (). If you click cancel, the timestamp should remain the same.

If you don't want a confirmation dialog box, simply do not include a return statement in your function. Just close the curly braces ( } ) naturally without a return statement.

window.onbeforeunload = function () {
   // This fucntion does nothing.  It won't spawn a confirmation dialog
   // But it will ensure that the page is not cached by the browser.
}

Detecting When The User Has Clicked Cancel

One of the things you may want to do is to be notified when the user clicks cancel, aborting a page unload. Unfortunately there's no way to be immediately notified. The best you can do is to set a unique global variable in your onbeforeunload event and then look to see if that variable has been set in other functions. There is no way to get an immediate notification that the user has aborted a page unload.

The example code I used above to do an example of an onbeforeunload dialog is as follows:

var _isset=0;

function demo() {
   window.onbeforeunload = function () {
      if (_isset==0) {
         _isset=1;  // This will only be seen elsewhere if the user cancels.
         return "This is a demonstration, you won't leave the page whichever option you select.";
      }
   }
   _isset=0;
   window.location.reload();
   return false;
}

This code defines a global variabled named _isset, and then initializes it to zero. In our onbeforeunload event the variable is checked and if it's set to one, no unload dialog box will appear. The only way _isset could ever be one is if the user previously aborted a page unload.

But as you can see this method won't help you if you need to be immediately notified that that the user has finished dealing with the confirmation box. You can detect when it appears on the screen but there's no way to know when the user has finished interacting with it if the user clicked cancel (if the user clicked OK, then of course the unload event will have been tripped).

Truly Dynamic Pages

Just having an unbeforeunload event handler -- regardless of whether or not it actually does anything, regardless of whether or not you spawn a dialog box or not, even if the entire function declaration consists entirely of just { } -- just defining an event handler will prevent the page from being cached -- ever.

As a matter of fact, even if you allow page caching, the page will be not be cached. Having an onbeforeunload event means the page will be re-built every single time it is accessed. Javascripts will re-run, server-side scripts will be re-run, the page will be built as if the user was hitting it for the very first time, even if the user got to the page just by hitting the back or forward button.

Some security considerations.

Although onbeforeunload ensures the page will be fresh each time, you may still want to issue meta commands and server-side headers to control the cache -- just to be sure the browser doesn't store a copy on the sly. You can do this with server side headers (php examples here)...

header("Cache-Control: no-cache, must-revalidate");
header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
header("Pragma: no-cache");

And with browser meta tags (not as reliable but it never hurts to try).

<META HTTP-EQUIV="CACHE-CONTROL" CONTENT="NO-CACHE">
<META HTTP-EQUIV="EXPIRES" CONTENT="01 Jan 1970 00:00:00 GMT">
<META HTTP-EQUIV="PRAGMA" CONTENT="NO-CACHE">

Now in the what were they thinking department, you want to put cache control meta tags in a <head> section below the <body> tag. Microsoft has a good writeup on why this is so. Just mark this up as yet another browser idiosyncrasy to contend with.

Good cache-control will reduce the chance that someone can go through the physical cache on the user's machine and extract sensitive information.

Logged out? No page for you!

Since onbeforeunload event pages are not cached and are rebuilt each time the user access it, it means your server side script can determine if the user is logged in or not and serve the appropriate pages. You can even see if the session has timed out and redirect the user to the login page.

Shutdown Nicely

In addition to disabling page caching, onbeforeunload gives you a chance to nicely handle unload events. For instance you could submit the current form data, marked as incomplete and rebuild it if the user comes back to the page. Another good example of this is handling session logging. By incorporating a small synchronous AJAX call you can log when the user left the page, giving you an idea of how long each user is staying on the page. If you don't set up a confirmation dialog, this activity would be completely transparent to the user. Here's a simple template you can use to do just that.

window.onbeforeunload = function() {
   // This template uses no error checking, it's just concept code to be
   // expanded on.
	 
   // Create a new XMLHttpRequest object
   var AJAX=new XMLHttpRequest();  
	 
   // Handle ready state changes ( ignore them until readyState = 4 )
   AJAX.onreadystatechange= function() { if (AJAX.readyState!=4) return false; }
	 
   // we're passing false so this is a syncronous request.
   // The script will stall until the document has been loaded.
   // the open statement depends on a global variable titled _userID.
   AJAX.open("GET", 'http://someurl.com/endsession.php?id='+_userID, false);
   AJAX.send(null);
}

The choice to use a synchronous call is deliberate and important. A synchronous request will stall the browser until it gets a reply back from the server. If we were to initiate an asynchronous request the request would be made but the browser would continue the unload event and chances are the browser would never get the response back from the server. So in your unload events you need to keep AJAX synchronous.

Browser Check

onbeforeunload was introduced with Internet Explorer 4, so it has pretty good support. A lot of users surf without javascript however so if you decide to use onbeforeunload to help manage your session you'll need to make extensive use of <noscript></noscript> and dynamic HTML generation through javascript to nudge users into enabling javascript, at least for your pages. On the server side you'll also need to do some browser checking to ensure the browser supports onbeforeunload.

You can't change history

The history object, for good reason, has been put into a security lockdown. You can't use it to modify the history. You can however have some small and very imperfect control over the current url.

location.replace(url)

Using the command above you can replace the current page with the url of your choice, this replaces the current URL in the user's history as well. If the page is VERY sensitive you can use this when the user opts to log out. Firefox will let you use location.replace(URL) right before the return statement in an onbeforeunload event. That is, if you do a location.replace and then spawn a confirmation box, the current page will be altered.

window.onbeforeunload = function () {
   location.replace('http://www.google.com');
   return "This session is expired and the history altered.";
}

It's kind of clunky in that if the user opts to cancel he'll be on the replacement page instead of the original page, and the replacement page will actually be loaded as he ponders the confirmation dialog. If he selects OK however he'll navigate forward normally and if he hits the back button he'll be taken to the replacement URL.

It's important to note than in Internet Explorer 6 that you can not use a location method at all in any unload event, so the trick of modifying the history of the current page will work (kinda) only in Firefox. It's not something you can depend on.

If you MUST control History

If you absolutely must be able to clear out the history, then spawn a new window (without a location line) and when the user is done, close the window. The URLs will still be in the browser's history list ( cntrl-h ), but the session's backward and forward button list will have been destroyed.

Conclusion

Really, manipulation of the browser's history is unnecessary as long as you are dynamically generating your web pages and you use onbeforeunload events to ensure the page will always be fresh. If the page is being dynamically generated then even if someone goes through the history list and clicks on one of your session URLs, it won't really matter because the page will be rebuilt from scratch meaning you can check to see if the session has expired and if the user is logged in or not. If the check is failed, you simply re-direct the browser to a login page.

And there you go! Everything you need to know to ensure secure sessions, stay secure and private, no matter who sits down at the browser an hour later.