PHP force download, not sending full binary files - php

I'm creating a protected file download system for an e-commerce website. I am using PHP to authenticate that a user is logged in and owns the product before it is downloaded. I am using the example provided by the PHP.net manual but have run into an error serving both PDF and PNG files.
It has worked at one point, during development. Now that we've gone live, it seems to have broke... Fortunately the website is not running full-force at the moment so it's not a huge issue right now.
What is happening:
You visit the download URL. You are verified as an owner of the product, and the download starts. Everything appears to be normal, reviewing the headers everything looks OK. The header states the content length is "229542" (224.16kb), which looks OK.
The problem:
When the download completes, the file size is only 222 bytes. There are no errors displayed, and no PHP errors/warnings/notices are being sent in the file or browser. It's as if PHP is being terminated, but without any warnings. Even if I turn debugging on, I don't see any warnings.
The source file looks like this (in Notepad++, it's a PDF so it's binary)
When downloaded, it looks like this (Only 7 lines, compared to 2554)
My guess:
I am fairly certain the issue is header related. Maybe caused by a plugin? This shows up in Chrome's networking console:
Response Headers:
Below are the response headers. The "Connection: close" concerns me. I am not sending that header. I'm guessing it is sent by the exit command.
Access-Control-Allow-Origin:*
Cache-Control:must-revalidate, post-check=0, pre-check=0
Connection:close
Content-Description:File Transfer
Content-Disposition:attachment; filename="workbook.pdf"
Content-Length:229542
Content-Transfer-Encoding:binary
Content-Type:application/pdf
Date:Tue, 03 Sep 2013 21:16:14 GMT
Expires:0
Pragma:public
Server:Apache
X-Powered-By:PleskLin
I have also tried:
Turning on PHP debugging to get some sort of error/warning, but I don't get anything useful in browser or in the downloaded file source.
Outputting to the browser, rather than streaming. The file is still cut short, I would expect a PDF would try to display in the browser's PDF reader - but it doesn't even try. Just a white page with a bunch of unknown symbols.
Using +rb read mode, using fopen( $file['file'], 'rb' ); echo fread( $fh, filesize( $file['file'] ) ); fclose( $fh );
Using ignore_user_abort(true) and set_time_limit(0) to no effect (also tried set_time_limit(10000) just for in case, still nothing)
Sending a .txt file. This worked. I tried with a simple 5-byte text file, and again with an 86.3kb file. Both appear to have come out the same as they went in. So it must be related to binary files...
/* --- This is what the $file variable looks like:
$file = array(
[file] => /var/www/vhosts/my-website/uploads/workbook.pdf
[type] => application/pdf
[filesize] => 229542
[filename] => workbook.pdf
[upload_date] => 1377278303
);
*/
// Stream the file to the user
if (file_exists($file['file'])) {
header_remove(); // Remove any previously set header
header('Content-Description: File Transfer');
header('Content-Type: ' . $file['type']);
header('Content-Disposition: attachment; filename="'.$filename.'"');
header('Content-Transfer-Encoding: binary');
header('Expires: 0');
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header('Pragma: public');
header('Content-Length: ' . $filesize);
ob_clean();
flush();
readfile($file['file']);
exit;
}

I know this is an older thread but I have the solution for everyone here.
You have a script somewhere in your application that's compressing spaces or other characters.
ob_start("sanitize_output");
That was the problem in one scenario I noticed. If you turn off the buffer reduction script (called "sanitize_output" above) then your .pdf will show up normally. That extra 2kb's you noticed that were trimmed was probably from spacing or cutting out unneeded characters.

Try this code: Just give $file a relative path to the file and check it.
<?php
// your file to download
$file = path/to/the/file;
header("Expires: 0");
header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
header("Cache-Control: no-store, no-cache, must-revalidate");
header("Cache-Control: post-check=0, pre-check=0", false);
header("Pragma: no-cache");
$ext = pathinfo($file, PATHINFO_EXTENSION);
$basename = pathinfo($file, PATHINFO_BASENAME);
header("Content-type: application/".$ext);
// tell file size
header('Content-length: '.filesize($file));
// set file name
header("Content-Disposition: attachment; filename=\"$basename\"");
readfile($file);
// Exit script. So that no useless data is output-ed.
exit;
?>
Let php calculate the file size and date and other stuff. It is quiet possible that an error can occur while feeding it via an array.
Also in this code I have used pathinfo so as to automatically set the Content-type and file name.

I would like to contribute for someone that was lost with this solution.
What worked for me, has said above by #Kalob Taulien was insert these lines that turn off a compactation algorith builtin that was corrupting my files on download by the script, here is:
#ob_end_clean();
if(ini_get('zlib.output_compression'))
ini_set('zlib.output_compression', 'Off');
After insert that before fpassthru($file) function, the downloads worked well.

Related

PHP: output file without getting it into memory [duplicate]

I want to serve an existing file to the browser in PHP.
I've seen examples about image/jpeg but that function seems to save a file to disk and you have to create a right sized image object first (or I just don't understand it :))
In asp.net I do it by reading the file in a byte array and then call context.Response.BinaryWrite(bytearray), so I'm looking for something similar in PHP.
Michel
There is fpassthru() that should do exactly what you need. See the manual entry to read about the following example:
<?php
// open the file in a binary mode
$name = './img/ok.png';
$fp = fopen($name, 'rb');
// send the right headers
header("Content-Type: image/png");
header("Content-Length: " . filesize($name));
// dump the picture and stop the script
fpassthru($fp);
exit;
?>
See here for all of PHP's filesystem functions.
If it's a binary file you want to offer for download, you probably also want to send the right headers so the "Save as.." dialog pops up. See the 1st answer to this question for a good example on what headers to send.
I use this
if (file_exists($file)) {
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename='.basename($file));
header('Content-Transfer-Encoding: binary');
header('Expires: 0');
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header('Pragma: public');
header('Content-Length: ' . filesize($file));
ob_clean();
flush();
readfile($file);
exit;
}
I use readfile() ( http://www.php.net/readfile )...
But you have to make sure you set the right "Content-Type" with header() so the browser knows what to do with the file.
You can also force the browser to download the file instead of trying to use a plug-in to display it (like for PDFs), I always found this to look a bit "hacky", but it is explained at the above link.
This should get you started:
http://de.php.net/manual/en/function.readfile.php
Edit: If your web server supports it, using
header('X-Sendfile: ' . $filename);
where file name contains a local path like
/var/www/www.example.org/downloads/example.zip
is faster than readfile().
(usual security considerations for using header() apply)
For both my website and websites I create for clients I use a PHP script that I found a long time ago.
It can be found here: http://www.zubrag.com/scripts/download.php
I use a slightly modified version of it to allow me to obfuscate the file system structure (which it does by default) in addition to not allowing hot linking (default) and I added some additional tracking features, such as referrer, IP (default), and other such data that I might need should something come up.
Hope this helps.
Following will initiate XML file output
$fp = fopen($file_name, 'rb');
// Set the header
header("Content-Type: text/xml");
header("Content-Length: " . filesize($file_name));
header('Content-Disposition: attachment; filename="'.$file_name.'"');
fpassthru($fp);
exit;
The 'Content-Disposition: attachment' is pretty common and is used by sites like Facebook to set the right header

headers to force the download of a tar archive

i have a tar archive on the server that must be downloadable through php. This is the code that i've used:
$content=file_get_contents($tar);
header("Content-Type: application/force-download");
header("Content-Disposition: attachment; filename=$tar");
header("Content-Length: ".strlen($content));
unlink($name);
die($content);
The file is downloaded but it's broken and it can't be open. I think that there's something wrong with headers because the file on the server can be open without problems. Do you know how can i solve this problem?
UPDATE
I've tried to print an iframe like this:
<iframe src="<?php echo $tar?>"></iframe>
And the download works, so i'm sure that there's something missing in headers.
I have used this code when I have had to do it:
function _Download($f_location, $f_name){
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header('Content-Length: ' . filesize($f_location));
header('Content-Disposition: attachment; filename=' . basename($f_name));
readfile($f_location);
}
_Download("../directory/to/tar/raj.tar", "raj.tar");
//or
_Download("/var/www/vhost/domain.com/httpdocs/directory/to/tar/raj.tar", "raj.tar");
Try that.
Don't use file_get_contents() and then echo or print to output the file. That loads the full contents of the file into memory. A large file can/will exceed your script's memory_limit and kill the script.
For dumping a file's contents to the client, it's best to use readfile() - it will properly slurp up file chunks and spit them out at the client without exceeding available memory. Just remember to turn off output buffering before you do so, otherwise you're essentially just doing file_get_contents() again
So, you end up with this:
$tar = 'somefile.tar';
$tar_path = '/the/full/path/to/where/the/file/is' . $tar;
$size = filesize($tar_path);
header("Content-Type: application/x-tar");
header("Content-Disposition: attachment; filename='".$tar."'");
header("Content-Length: $size");
header("Content-Transfer-Encoding: binary");
readfile($tar_path);
If your tar file is actually gzipped, then use "application/x-gtar" instead.
If the file still comes out corrupted after download, do some checking on the client side:
Is the downloaded file 0 bytes, but the download process seemed to take much longer than it would take for 0 bytes to transfer, then it's something client-side preventing the download. Virus scanner? Trojan?
Is the downloaded file partially present, but smaller than the original? Something killed the transfer prematurely. Overeager firewall? Download manager having a bad day? Output buffering active on the server and the last buffer bucket not being flushed properly?
Is the downloaded file the same size as the original? Do an md5/sha1/crc checksum on both copies. If those are the same, then something's wrong with the app opening the file, not the file itself
Is the downloaded file bigger than the original? Open the file in notepad (or something better like notepad++ which doesn't take years to open big fils) and see if any PHP warnings messages, or some invisible whitespace you can't see in your script got inserted into the download at the start or end of the file.
Try something like the following:
$s_filePath = 'somefile.ext';
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="'. s_filePath.'"');
header('Content-Transfer-Encoding: binary');
header('Accept-Ranges: bytes');
header('Cache-control: private');
header('Pragma: private');
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header("Content-Length: ".filesize($s_filePath));
$r_fh = fopen($s_filePath,'r');
while(feof($r_fh) === false) {
$s_part = fread($r_fh,10240);
echo $s_part;
}
fclose($r_fh);
exit();
Use Content-Type: application/octet-stream or Content-Type: application/x-gtar
Make sure you aren't echoing anything that isn't the file output. Call ob_clean() before the headers

Linking to a forced download script in a pdf

I am generating dynamic PDF reports in PHP and having some issues with links to the web.
My links are to a PHP script that forces the download of a file attachment. This script works perfectly in all browser when accessed via the browser. It also works from the PDF in all browser except Internet Explorer.
Instead of IE seeing the file as a PDF, PNG, or whatever the file is, the download prompt says the document type is: "HTML Plugin Document"
If the user clicks "Open" or "Save" IE says it cannot download the file and gives the filename as "index2.php". That is the beginning of the URI of address.
The correct filesize is given so I know it is getting the file. Maybe it is a header issue?
Here is the header I'm creating on the download script:
header("Expires: Mon, 26 Jul 1997 05:00:00 GMT;");
header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT;");
header("Pragma: no-cache;"); // HTTP/1.0
header('Content-Type: '.$file->file_mime_type.';');
header("Content-Description: File Transfer");
header("Cache-Control: public");
header('Content-Disposition: attachment; filename="'.$file->file_name.'";');
header('Content-Length: '.$file->file_size.';');
header('Content-transfer-encoding: binary');
Any input would be greatly appreciated.
Here's what I use and that is proven to work:
header('Content-Type: application/pdf');
header('Content-Disposition: attachment; filename="'.$file->file_name.'"');
readfile($filename);
have you tried something like this?
header('Content-Disposition: attachment; filename="'.$file->filename.'"');
It might be worth mentioning that there is also a known issue on several versions of IE when transferring files over SSL which requires the following work around:
header("Cache-Control: maxage=1");
header("Pragma: public");
There is more information regarding this bug here: http://support.microsoft.com/kb/812935

PHP output file on disk to browser

I want to serve an existing file to the browser in PHP.
I've seen examples about image/jpeg but that function seems to save a file to disk and you have to create a right sized image object first (or I just don't understand it :))
In asp.net I do it by reading the file in a byte array and then call context.Response.BinaryWrite(bytearray), so I'm looking for something similar in PHP.
Michel
There is fpassthru() that should do exactly what you need. See the manual entry to read about the following example:
<?php
// open the file in a binary mode
$name = './img/ok.png';
$fp = fopen($name, 'rb');
// send the right headers
header("Content-Type: image/png");
header("Content-Length: " . filesize($name));
// dump the picture and stop the script
fpassthru($fp);
exit;
?>
See here for all of PHP's filesystem functions.
If it's a binary file you want to offer for download, you probably also want to send the right headers so the "Save as.." dialog pops up. See the 1st answer to this question for a good example on what headers to send.
I use this
if (file_exists($file)) {
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename='.basename($file));
header('Content-Transfer-Encoding: binary');
header('Expires: 0');
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header('Pragma: public');
header('Content-Length: ' . filesize($file));
ob_clean();
flush();
readfile($file);
exit;
}
I use readfile() ( http://www.php.net/readfile )...
But you have to make sure you set the right "Content-Type" with header() so the browser knows what to do with the file.
You can also force the browser to download the file instead of trying to use a plug-in to display it (like for PDFs), I always found this to look a bit "hacky", but it is explained at the above link.
This should get you started:
http://de.php.net/manual/en/function.readfile.php
Edit: If your web server supports it, using
header('X-Sendfile: ' . $filename);
where file name contains a local path like
/var/www/www.example.org/downloads/example.zip
is faster than readfile().
(usual security considerations for using header() apply)
For both my website and websites I create for clients I use a PHP script that I found a long time ago.
It can be found here: http://www.zubrag.com/scripts/download.php
I use a slightly modified version of it to allow me to obfuscate the file system structure (which it does by default) in addition to not allowing hot linking (default) and I added some additional tracking features, such as referrer, IP (default), and other such data that I might need should something come up.
Hope this helps.
Following will initiate XML file output
$fp = fopen($file_name, 'rb');
// Set the header
header("Content-Type: text/xml");
header("Content-Length: " . filesize($file_name));
header('Content-Disposition: attachment; filename="'.$file_name.'"');
fpassthru($fp);
exit;
The 'Content-Disposition: attachment' is pretty common and is used by sites like Facebook to set the right header

How can I allow a download to pause/resume?

Normally, when I want to allow a user to download a file without revealing the exact location, I just use something like this to let them download the file:
header("Pragma: public");
header("Expires: 0");
header("Cache-Control: must-revalidate, post-check=0, pre-check=0");
header("Content-Type: application/octet-stream");
header("Content-Disposition: attachment; filename=\"" . $filename) . "\";");
header("Content-Transfer-Encoding: binary");
header("Content-Length: " . filesize($filename));
readfile("$filename");
But if they are using a modern browser or other download client, and they pause the download and try to resume it, the script (assuming they are still authenticated or whatever) will resend the headers and the file contents from the beginning, thus breaking the download, and basically requiring the file to be redownloaded from the beginning.
How can I enable my script to compensate for paused (and consequentially, resumed) downloads?
Use php's built-in fopen to open the file and then fseek to the right place (based on the range in the request header) and then return the partial file using fpassthru instead of using readfile.
You can find some example code in php under the comments for fread
You need to read the request headers like Range, If-Range, etc then seek to the correct location in the file. Normally a web-server would do this for you on an ordinary file. It's a bit complex but here's something that might get you started:
http://forums.asp.net/t/1218116.aspx
http://www.notes411.com/dominosource/tips.nsf/0/480C4E3BE825F69D802571BC007D5AC9!opendocument
For the second link the code is in part 12

Categories