How To Force File Download With PHP
Say you want a PHP script that will make the browser to download a file instead of opening it. This is useful for common filetypes that would normally be displayed in a browser, like .html, images, PDFs and .doc files.
You can find a lot of scripts that do this on the web but one thing most of them don’t handle is download resuming and multi-thread downloading. If you don’t need that feature, feel free to use a simpler script. Personally I’ve found that a function that handles download resuming works more reliably across various browsers (what actually happened : a simpler script didn’t work with Opera + my weird Internet connection, so I found another script and decided to make broad generalizations about download resuming :p).
Note – most of the code below isn’t mine. I found it somewhere on the web and adjusted it for my needs; unfortunately I’ve lost the URL of the original page. If you know where it came from, let me know and I’ll add a link to this post.
function output_file($file, $name, $mime_type=”)
{
/*
This function takes a path to a file to output ($file),
the filename that the browser will see ($name) and
the MIME type of the file ($mime_type, optional).
If you want to do something on download abort/finish,
register_shutdown_function(‘function_name’);
*/
if(!is_readable($file)) die(‘File not found or inaccessible!’);
$size = filesize($file);
$name = rawurldecode($name);
/* Figure out the MIME type (if not specified) */
$known_mime_types=array(
“pdf” => “application/pdf”,
“txt” => “text/plain”,
“html” => “text/html”,
“htm” => “text/html”,
“exe” => “application/octet-stream”,
“zip” => “application/zip”,
“doc” => “application/msword”,
“xls” => “application/vnd.ms-excel”,
“ppt” => “application/vnd.ms-powerpoint”,
“gif” => “image/gif”,
“png” => “image/png”,
“jpeg”=> “image/jpg”,
“jpg” => “image/jpg”,
“php” => “text/plain”
);
if($mime_type==”){
$file_extension = strtolower(substr(strrchr($file,”.”),1));
if(array_key_exists($file_extension, $known_mime_types)){
$mime_type=$known_mime_types[$file_extension];
} else {
$mime_type=”application/force-download”;
};
};
@ob_end_clean(); //turn off output buffering to decrease cpu usage
// required for IE, otherwise Content-Disposition may be ignored
if(ini_get(‘zlib.output_compression’))
ini_set(‘zlib.output_compression’, ‘Off’);
header(‘Content-Type: ‘ . $mime_type);
header(‘Content-Disposition: attachment; filename=”‘.$name.'”‘);
header(“Content-Transfer-Encoding: binary”);
header(‘Accept-Ranges: bytes’);
/* The three lines below basically make the
download non-cacheable */
header(“Cache-control: private”);
header(‘Pragma: private’);
header(“Expires: Mon, 26 Jul 1997 05:00:00 GMT”);
// multipart-download and download resuming support
if(isset($_SERVER[‘HTTP_RANGE’]))
{
list($a, $range) = explode(“=”,$_SERVER[‘HTTP_RANGE’],2);
list($range) = explode(“,”,$range,2);
list($range, $range_end) = explode(“-“, $range);
$range=intval($range);
if(!$range_end) {
$range_end=$size-1;
} else {
$range_end=intval($range_end);
}
$new_length = $range_end-$range+1;
header(“HTTP/1.1 206 Partial Content”);
header(“Content-Length: $new_length”);
header(“Content-Range: bytes $range-$range_end/$size”);
} else {
$new_length=$size;
header(“Content-Length: “.$size);
}
/* output the file itself */
$chunksize = 1*(1024*1024); //you may want to change this
$bytes_send = 0;
if ($file = fopen($file, ‘r’))
{
if(isset($_SERVER[‘HTTP_RANGE’]))
fseek($file, $range);
while(!feof($file) &&
(!connection_aborted()) &&
($bytes_send<$new_length)
)
{
$buffer = fread($file, $chunksize);
print($buffer); //echo($buffer); // is also possible
flush();
$bytes_send += strlen($buffer);
}
fclose($file);
} else die('Error - can not open file.');
die();
}
/*********************************************
Example of use
**********************************************/
/*
Make sure script execution doesn't time out.
Set maximum execution time in seconds (0 means no limit).
*/
set_time_limit(0);
$file_path='that_one_file.txt';
output_file($file_path, 'some file.txt', 'text/plain');
[/sourcecode]
Related info
Determining MIME type of a file automatically
Common MIME types
HTTP, caching and other stuff
hehe, no time to learn php, only to make it work asap 🙂
i’m going to ask the folks in the forums, thanks for your quick reply 🙂
Hi
Thanks for the script!
It’s working fine as long as I’m using it for small files, but as soon as I’m trying to use it with larger files the file size in the header turns to zero and so the downloaded file has a size of zero.Anyhow when I link the file normaly and right click the link I can download the file. Do you have any suggestion why this happens?
How “large” are we talking about? If it’s over 4 GB, it might cause an integer overflow.
Thanks for the fast reply.
No we are talking about files bigger than 10MB.
Also when I’m using the line “@ob_end_clean();” FF is telling me that he can’t open the file on the server. If I comment it out its workin as explained.
Here two headers probably they help
Working one:
HTTP/1.1 200 OK
Date: Wed, 10 Mar 2010 10:19:33 GMT
Server: Apache/2.2.14 (Unix) mod_ssl/2.2.14 OpenSSL/0.9.8e-fips-rhel5 mod_auth_passthrough/2.1 mod_bwlimited/1.4 FrontPage/5.0.2.2635
X-Powered-By: PHP/5.2.9
Content-Disposition: attachment; filename=”v_2010-03-09_001.wma”
Content-Transfer-Encoding: binary
Accept-Ranges: bytes
Cache-Control: private
Pragma: private
Expires: Mon, 26 Jul 1997 05:00:00 GMT
Content-Encoding: gzip
Vary: Accept-Encoding
Content-Length: 3326385
Connection: close
Content-Type: audio/x-ms-wma
Not working:
HTTP/1.1 200 OK
Date: Wed, 10 Mar 2010 10:22:03 GMT
Server: Apache/2.2.14 (Unix) mod_ssl/2.2.14 OpenSSL/0.9.8e-fips-rhel5 mod_auth_passthrough/2.1 mod_bwlimited/1.4 FrontPage/5.0.2.2635
X-Powered-By: PHP/5.2.9
Content-Disposition: attachment; filename=”v_2010-03-09_002.wma”
Content-Transfer-Encoding: binary
Accept-Ranges: bytes
Cache-Control: private
Pragma: private
Expires: Mon, 26 Jul 1997 05:00:00 GMT
Content-Length: 0
Connection: close
Content-Type: audio/x-ms-wma
As you might have gathered from my lack of timely response (sorry about that), I don’t have any bright ideas about how to fix this.
For debugging, you could try modifying the script so that it only outputs the file size and other variables as normal HTML and doesn’t actually send the file. This would help you find the point at which it fails.
I would recommend Microsoft’s Fiddler 2 tool, which can be installed as a plug-in in Firefox I believe (it wasn’t always a MS product). It allows you to view detailed HTTP headers and what not. This tool came in handy when I was debugging my download script.
hey that script doesn’t work, I tried this to send a rtf file to the client and on Internet Explorer I can’t download the file, on the other browsers like firefox and google chrome works fine
actually the code is very large and sometimes redundant… here it is a small version that works with all files (I assume).
$name = $_GET[‘file’];
header(“Content-disposition: attachment; filename=$name”);
header(“Content-type: application/octet-stream”);
readfile($name);
or if just need an advanced version:
if (!isset($_GET[‘file’]) || empty($_GET[‘file’])) {
exit();
}
$root = “img/”;
$file = basename($_GET[‘file’]);
$path = $root.$file;
$type = ”;
if (is_file($path)) {
$size = filesize($path);
if (function_exists(‘mime_content_type’)) {
$type = mime_content_type($path);
} else if (function_exists(‘finfo_file’)) {
$info = finfo_open(FILEINFO_MIME);
$type = finfo_file($info, $path);
finfo_close($info);
}
if ($type == ”) {
$type = “application/force-download”;
}
// Set Headers
header(“Content-Type: $type”);
header(“Content-Disposition: attachment; filename=$file”);
header(“Content-Transfer-Encoding: binary”);
header(“Content-Length: ” . $size);
// Download File
readfile($path);
} else {
die(“File not exist !!”);
}
cheers and share
[…] can use a force download script. I use this one: How To Force File Download With PHP | W-Shadow.com __________________ This space for rent. After Hours […]
[…] Original Content from http://w-shadow.com […]
This script sucks. There are three vulnerabilities in this script: Local File Inclusion, Remote File Inclusion and Directory Traversal. These vulnerabilities allow an attacker to e.g read /etc/passwd file or steal a cookie. First of all, you should filter user supplied data.
I wrote a blog post about this: http://pklog.jogger.pl/2010/07/20/pewien-hotel-i-local-file-inclusion-studium-przypadku/ . It’s not English – Polish. You can use Google Translate or only look at code.
Anyway, just read about LFI, RFI, DT and white lists.
Finally, I’m sorry for my English – it’s not my native language.
Thank you for posting this article, it really helped when I needed to force download a connection program for our remote pc support business.
What a great script! Very easy to follow. I hate when script writers cannot realize that they need to make their code easy to follow if they want other people to comprehend it. I used your script here:
http://www.pixnorth.com/phplx/mycode.php
You can use the php tools to add to the “my code” section. Your script then makes it possible to export to a php script whatever is in that section. Thanks for the script!
Thanks for the info. Can you also provide an example of use for this method?
amm i don’t know why but when the window to save the file opens the file is downloaded like this “-myFile.pdf-“, i mean the extension is changed for .pdf- so it downloads corrupted or imposible to open any idea why this happens???
@pk1001100011
You don’t need to put full filters on this function, the filters should be added somewhere on your code to prevent bloating your functions.
Provided code is only an example on how to do things.
I read your website and this is different from the script that you reviewed.
safe_mode can only be turned on by system admins and not ordinary hosted users unless you have a VPS account or a dedicated account. This function is turned on 97% of the time on shared hosting.
open_basedir is enabled on secured servers and there is no way to activate it on shared hosting accounts. This will cause trouble on account creations due to the restrictions that needs to be applied per account/domain.
open_basedir rule on applies to 1 and 1 domain or subdomain causing errors on the next subdomains that will be created unless you add a new open_basedir for that subdomain.
how to use this script ?
foredownload.php?file=http://–.zip <- like that?
Thanks a lot !!! Just more things for force download on cross -domain
delte this line “if(!is_readable($file)) die(‘File not found or inaccessible!’);” and for the file size use curl method like above
// Create a curl connection
$chGetSize = curl_init();
// Set the url we’re requesting
curl_setopt($chGetSize, CURLOPT_URL, “http://www.example.com/file.exe”);
// Set a valid user agent
curl_setopt($chGetSize, CURLOPT_USERAGENT, “Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.11) Gecko/20071127 Firefox/2.0.0.11”);
// Don’t output any response directly to the browser
curl_setopt($chGetSize, CURLOPT_RETURNTRANSFER, true);
// Don’t return the header (we’ll use curl_getinfo();
curl_setopt($chGetSize, CURLOPT_HEADER, false);
// Don’t download the body content
curl_setopt($chGetSize, CURLOPT_NOBODY, true);
// Run the curl functions to process the request
$chGetSizeStore = curl_exec($chGetSize);
$chGetSizeError = curl_error($chGetSize);
$chGetSizeInfo = curl_getinfo($chGetSize);
// Close the connection
curl_close($chGetSize);// Print the file size in bytes
$size=$chGetSizeInfo[‘download_content_length’];
thx again !
Goosy, the cURL method to retrieve the file size doesn’t work ever.
The best function to retrieve the size of a remote file is this:
function remotefilesize( $url ){
$size = get_headers( $url, 1 );
$size = $size[ “Content-Length” ];
return $size;
}
Anyway thanks a lot for the snippet..
swellendam accommodation…
[…]How To Force File Download With PHP | W-Shadow.com[…]…