Building a Force Download Script with PHP and JavaScript

I’ve had the recent challenge of writing a script to forcing a file to be automatically downloaded from a webpage. If you’ve ever gone to sites such as Download.com, or Tucows.com and downloaded a file then you’ve seen this technique in action.

The idea is rather than you clicking a direct link on a file to be downloaded, a script is used as a sort of redirect to the file itself. This gives the developer a chance to determine who you are, if you have access to the file and anything else he wants to dynamically generate.

The Problem
I had to present a page, which a user would read over then click to load a form. They would then fill out the form and submit this to the server which would result in a file being downloaded and the form disappearing (or being removed from the page).

The Solution
I used a mix of PHP and JavaScript with the help of Prototype/Scriptaculous libraries. Here is the page being examined:
Divvies Stores

At the bottom of the page is a large button which upon clicking would load an overlay with the form. This is done with some AJAX magic where the form is actually a separate PHP file being echoed into a div using the AJAX.Request method.

$('store_localButton').observe('click', fadeArea);
$('store_localButton').observe('click', loadHTMLPage);

function fadeArea() {
var bodyOb=document.getElementsByTagName('body')[0];
var newOb='<div id="fadearea"></div>';
$(bodyOb).insert({
'top':newOb
});
}

function loadHTMLPage(url) {
if($('top')) {
location.href="#top";
}
var url=url;
new Ajax.Request(url, {
onSuccess: function(transport) {
displayPage(transport.responseText);
}
});
}

The first part of the script defines a few click event handlers which will handle user clicking the large button on the page. Both handlers work off the same click, and call two different function in order.

The first function, fadeArea, simply inserts an empty div with an id of “fadearea”. This is styled with CSS to fill the entire page with 50% black.

In the next function, loadHTMLPage, I first check if there is an element on the page with an id of ‘top’, and go there. The reason here is because the anchor for the button is null which doesn’t go anywhere. Often times pages will use href=”#” but this sends you back to the top of the page which is a very jarring experience and can usually confuse users. In cases where I am attaching events to anchors, I tend to make the href attribute null like: href=”javascript:void(null);”, which basically returns nothing. The second part of the function makes a request for the specified url, which in this case is a PHP file.

Once the file is successfully retrieved the displayPage is called.

function displayPage(htmlSnippet) {

$('fadearea').insert({
'top':htmlSnippet
})
}

Here, knowing its being inserted into the fadearea div, we use Prototype to easily insert the contents of the requested file.

In my form I used an inline onsubmit event to kickoff the whole submitting process (Probably could have/should have used the DOM3 method).

Heres where it gets sticky, the form now gets submitted using Prototype’s Form .Request method, which is an http request with your serialized data. The Form.Request method sends its data to the file in your forms action attribute, which if all is ok, will simply echo the string literal “download.php”.

The HTML form tag

The submitAjaxForm function

This function sends the data to my PHP script, and the PHP script returns a reply back to the script. In the reply is my text “download.php”. You’ll see in the onComplete option, a function is run closing the fade area and using the javascript location object to redirect to the page provided in my response text (download.php).

function submitAjaxForm(formID) {

var inputs=document.getElementsByTagName('input');
vartextA=document.getElementsByTagName('textarea');
var numInputs =inputs.length;

var myForm=$A($(formID).getInputs());
$(formID).request({
onComplete: function(trans){
closeFadeArea();
window.location = trans.responseText;
}
});
return false;
}

The download.php script is called and the download begins. Because you aren’t really directed, the page you are currently on stays loaded.

$file="/home/divvies/www/www/downloads/stores_pdf_Divvies.pdf";
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename='.basename($file));
header('Content-Transfer-Encoding: binary');
header('Expires: 0');
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header('Pragma: public');
header("Content-Length: ".filesize($file));
ob_clean();
flush();
readfile($file);

Damn You IE
I had one snafu in this project. It doesn’t work in IE7. Why? When a file is requested that tells the browser to download a files (instead of displaying an HTML page), IE stops it from happening and ask you to make sure you want this to happen. You click on the the tiny little strip at the top of the window and choose download this file….

But choosing this doesn’t download the file because IE7 simply refreshes the page and grants the page access to the file. In the case above, because I’m using some Javascript with HTTP requests, the page is fully loaded forcing the user to start all over.

So the work around I had to do was to actually redirect the user to a new page (thankyou.php), in which that page forces the download. Now I have no work around for suppressing IE’s weird popup blocker so you still have to click it to approve the download, but now it works. Subsequently, it gave us a place to offer a direct link to the file.

Hope you got something out of this, I don’t write about my scripts too often so it may sound a little unclear at time. I’ll be write more so I will work on that.