Idiot-proof, cross-browser force download in PHP

I use forced download to upload mainly zips and mp3 to the site I made ( http://pr1pad.kissyour.net ) - track downloads in Google analytics, in the database and hide the real download path:

It:

extending CI model ... - bunch of code function _fullread ($sd, $len) { $ret = ''; $read = 0; while ($read < $len && ($buf = fread($sd, $len - $read))) { $read += strlen($buf); $ret .= $buf; } return $ret; } function download(){ /* DOWNLOAD ITSELF */ ini_set('memory_limit', '160M'); apache_setenv('no-gzip', '1'); ob_end_flush(); header("Pragma: public"); header("Expires: 0"); header("Cache-Control: must-revalidate, post-check=0, pre-check=0"); header("Cache-Control: public",FALSE); header("Content-Description: File Transfer"); header("Content-type: application/octet-stream"); if (isset($_SERVER['HTTP_USER_AGENT']) && (strpos($_SERVER['HTTP_USER_AGENT'], 'MSIE') !== false)) header('Content-Type: application/force-download'); //IE HEADER header("Accept-Ranges: bytes"); header("Content-Disposition: attachment; filename=\"" . basename("dir-with- files/".$filename) . "\";"); header("Content-Transfer-Encoding: binary"); header("Content-Length: " . filesize("dir-with-files/".$filename)); // Send file for download if ($stream = fopen("dir-with-files/$filename", 'rb')){ while(!feof($stream) && connection_status() == 0){ //reset time limit for big files set_time_limit(0); print($this->_fullread($stream,1024*16)); flush(); } fclose($stream); } } 

This is on LAMP with CI 1.7.2. This is my own method, made up of different practices throughout the Internet, because during development these problems arose: - server limitation . ini_set did not help, so I used buffered _fullread instead of the usual fread , which was used insted from @readonly - ob_end_flush (), because the site is executed in CI1.7.2, and I need to clear the buffer

Now ... This will not work. So it was, then it stopped showing the expected size / loading time - I tried to clear it, and while I was clearing the code, something happened, I do not know that in any previous version it did not work (no changes in the settings in general) - edit: do not work = displays everything in a browser window.

So, I said, screw it in, I'll look here.

So, I'm basically looking for a script or function that I can put in my output model and do:

  • Force download call (at the beginning of Chrome download, in IE, FF, Safari open modal opening / saving / canceling)
  • Show file size and estimated dl time (what about the browser, I know, but first the browser should know the file size
  • WORK (verified and verified!) In IE6,7,8, FF3, Opera, Chrome and Safari on PC + Mac (Linux ... I don't care) - this is for part of the header
  • on the server, I also have something like a 56 MB memory limit, which I cannot add to, so itโ€™s also important

Thanks in advance.

Edit : now I feel more screwed up than ever / before, since I tried to force download using .htaccess - while it worked, it had a few minor / major (pick yours) problems

  • he showed the full path (insignificant for me)
  • it waits for the complete download to complete (shown as a โ€œconnectionโ€), and then just shows its download - and download in one second (major for me).

Now, although I deleted .htaccess, it is still waiting for the download to complete (as if it first loaded into the cache), and it just gets connected and opens the open / save dialog.

+6
php header
source share
7 answers

So, I used this code (this is a modified version of the renewable http download found on the Internet)

 function _output_file($file, $path) { $size = filesize($path.$file); @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: application/force-download'); header('Content-Disposition: attachment; filename="'.basename($file).'"'); header("Content-Transfer-Encoding: binary"); header('Accept-Ranges: bytes'); /* The three lines below basically make the download non-cacheable */ header("Cache-control: no-cache, pre-check=0, post-check=0"); 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($path.$file, 'rb')) { 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(); } 

and then in the model:

 function download_file($filename){ /* DOWNLOAD */ $path = "datadirwithmyfiles/"; //directory //track analytics include('includes/Galvanize.php'); //great plugin $GA = new Galvanize('UA-XXXXXXX-7'); $GA->trackPageView(); $this->_output_file($filename, $path); } 

It works as expected in all browsers mentions on Win / MAC - there are still no problems with it.

+6
source share

Well, this is an old question, and Adam already accepted his own answer, so apparently he got this job for himself, but he did not explain why it worked. One thing I noticed was that he used headers:

 header("Pragma: public"); header("Cache-Control: public",FALSE); 

While in the solution, he used:

 header("Cache-control: private"); header('Pragma: private'); 

He did not explain why he changed them, but I suspect that this is due to the use of SSL. I recently solved a similar problem in software that should include downloading both HTTP and HTTPS, using the following to add the correct header:

 if(!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' || $_SERVER['SERVER_PORT'] == 443) { header("Cache-control: private"); header('Pragma: private'); } else { header('Pragma: public'); } 

Hope someone finds the information in this answer a useful addition to the previous one.

+2
source share

There is one strange thing: you call ob_end_flush() at the beginning of the function. This actually clears the output buffer, but also prints everything to the client first (I assume, including the Content-Headers installed by CodeIgniter). Change the call to ob_end_clean() , it will clear the buffer and cancel it. This will give you a clean start to create your own headlines.

Another tip:

Instead of reading the file as a stream and passing it block by block, you can try this function:

 // ... if (file_exists("dir-with-files/$filename")) { readfile($file); } 

That is almost all.

+1
source share

print($this->_fullread($stream,1024*16));

I assume _fullread is inside the class? If the code looks like this, then $this-> does not work.

Does the contents of the file display if you comment out all the header material?

0
source share

Just a shot in the dark ... every header that I send in my force loading code (which is not as well tested as yours) is the same as yours, except that I call: header ("Cache-Control: private ", false);

instead: header ("Cache-Control: public", FALSE);

I do not know if this will help.

0
source share

If you intend to use this method "Echo it out with php", then you will not be able to show the remaining time or expected size for your users. What for? Because if the browser tries to resume loading in the middle, you have no way to handle this case in PHP.

If you have a regular file download, Apache can support renewed downloads over HTTP, but if the download is paused, Apache has no way to find out where things were done in your script when the client asks for the next snippet.

In fact, when the browser pauses the download, it completely terminates the connection to the web server. When you resume the download, the connection opens again, and the request contains the flag โ€œStart from byte Xโ€. But for a web server looking at your PHP above, where is the X byte?

Although it is theoretically possible that the server will be able to determine where you can resume your script in case of interruption in loading, Apache does not try to figure out where to resume. As a result, the header sent to the browser indicates that the server does not support renewal, which disables the expected size and time elements in most major browsers.

EDIT: It seems you could handle this thing, but it will take a lot of code on your part. See http://www.php.net/manual/en/function.fread.php#84115 .

0
source share

Instead of hiding your download path from the world, it makes it inaccessible from the outside and only access to files with the above script. To do this, you put the htaccess file (a text file named ".htaccess" do not forget about the leading point) in the directory. The content of htaccess will be as follows:

 order deny,allow deny from all allow from localhost 

Now, trying to access the path from * world, the web server will create the forbidden 401.

Security through obscurity is not what you want.

0
source share

All Articles