Force Downloading a PDF file, corrupt file - php

I've got a problem that has risen many times on SO, but I can't seem to find the solution to mine! I'm trying to deliver a pdf file to the client without it opening in the browser, the file downloads but it is corrupt when I open it and is missing quite a few bytes from the original file. I've tried several such methods for downloading the file but I'll just show you the latest I've used and hopefully get some feedback.
I have also opened the downloaded PDF in a text editor and there are no php errors at the top of it that I can see!
I'm also aware that readfile() is much quicker but for testing purposes I am desperate to get anything working so I used the while(!feof()) approach!
Anyway enough rambling, heres the code (taken from why my downloaded file is alwayes damaged or corrupted?):
$file = __DIR__ . '/reports/somepdf.pdf';
$basename = basename($file);
$length = sprintf("%u", filesize($file));
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . $basename . '"');
header('Content-Transfer-Encoding: binary');
header('Connection: Keep-Alive');
header('Expires: 0');
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header('Pragma: public');
header('Content-Length: ' . $length);
ob_clean();
set_time_limit(0);
readfile($file);
Also to note was the difference in file size:
Original: 351,873 bytes
Downloaded: 329,163 bytes

Make sure you're not running any compression output buffering handlers, such as ob_gzhandler. I had a similar case and I had to disable output buffering for this to work properly

You are using the the ob_gzhandler on the output buffer.
It works by gzencoding chunks of output. The output then is a stream of the encoded chunks.
Each chunk needs to get some bytes to get encoded, so the output is a little bit buffered until enough bytes are available.
However at the end of your script you discard the remaining buffer instead of flushing it.
Use ob_end_flush() instead of ob_clean() and the file gets through fully and not corrupted.
You are to use the transfer encoding of ob_gzhandler with file-uploads not having any problems when you don't destroy the output-buffer before it could have done it's work.
This is also the same if any other output buffering that works chunked would have been
enabled.
Example code:
$file = __DIR__ . '/somepdf.pdf';
$basename = basename($file);
$length = sprintf("%u", filesize($file));
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . $basename . '"');
header('Content-Transfer-Encoding: binary');
header('Connection: Keep-Alive');
header('Expires: 0');
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header('Pragma: public');
header('Content-Length: ' . $length);
ob_end_flush(); // <--- instead of ob_clean()
set_time_limit(0);
readfile($file);
return;
(FYI: actually even the ob_end_flush(); is not necessary, the important part is not to just kick the output-buffer before it could have done it's work)

I fought with using content-disposition for pushing a PDF download for two days before finding a solution to my problem. My PDF files were also smaller in size and corrupt - however, I could open them in Windows Preview - just not Adobe. After much troubleshooting, I discovered that Adobe expects the %PDF in the first 1024 bytes of the file. I was doing all my file type checks in my php code before creating the headers. I took out the majority of code before the headers and my PDF file was fixed.
You might not be setting it up the same way I did, but it might be the same problem:
http://helpx.adobe.com/acrobat/kb/pdf-error-1015-11001-update.html

Related

PHP: image corrupted if I force the download

I've a strange behavior with a simple PHP code. When I try to force the download or print out the image using the correct content-type, the output file is corrupted.
Seems that the webserver (apache) adds two bytes (0x20 and 0x0A) at the begin of the file.
This is the code:
$file = "image.png";
$image = file_get_contents($file);
// Test
file_put_contents("test.png", $image);
// Download
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename='.basename($file));
echo $image;
I use the same code on other websites hosted on the same server without problems.
The problem is only on download only because test.png works properly. The MD5 checksum of text.png and the original image are equals.
This is the hex code of test.png.
And this is the hex code of the corrupted file after download:
As you can see, there are 2 extra bytes at the begin. If I remove them, the file returns to work properly.
I attach the screen of Wireshark (as you can see is not a browser issue):
How can I fix it?
The server is Ubuntu 16.04 with PHP-5.6 (yes I done the downgrade from 7.0 to 5.6 for compatibility issues with roundcube)
UPDATE 1: I'm trying to find if somewhere in the file there is a space + newline
UPDATE 2:
First of all: thanks.
The code is part of a Wordpress plugin and the download is called using the AJAX system. I wrote a simple plugin test:
<?php
/*
Plugin Name: Test
Plugin URI: http://www.google.com
Description: Test
Author: Anon
Version: 4.0
*/
function downlod_test() {
echo "test";
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename=prova.html');
die();
}
function iopman_shared_download_doc_ajax() {
downlod_test();
}
add_action('wp_ajax_frontend_download_doc', 'iopman_shared_download_doc_ajax');
//downlod_test();
?>
If I call downlod_test with /wp-admin/admin-ajax.php?action=frontend_download_doc it adds the 2 extra bytes. If I call it directly (by removing the comments), it works.
So the problem now is: how to strip out these bytes that wordpress adds?
$file = "image.png";
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename=' . basename($file));
header("Content-Encoding: gzip");
header('Expires: 0');
header('Cache-Control: must-revalidate');
header('Pragma: public');
header("Content-Length: " . filesize($file));
header('Content-Transfer-Encoding: binary');
header('Connection: Keep-Alive');
header('Expires: 0');
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
ob_get_clean();
readfile($file);
exit;
To help you find that unwanted whitespace you can track loaded files with get_included_files(). Additionally, a backtrace could also shred some light on what your script does.
In many cases, it'll come from closing PHP tags at the end of the file. Since they're optional it's recommended to just not use them.
Once you locate the file where that white space is, you only need to load in your favourite text editor and remove them (you might need to enable your editor's Show hidden chars feature).
P.S. I understand that's probably simplified code to illustrate the issue but you may want to give readfile() a try.

PHP File download with original filename

I'm searching for a possibility to download a file with PHP and keep its original name.
This should work for direct links to a file ("foo.bar/test.zip") and for downloads who are proxied through PHP files ("foo.bar/download.php?fileid=12345" <- this would open a save-as prompt with test.zip as given name).
Is there a possibility in PHP that I could use to do this?
I tried different possibilities (fopen, cURL, file_get_content, etc.). But all possibilities I've found required me to manually set the new filename in order to save it locally.
Even better would be if I could proxy the remote file through my script (with it's original name!) to the client who is calling the script, without saving it temporarily.
I found the following snippet which accomplishes this exact thing
$fp = fopen($_POST["url"], 'rb');
foreach (get_headers($_POST["url"]) as $header)
{
header($header);
}
fpassthru($fp);
The file I get to download is of the right size but has the wrong name (it has the name of the called PHP script, in my case downloader.php instead of e.g. test.zip).
Any idea how to accomplish this would be very appreciated.
Thanks in advance!
May be something like this will help:
$quoted = sprintf('"%s"', addcslashes(basename($file), '"\\'));
$size = filesize($file);
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename=' . $quoted);
header('Content-Transfer-Encoding: binary');
header('Connection: Keep-Alive');
header('Expires: 0');
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header('Pragma: public');
header('Content-Length: ' . $size);
Just replace $file with the file you want to be downloaded.

PHP to CSV output to download

The snippet reads an ouput stream in a new file CSV. That part works. I can open the file from the server and it looks perfect. The problem is with the file which downloads to my hard drive through the browser. It will not open and read properly in spreadsheet software or Excel on a Windows machine:
$NewFile = fopen($FileName,"w");
fwrite($NewFile, $output);
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
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($NewFile));
ob_clean();
flush();
readfile($NewFile);
The content of the CSV looks ��ࡱ� like when I open it from the browser download but looks perfect when I open it directly on the server or download the stored file with FTP.
The browser sees the application/octet-stream as a binary type. You need a text/plain content type:
header('Content-Type: text/plain');
// Or:
header('Content-Type: text/csv');
// Or:
header('Content-Type: application/csv');
The Content-Transfer-Encoding header should be unnecessary if you have the Content-Type correctly set, and indeed it is probably misleading the browser into thinking it has received a binary file as well:
// No need, possibly harmful. Remove it...
// header('Content-Transfer-Encoding: binary');
Update:
I see another problem. You are setting the Content-Length not to the size of the file, but rather to the file handle opened by fopen(), which mis-informs the browser of the number of bytes to expect. filesize() takes a string filename as its argument, not a file handle. You will probably need to close the handle with fclose($NewFile) before calling filesize().
// Instead of:
header('Content-Length: ' . filesize($NewFile));
// You mean to use $FileName
// close the file handle first...
fclose($NewFile);
header('Content-Length: ' . filesize($FileName));

PHP downloading excel file becomes corrupt

I have an excel file that i want a user to be able to download from my server. I have looked at a lot of questions on here but i cannot find a way to correctly download the file w/o corruption. I am assuming it is the headers but i haven't had a working combination of them yet. This is what i have right now and in the corrupt file that i receive i can see the column names of the spreadsheet i want but its all messed up.
$filename = '/var/www/web1/web/public/temporary/Spreadsheet.xls';
header("Content-type: application/octet-stream");
header("Content-type: application/vnd-ms-excel");
header("Content-Disposition: attachment; filename=ExcelFile.xls;");
header("Pragma: no-cache");
header("Expires: 0");
readfile($filename);
edit: Solution I forgot to add that i was using Zend and it was corrupting the files when trying to use native php methods. My finsihed code was to place a link to another action in my controller and have the files download from there
public function downloadAction(){
$file = '/var/www/web1/web/public/temporary/Spreadsheet.xls';
header('Content-Type: application/vnd.ms-excel');
header('Content-Disposition: attachment; filename="Spreadsheet.xls"');
readfile($file);
// disable the view ... and perhaps the layout
$this->view->layout()->disableLayout();
$this->_helper->viewRenderer->setNoRender(true);
}
try doing it this way
ob_get_clean();
echo file_get_contents($filename);
ob_end_flush();
For one, only specify Content-Type once. You can use the excel-specific header but the generic application/octet-stream may be a safer bet just to get it working (the real difference will be what the browser shows the user with regards to "what would you like to open this file with", but basic browsers can rely on the extension as well)
Also, make sure you specify Content-Length and dump the size (in bytes) of the file you're outputting. The browser needs to know how big the file is and how much content it's expecting to receive (so it doesn't stop in the middle or a hiccup doesn't interrupt the file download).
So, the entire file should consist of:
<?php
$filename = '/var/www/web1/web/public/temporary/Spreadsheet.xls';
header("Content-Disposition: attachment; filename=ExcelFile.xls;");
header('Content-Type: application/octet-stream');
header('Content-Length: ' . filesize($filename));
header("Pragma: no-cache");
header("Expires: 0");
#readfile($filename);
$file_name = "file.xlsx";
// first, get MIME information from the file
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $file_name);
finfo_close($finfo);
// send header information to browser
header('Content-Type: '.$mime);
header('Content-Disposition: attachment; filename="download_file_name.xlsx"');
header('Content-Length: ' . filesize($file_name));
header('Expires: 0');
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
//stream file
ob_get_clean();
echo file_get_contents($file_name);
ob_end_flush();

php image file download by code

This concerns downloading files with PHP
My php version is 5.3.5 and my apache is 2.2.17
I am trying to dowload files (pdf,jpg,tiff) that I have uploaded in my server, and they download with the same size and type but I can not see them. I am guessing they are not copied right. But when I open the original uploaded ones they work just fine.
I have seen almost all the questions that appeared as suggested and none of them answered the question, te only similar one is this, but still doesnt answer my question.
to download I am using this code
header("Content-type: application/force-download");
header('Content-Disposition: inline; filename="' . $dir . '"');
header("Content-Transfer-Encoding: Binary");
header("Cache-Control: no-store, no-cache, must-revalidate");
header("Cache-Control: post-check=0, pre-check=0", false);
header("Pragma: no-cache");
header("Content-length: ".filesize($dir));
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . $file . '"');
readfile("$dir");
where $dir="62756d616769636e63/646973736572746174/ehddggh/1.JPG"
and $file="1.JPG"
can anyone give me a hint on what I am doing wrong, or give me a better solution to download files?
This smells like you are getting extra (spurious) content in your downloaded files.
Make sure you have no BOM headers, spaces, or anything else before your PHP open tags in your files; also, that you have no trailing whitespace or any other data after the closing PHP tags (if you close your PHP tags).
Also, clean up your code a bit: why multiple Content-Type headers? Why multiple Content-Disposition headers?
readfile($dir); // without the quotes?
Also, make sure that $dir actually exists
is_file($dir) or file_exists($dir)
Thank you all for the answers. I was calling the download as a function in a file with other functions in it so in the end I had to write a script alone, apart from other files. My problem was that I needed it to be safe and to only download a file if it belonged to the user and the user was logged in, so I send all the data I need, ciphered and inside the script I use a series of things to see if the owner is really the logged user. So if anyone wants to know this is the code I used and works perfectly.
<?php
session_start();
$a=$_GET['a'];
$parts=explode("-",$a);
$tres=$parts[0];
$nombre=$partes[1];
$dbcodletra=substr($tres,2);
if($dbcod!=$_SESSION["username"])$boolt=0;
if($ext==1) $extl=".jpg";
if($ext==2) $extl=".jpeg";
if($ext==3) $extl=".tif";
if($ext==4) $extl=".tiff";
if($ext==5) $extl=".pdf";
if($ext==6) $extl=".doc";
if($ext==7) $extl=".docx";
if($tipoproy==1) $dir="rute/".$dbcodletra."/".$nombre.$extl;
if($tipoproy==2) $dir="rute/".$dbcodletra."/".$nombre.$extl;
if($tipoproy==3) $dir="rute/".$dbcodletra."/".$nombre.$extl;
if($tipoproy==4) $dir="rute/".$dbcodletra."/".$nombre.$extl;
if (file_exists($dir) && $boolt) {
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename='.basename($dir));
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($dir));
ob_clean();
flush();
readfile($dir);
exit;
}else echo "<meta http-equiv=\"Refresh\" content=\"0;url=misdocumentos.php\">";
?>
Paying it forward on a two-year old question...
I had a similar issue with corrupt downloads that could not be opened (when right-click & save-as worked perfectly). After reading #Jon's answer, I figured he was on to something. If you look at the docs for readfile (linked below), you will see an ob_clean(), a flush(), and an exit in their example. All of those will minimize leakage of extra character data in the response. I just copied their Example #1 and my problem was solved.
http://php.net/readfile
Your headers look messy, try just doing this:
header('Pragma: public');
header('Cache-Control: public, no-cache');
header('Content-Type: application/octet-stream');
header('Content-Length: ' . filesize($dir));
header('Content-Disposition: attachment; filename="' . basename($dir) . '"');
header('Content-Transfer-Encoding: binary');
readfile($dir);

Categories