Forcing File Downloads in PHP

A cross-browser PHP script to force downloads through HTTP headers.

I’ve seen a number of methods to force file downloads using the PHP header() function which, essentially, sends a raw HTTP header to the browser. Depending on your browser, some files won’t be downloaded automatically. Instead, they will be handled by the browser itself or a corresponding plug-in. This is often an issue with PDF files, TXT files, CSV files, LOG files, multimedia files (MP3, WAV, MOV, MPEG, AVI, etc.), and, for many users, Microsoft Office files. As a developer, being able to force the download of any type of file is extremely useful.

To Force, Or Not To Force?

The argument of whether or not it is considered “good practice” to force users to download files rather than letting their browser handle them as exepected does not really have a place in this article. In some cases it is appropriate while in others it is probably not.

It has been a convenience the many times I have used it to push CSV files out to end users who wouldn’t know enough to copy and paste the data from the browser window into a file and then save it.

It is also handy for when you generate PDF files and want to push them to the user as a download rather than have it open up inside the browser window. Not only does this make the PDF easier for the user to download, it also provides consistency for the way downloads are handled regardless of what browser plug-ins the user has installed.

Again, in some cases it is appropriate to force a download, in others it is not.

The Force Download Script

After rigorous browser testing and code tweaking, here is the script I ended up with. All of the unnecessary stuff has been stripped out and it has been simplified as much as possible.

<?php
$file = "filename.ext"

// Quick check to verify that the file exists
if( !file_exists($file) ) die("File not found");

// Force the download
header("Content-Disposition: attachment; filename=\"" . basename($file) . "\"");
header("Content-Length: " . filesize($file));
header("Content-Type: application/octet-stream;");
readfile($file);
?>

Using the script

Although you can implement this script practically anywhere you want, it is easiest to copy the code into a file named something like force_download.php and pass an identifier via query string, POST form data, or session variable. Users who are directed to the script will be prompted to download the appropriate file regardless of their browser and plug-in settings.

The PHP readfile() function reads files verbatim, including PHP files. This opens up a major security hole if you are passing the actual filename using GET or POST form data (or anything else that the user can spoof). A curious user could easily gain access to sensitive database connection information or other system data by entering something like ?file=../includes/db_connection.php. You should always use some kind of identifier to prevent unauthorized access to sensitive information.

Example

Clicking this link will probably show you the text file in your browser window.

Clicking this link will prompt you to download the text file to your computer.

Compatibility

This script has been tested to work in IE6/7, Firefox 2, Opera 9, and Safari 3.

Browser Issues

Safari and Filenames

Surprisingly, all of the force-download scripts I researched online failed to work properly in Safari. The download occurred, but the resulting file was named after the script (i.e. force_download.php). If I renamed the downloaded file to its correct name, it would open just fine. This, however, was a terrible inconvenience. A bit of testing revealed the culprit. Most of the scripts had a line similar to this:

header("Content-Disposition: attachment");

Others went a step further and had something like this:

header("Content-Disposition: attachment; filename=" . basename($file));

But the correct way to specify the filename requires that you put double quotes around the filename attribute:

header("Content-Disposition: attachment; filename=\"" . basename($file) . "\"");

By simply adding quotes around the filename, Safari correctly names the resulting download.

Caching Problems

Many of the scripts I found included header calls to tell the browser not to use a cached version of the file. Caching wasn’t an issue with any of the browsers I tested, but if it becomes problematic, add the following lines to the script (above the call to readfile()).

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

The above lines are from the PHP Manual and are expected to work in most browsers.

Internet Explorer & HTTPS

Certain versions of Internet Explorer will generate the following error message when downloads are forced using this technique:

Internet Explorer cannot download file from server.

Internet Explorer was not able to open this Internet site. The requested site
is either unavailable or cannot be found. Please try again later.

To get around this, whenever you are forcing a download over HTTPS append the following two lines of code to the force download script (above the call to readfile()).

// IE fix (for HTTPS only)
header('Cache-Control: private');
header('Pragma: private');
?>

You can find more information about this bug on the MSDN Help & Support website.

Comments

I found the double quote thing was making Safari download with quotes around the file name. I found one set of quotes was all that is needed in Safari 3.0.4

#1 CpILL on Dec 28th, 2007

CpILL - I'm not sure if you are using the Mac version of Safari or the Windows version. Since I don't have a Mac I can only test it on the Windows version which, I have come to realize, works a bit differently across both platforms. I think further testing on this is needed.

Thanks for your input.

#2 Cory S.N. LaViska on Apr 21st, 2008

Need to create a script to force the browser of users to connect to *.ins.

#3 Monde on Jul 9th, 2008

Thanks for the IE & HTTPS fix. I was having problems with that until I found your page.

#4 Carl von Buelow on Aug 13th, 2008

the script doesnt work.

im making a halo game site and the file im trying to download is called floodgulch4.zip

-can you make the script for me please to suit that file, i can learn from there on.

and i dont care what file name you want it to be, eg:
-force.php
-download.php
etc.

#5 daniel on Aug 23rd, 2008

@daniel: "doesn't work" isn't very descriptive. This code is used in a number of applications successfully. My guess is there may be something erroneous in your code (i.e. using urls instead of system paths).

#6 Cory S.N. LaViska on Aug 24th, 2008

It does not work on IE 8 :(
Firefox 3 works good.
Please help me a little bit.

if ( $_POST["execute"] == "download" )
{
$docs_id = intval($_POST["doc"]) ;
$query = "SELECT D.file, D.filename, D.filesize, D.filetype FROM DOCS D INNER JOIN USERS_DOCS UD ON UD.docs_id = D.id WHERE UD.users_id=$logedid AND D.id=$docs_id" ;
$result = mysql_query($query) ;
if ($result)
{
if ( mysql_num_rows($result) == 1 )
{
$row = mysql_fetch_array($result) ;
header('Cache-Control: private');
header('Pragma: private');
header("Content-Type: " . $row["filetype"] ) ;
header("Content-Disposition: " . (!strpos($HTTP_USER_AGENT,"MSIE 5.5")?"attachment; ":"") . "filename=" . $row["filename"]) ;
header("Content-Transfer-Encoding: binary") ;
header("Content-Length: {" . $row["filesize"] . "}") ;
echo $row["file"];
exit;
}
}
else
{ echo "SQL-Fehler: " . mysql_error() . "<br>" . $query ; }
}

#7 Marky on Sep 8th, 2008

Ooops! Not ready and posted...
On my Site logged users can download only own files stored in database. I must use HTTPS! But download wors only via http in Internet Explorer. I think it is not a IE8 Bug. IE7 dont work too.

#8 Marka on Sep 8th, 2008

Is is possible to have force download script and download link to run in the one php file instead of two?
If so how?

Thanks, Wayne

#9 Wayne Nort on Sep 13th, 2008

Nice site, good tutorials thanks for that. I guess you didn't check out LinkLok URL then? That script is probably the best script I have ever found for this type of thing and works flawlessly in all browsers.

Best wishes,

Mark

#10 Mark Bowen on Sep 19th, 2008

Issues with can occur when using http -or- https when using sessions on the page processing the download.

To patch this issue, call 'session_cache_limiter('public');' PRIOR to session_start().

#11 Ben on Sep 22nd, 2008

That sentence should have read...... "Issue with IE can...."

#12 Ben on Sep 22nd, 2008

Ben, you've just saved my life... I mean time :P

#13 Gixx on Oct 7th, 2008

Thanks! works like a charm.

#14 Mario on Oct 8th, 2008

thanks man,

I had to use the pragma private you describe to make it work in ie, but it actually worked again.

Thank you for your time, that saved mine :)))

#15 yahel on Oct 23rd, 2008

Hi,

after some problems I've getting the File Tree to work on my Server. Now, I want to offer the files in the tree for downloading.

I won't put every single filename to the .php File like in the example above. Working with a variable (like this?: $file = $_GET['file'];
) is probably the best way for me, but I don't now how to implement this to the File Tree. It would be great if anybody can help me with that. Thanks in advance.

#16 Daniel on Oct 24th, 2008

Hi,
I tried your methond. But, there is a potential problem with this kind of download. As you are using readfile($file). The readfile function can only read file as large as apache can handle (this memory size is set in php.ini file). So, to send large files, you cannot use this method.

#17 Nishars on Oct 26th, 2008

I used the same sample code with your demo, but the trees root doesn't viewed. Only Blank DIV Area.

<script type="text/javascript">

$(document).ready( function() {

$('#fileTreeDemo_1').fileTree({ root: '<? echo $rootFileTree ?>', script: 'jqueryFileTree.php' }, function(file) {
alert(file);
});

$('#fileTreeDemo_2').fileTree({ root: '<? echo $rootFileTree ?>', script: 'jqueryFileTree.php', folderEvent: 'click', expandSpeed: 750, collapseSpeed: 750, multiFolder: false }, function(file) {
alert(file);
});

$('#fileTreeDemo_3').fileTree({ root: '/dds-includes/', script: 'jqueryFileTree.php', folderEvent: 'click', expandSpeed: 750, collapseSpeed: 750, expandEasing: 'easeOutBounce', collapseEasing: 'easeOutBounce', loadMessage: 'Un momento...' }, function(file) {
alert(file);
});

$('#fileTreeDemo_4').fileTree({ root: '<? echo $rootFileTree ?>', script: 'jqueryFileTree.php', folderEvent: 'dblclick', expandSpeed: 1, collapseSpeed: 1 }, function(file) {
alert(file);
});

});


<div class="example">
<h2>{ Root } <? echo $rootFileTree; ?></h2>

<div id="fileTreeDemo_3" class="demo"></div>
</div>

#18 hairulazami on Nov 20th, 2008

Add a comment

Name*

Email*

Never, ever sold or spammed :)

Homepage

Comment*

Sorry, plain text only :(