I am trying to serve up a dynamically generated csv file. For some reason when I get the file, there are 18 empty rows preceding the data. I don't have any space between the headers I define and the csv data I'm sending. If I write the data to a file on the server, it does not get these empty rows. However, if I write the file and then try to serve it to the user, the empty lines come back. So I'm wondering if perhaps I've messed up the headers, or if perhaps there is another issue I'm not thinking of:
function generate_csv($source_type, $include_unpublished = FALSE) {
// retrieve data from DB
....
// start up headers
$csv_name = "$source_type-$data_set-csv_" . date('Y-m-d') . '.csv';
header('Content-Type: text/x-comma-separated-values');
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header('Cache-Control: private', false); // required for certain browser
header('Content-Disposition: attachment; filename="' . $csv_name . '"');
// send csv data
print $csv_data;
} //end function
Disclaimer: I asked this question at https://drupal.stackexchange.com/questions/27649/extra-empty-rows-when-serving-csv-file, but it dosn't seem to be drupal-specific and there weren't many ideas coming up over there..
Maybe this lines "hang" in an output buffer, that were started some time before. This way you can set headers without the good old "headers already sent"-error, but this content will be send to the browser when flushing the buffer anyway.
Try
ob_clean();
print $csv_data;
http://php.net/ob-clean
It must be problem with files that you are including. Every whitespace more than one newline after php closing tag ?> is sent to the browser.
Best solution is to get rid of this closing tags in every php file.
Other option will be to remove only unnecessary new lines from them or to bufer output and disregard it before serving file.
Related
I've been trying several things and I've looked for an answer here but I haven't found anything. Therefore I'm asking the question.
I'm not sure if it is an error or if it is the way this is suposed to work.
What I'm doing (and works perfectly)
I've written a code that parses a xlsx file and decides what rows have to be inserted in the DB, and what rows have to be rejected because they contain errors (this part works perfectly).
Step 1 generates an array that contains all the rows that have problems (plus a column with info about the error). (This works perfectly).
With the array generated in step 2 I create a form element that contains a serie of inputs with the serialized info of the rejected rows + The information of the rejected rows for the user to see (this also works perfectly).
In this form, there's a button that triggers the form to be sent via POST. When this is done, all the rejected rows travel thorough POST and a $_POST variable named "confirm_upload" is set. (this works perfectly)
When step 4 is done, the page is refreshed and within the controller (the part that manages user actions) if the $_POST['confirm_upload'] is setted, the form created in step 3 is processed, generating a xlxs and downloading it for the user to inspect why those rows were rejected. The code user for the one shown below (And works perfectly).
$lines_to_download = array_merge( array($header) ,$rep_lines,$conf_lines,$error_lines);
$spreadsheet = new PhpSpreadsheet\Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->fromArray($lines_to_download, NULL , 'A1');
header('Content-Description: File Transfer');
header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet; charset=utf-8');
header('Content-Disposition: attachment; filename="'. urlencode("uninserted.xlsx").'"');
header('Content-Transfer-Encoding: binary');
header('Cache-Control: must-revalidate;');
header('Expires: 0');
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header('Content-Length: ' . filesize($spreadsheet));
ob_clean();
flush();
$writer = new PhpSpreadsheet\Writer\Xlsx($spreadsheet);
$writer->save('php://output');
What's the problem?
The problem is that, the same $_POST['confirm_upload'] that triggers the download, is supossed to change the template in the view, so the user no longer see the list of rejected lines, but the template doesn't change. I've checked in the favicon, and the form refreshes the page, but, for some reason, the view is not affected by $_POST changes.
I've commented the lines related to the download, and when they are commented, the VIEW do refresh properly. SO it's obvious that for some reason, the download process prevents the $_POST variables affect the view (although theey are obviously sent, because if it was not the case, the download would not take place).
Specifically, the view is properly affected by the $_POST changes if I comment this lines (but, obviously, then the file do not download):
header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet; charset=utf-8');
header('Content-Disposition: attachment; filename="'. urlencode("lineas_no_insertadas.xlsx").'"');
I've also tried adding Header("location: index.php ") and header(Refresh: 0), but they don't do anything. I've tried everything, including all the combinations of commenting one line and leaving the others, deleting some headers, keeping the headers to the minimim. At the end, anything but commenting those two lines, blocks the request (so the view remains unafected), because the download breaks the request.
Is it the way it is supossed to work or there's something else that I'm doing wrong?
Thanks
I have a class for writing CSV lines to a new file
fwrite($this->the_file_resource, implode($this->delimiter, $headers_array) . PHP_EOL);
When this runs on our application, every so often the data will write to the CSV file corrupted, I can run the same command immediately afterwards and it works completely as expected.
Expected:
Corrupt:
One thing I have noticed is that it's always the header and always in the first couple of columns.
What would the best method be to troubleshoot this? It's very difficult to replicate as it's not consistent.
Try setting the header correctly before generating your CSV. Your problem looks like an encoding problem
header("Content-Type: text/csv");
header("Content-Disposition: attachment; filename=file.csv");
header("Content-Transfer-Encoding: UTF-8");
On my page, people can choose to either view a pdf-file (on screen) or to download it. (to view it later on when they're offline)
When users choose to download, the code is executed once. I am keeping track of this with a counter and it increments by 1 for each download. So, this option is working fine and can be seen in the if-block below.
When users choose to view the file, the pdf file is displayed - so that's OK - but the counter increments by 2 for each view. This code is run from the else-block below.
I also checked the "Yii trace" and it is really going through all of it twice, but only when viewing the file...
if ($mode==Library::DOWNLOAD_FILE){
//DOWNLOAD
Yii::app()->getRequest()->sendFile($fileName, #file_get_contents( $rgFiles[0] ) );
Yii::app()->end();
}
else {
//VIEW
// Set up PDF headers
header('Content-type: application/pdf');
header('Content-Disposition: inline; filename="' . $rgFiles[0] . '"');
header('Content-Transfer-Encoding: binary');
header('Content-Length: ' . filesize($rgFiles[0]));
header('Accept-Ranges: bytes');
// Render the file
readfile($rgFiles[0]);
Yii::app()->end();
}
}
I tried a few other options, just to see how it would cause this to run twice:
When removing the "PDF headers" from the code above, the counter is
incremented by 1, but I obviously only get garbage on the screen...
If I get rid off the readfile command, the counter is also incremented by 1,
but the browser won't render the pdf (because it is not getting the data without this line)...
So, it's only when going through the else-block that all of it (Yii request) is executed twice...
Thanks in advance for any suggestions...
I think that is because with the sendFile() method you open the file actually just once, and in the else branch you really open it twice.
In the if branch you open the file once with the file_get_contents() and pass the file as a string to the sendFile() method and then it counts the size of this string, outputs headers, etc: http://www.yiiframework.com/doc/api/1.1/CHttpRequest#sendFile-detail
In the else branch you open the file first with the filesize() and then also with the readfile() method.
I think you could solve this problem by rewriting the else branch similar to the sendFile() method:
Basically read in the file with file_get_contents() into a string, and then count the length of this string with mb_strlen(). After you output the headers, just echo the content of the file without reopening it.
You could even copy-paste the whole sendFile() method into the else branch, just change the "attachment" to "inline" in the line (or replace this whole if/else statement with the sendFile method and just change the attachment/inline option to download or view, an even more elegant way would be overriding this method and extending with another parameter, to view or download the given file) :
header("Content-Disposition: attachment; filename=\"$fileName\"");
So I think something like this would be a solution:
// open the file just once
$contents = file_get_contents(rgFiles[0]);
if ($mode==Library::DOWNLOAD_FILE){
//DOWNLOAD
// pass the contents of file to the sendFile method
Yii::app()->getRequest()->sendFile($fileName, $contents);
} else {
//VIEW
// calculate length of file.
// Note: the sendFile() method uses some more magic to calculate length if the $_SERVER['HTTP_RANGE'] exists, you should check it out if this does not work.
$fileSize=(function_exists('mb_strlen') ? mb_strlen($content,'8bit') : strlen($content));
// Set up PDF headers
header('Content-type: application/pdf');
header('Content-Disposition: inline; filename="' . $rgFiles[0] . '"');
header('Content-Transfer-Encoding: binary');
header('Content-Length: ' . $fileSize);
header('Accept-Ranges: bytes');
// output the file
echo $contents;
}
Yii::app()->end();
I hope this solves your problem, and my explanations are understandable.
I need to write a CSV file to the PHP output buffer and then download that file to the client's computer after it's done writing. (I wanted to just write it on the server and download it which was working, but it turns out I won't have write access on production servers).
I have the following PHP script:
$basic_info = fopen("php://output", 'w');
$basic_header = array(HEADER_ITEMS_IN_HERE);
#fputcsv($basic_info, $basic_header);
while($user_row = $get_users_stmt->fetch(PDO::FETCH_ASSOC)) {
#fputcsv($basic_info, $user_row);
}
#fclose($basic_info);
header('Content-Description: File Transfer');
header('Content-Type: application/csv');
header('Content-Disposition: attachment; filename=test.csv');
header('Expires: 0');
header('Cache-Control: must-revalidate');
header('Pragma: public');
header('Content-Length: ' . filesize("php://output"));
ob_clean();
flush();
readfile("php://output");
I'm not sure what to do. The CSV file downloads but displays nothing. I assume it has something to do with the ordering of my ob_clean() and flush() commands, but I'm not sure what's the best way to order these things.
Any help is appreciated.
You're doing a little too much. Create the script with the sole purpose of outputting the CSV. Just print it out directly to the screen. Don't worry about headers or buffers or php://output or anything like that yet.
Once you've confirmed that you're printing the data out to the screen appropriately, just add these headers at the beginning:
<?php
header("Content-disposition: attachment; filename=test.csv");
header("Content-Type: text/csv");
?>
... confirm that that downloads the file appropriately. Then you can add the other headers if you like (the headers I included above are those I've used myself without any extra cruft to get this working, the others are basically for efficiency and cache control, some of which may already be handled appropriately by your server, and may or may not be important for your particular application).
If you want, use output buffering with ob_start() and ob_get_clean() to get the output contents into a string which you can then use to fill out Content-Length.
As mentioned in my comments of Edson's answer, I expected a "headers already sent" warning at the last line of code:
header('Content-Length: '.$streamSize);
since output is written before this header is sent, but his example works ok.
Some investigation leads me to to following conclusions:
At the time you use an output buffer (weither a user one, or the
default PHP one), you may send HTTP headers and content the way you
want. You know that any protocol require to send headers before body
(thus the term "header"), but when you use an ouput buffer layer, PHP
will take care of this for you. Any PHP function playing with output
headers (header(), setcookie(), session_start()) will in fact use the
internal sapi_header_op() function which just fills in the headers
buffer. When you then write output, using say printf(), it writes into
the output buffer (assuming one). When the output buffer is to be
sent, PHP starts sending the headers first, and then the body. PHP
takes care of everything for you. If you dont like this behavior, you
have no other choice than disabling any output buffer layer.
and
The default size of the PHP buffer under most configurations is 4096
bytes (4KB) which means PHP buffers can hold data up to 4KB. Once this
limit is exceeded or PHP code execution is finished, buffered content
is automatically sent to whatever back end PHP is being used (CGI,
mod_php, FastCGI). Output buffering is always Off in PHP-CLI.
Edson's code works because the output buffer did not automatically get flushed because it doesn't exceed the buffer size (and the script isn't terminated obviously before the last header is sent).
As soon as the data in the output buffer exceeds the buffer size, the warning will be raised. Or in his example, when the data of
$get_users_stmt->fetch(PDO::FETCH_ASSOC)
is too large.
To prevent this, you should manage the output buffering yourself with the ob_start() and ob_end_flush(); like below:
// Turn on output buffering
ob_start();
// Define handle to output stream
$basic_info = fopen("php://output", 'w');
// Define and write header row to csv output
$basic_header = array('Header1', 'Header2');
fputcsv($basic_info, $basic_header);
$count = 0; // Auxiliary variable to write csv header in a different way
// Get data for remaining rows and write this rows to csv output
while($user_row = $get_users_stmt->fetch(PDO::FETCH_ASSOC)) {
if ($count == 0) {
// Write select column's names as CSV header
fputcsv($basic_info, array_keys($user_row));
} else {
//Write data row
fputcsv($basic_info, $user_row);
}
$count++;
}
// Get size of output after last output data sent
$streamSize = ob_get_length();
//Close the filepointer
fclose($basic_info);
// Send the raw HTTP headers
header('Content-Type: text/csv');
header('Content-Disposition: attachment; filename=test.csv');
header('Expires: 0');
header('Cache-Control: no-cache');
header('Content-Length: '. ob_get_length());
// Flush (send) the output buffer and turn off output buffering
ob_end_flush();
You're still bound to other limits, though.
I did some tweaks to your code.
Moved headers before any output, as suggested by PHP's doc;
Remember that header() must be called before any actual output is
sent, either by normal HTML tags, blank lines in a file, or from PHP
Removed some headers that didn't make much of a change;
Commented another option of writing csv header using the select column's names;
Now content length works;
There is no need to echo $basic_info as it is already at output buffer and we redirected it inside a file through headers;
Removed # (PHP Error Control Operator) as it may cause overhead, don't have a link to show you right now but you might find it if you search. You should think twice before silencing errors, most times it should be fixed instead of silenced.
header('Content-Type: text/csv');
header('Content-Disposition: attachment; filename=test.csv');
header('Expires: 0');
header('Cache-Control: no-cache');
$basic_info = fopen("php://output", 'w');
$basic_header = array(HEADER_ITEMS_IN_HERE);
fputcsv($basic_info, $basic_header);
$count = 0; // auxiliary variable to write csv header in a different way
while($user_row = $get_users_stmt->fetch(PDO::FETCH_ASSOC)) {
// Write select column's names as CSV header
if ($count == 0) {
fputcsv($basic_info, array_keys($user_row));
}
fputcsv($basic_info, $user_row);
$count++;
}
// get size of output after last output data sent
$streamSize = ob_get_length();
fclose($basic_info);
header('Content-Length: '.$streamSize);
I'm currently building a script that will allow a user to download a file via a URL without actually seeing the filename or where the file is stored. So far I have everything built out, but I need to know how I would go about calling the file to open and download. I currently have a working version (code below), but for some reason the PHP is corrupting the download. Everytime I try to open a file that downloads to my desktop I get a corrupt error message. When I open the same file on the server itself, the file works just fine.
URL Structure:
http://www.example.com/download/file/cjVQv0ng0zr2
Code that initiates the download
$fullpath = BASE_PATH . '../uploads/brochures/' . $vendors['0']['filename'];
header("Content-type: application/pdf");
header('Content-disposition: attachment; filename="' . $fullpath . '"');
Am I doing something wrong that would cause the file to become corrupt? Am I missing a header or two?
Thanks in advance,
Jake
You need to call the following line after sending the header.
readfile($fullpath);
and also adjust in the header like this:
header('Content-disposition: attachment; filename="' . basename($fullpath) . '"');
One thing i am not sure about is the $fullpath .. try to see if the $fullpath you have is correct and you can actually reach the file, this needs to be the full physical path of the file.
I think it would also be a good idea to add the following header as well:
header("Content-Transfer-Encoding: binary");
I had a similar issue a while back. Make sure you don't have any extra whitespace in your script file, either before the "<?php" tag or after the "?>" tag. In my case the last character of my script was "\n" instead of the expected ">".
I had faced the same problem sometime back, following worked for me; put a
while( #ob_end_clean() );
just before header functions:
header("Content-Type: ". $row['p_mime']);
header("Content-Length: ". $row['p_size']);
header("Content-Disposition: inline; filename=".$row["p_name"]);
Content-disposition: attachment/inline has to be set according to cases (1. prompt for download / 2. open in browser)
NOTE: Take care that you are not echoing and value before the header function, and being over cautious will not do any harm, silent out all the function before header function which you think would fail or spawn a warning message prefixing "#" symbol to those lines of php code.
all the best :)
Make sure you exit...
(i'm using a blob)
header("Content-Type: " . $response['content_type'] );
header("Cache-Control: maxage=1");
header("Pragma: public"); //fixes ie bug
echo trim($_data);
exit();