I don’t think I have posted this snipped of code here yet. It is old as hell, but I use it all over the place lately so I figured I just post it here for future reference.

The script below solves the age old problem of making bunch of files accessible to your users without dumping them in a publicly accessible directory (files, not users). For example, you have bunch of PDF documents you don’t want to be indexed by Google, or directly linked to from other websites. What you want is to allow certain users to download them after logging into your web app and checking their cridentials. How do you do that?

Well, the simplest thing to do is to put them in some directory outside of your web root. Set up .htaccess (or whatever you use) to disallow any connection to that folder from the outside world. The only machine with access to these files should be localhost. Then you do some PHP magic in your application to stream the file into the browser upon successful authentication.

The streaming part is actually pretty straightforward – it is a simple combination of the print and fread commands. The convoluted part is convincing your browser to actually initiate file download instead of just dumping ASCII gibberish onto the page for binary files. This is accomplished by sending bunch of headers to the browser prior to streaming the files. The headers are different for IE and the rest of the world but this is pretty much what I ended up with after a lot of trial and error:

// change these to whatever is appropriate in your code $my_place = "/path/to/the/file/"; // directory of your file $my_file = "filename.ext"; // your file $my_path = $my_place.$my_file; header("Pragma: public"); header("Expires: 0"); header('Cache-Control: no-store, no-cache, must-revalidate'); header('Cache-Control: pre-check=0, post-check=0, max-age=0', false); header('Last-Modified: '.gmdate('D, d M Y H:i:s') . ' GMT'); $browser = $_SERVER['HTTP_USER_AGENT']; if(preg_match('/MSIE 5.5/', $browser) || preg_match('/MSIE 6.0/', $browser)) { header('Pragma: private'); // the c in control is lowercase, didnt work for me with uppercase header('Cache-control: private, must-revalidate'); // MUST be a number for IE header("Content-Length: ".filesize($my_path)); header('Content-Type: application/x-download'); header('Content-Disposition: attachment; filename="'.$my_file.'"'); } else { header("Content-Length: ".(string)(filesize($my_path))); header('Content-Type: application/x-download'); header('Content-Disposition: attachment; filename="'.$my_file.'"'); } header('Content-Transfer-Encoding: binary'); if ($file = fopen($my_path, 'rb')) { while(!feof($file) and (connection_status()==0)) { print(fread($file, filesize($my_path)); flush(); } fclose($file); }

I found these headers to work for me. Your millage may vary. I tested the script above in IE 6 and 7, Firefox 2.x and 3.x, Konqueror 3.5.8 and Chrome 0.2.149.30 and experienced no problems. As usual, I’m always open to constructive criticism and better solutions in the comments. Let me know what you think!