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');

Related info
Determining MIME type of a file automatically
Common MIME types
HTTP, caching and other stuff

Related posts :

111 Responses to “How To Force File Download With PHP”

  1. mark says:

    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 :)

  2. Joern says:

    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?

  3. White Shadow says:

    How “large” are we talking about? If it’s over 4 GB, it might cause an integer overflow.

  4. Joern says:

    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

  5. White Shadow says:

    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.

  6. Eric says:

    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.

  7. Manuel says:

    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

  8. cesar says:

    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

  9. [...] 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 [...]

  10. pk1001100011 says:

    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.

  11. Ozzie Bock says:

    Thank you for posting this article, it really helped when I needed to force download a connection program for our remote pc support business.

  12. Shawn Deprey says:

    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!

  13. JavaGenious says:

    Thanks for the info. Can you also provide an example of use for this method?

  14. Flor says:

    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???

  15. vhortex says:

    @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.

  16. nlayno says:

    how to use this script ?

    foredownload.php?file=http://–.zip <- like that?

  17. goosy says:

    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 !

  18. WuFe says:

    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..

  19. swellendam accommodation…

    [...]How To Force File Download With PHP | W-Shadow.com[...]…

Leave a Reply