PHP: headers already sent when using fwrite but not when using fputcsv - php

I know the theory behind this error however it is now driving me crazy again. I'm using Tonic in my application. With this library you redirect all traffic to your dispatch.php script which then executes the appropriate Resource and that Resource return a Response which is displayed (output) by dispatch.php.
The output method of Response look like this:
/**
* Output the response
*/
public function output()
{
foreach ($this->headers as $name => $value) {
header($name.': '.$value, true, $this->responseCode());
}
echo $this->body;
}
So AFAIK this tells us that you can't write anything to php output in your Resource.
I now have a Resource that dynamically generates a csv from an input csv and outputs it to the browser (it converts 1 column of the data to a different format).
$csv = fopen('php://output', 'w');
// sets header
$response->__set('contentDisposition:', 'attachment; filename="' . $fileName . '"');
while (($line = fgetcsv($filePointer, 0, ",", '"')) !== FALSE) {
// generate line
fputcsv($csv, $line);
}
fclose($filePointer);
return $response;
This works 100% fine. No issue with headers and the correct file is generated and offered for download. This is already confusing because we are writing to the output before headers are set? What does fputcsv actually do?
I have a second resource that does a similar thing but it outputs a custom file format (text file).
$output = fopen('php://output', 'w');
// sets header
$response->__set('contentDisposition:', 'attachment; filename="' . $fileName . '"');
while (($line = fgetcsv($filePointer, 0, ",", '"')) !== FALSE) {
// generate a record (multiple lines) not shown / snipped
fwrite($output, $record);
}
fclose($filePointer);
return $response;
The only difference is that it uses fwrite instead of fputcsv and bang
headers already sent by... // line number = fwrite()
This is very confusing! IMHO it should actually fail in both cases? Why does the first one work? how can I get the second one to work?
(I can generate a huge string containing the file and put that into the responses body and it works. However files could be rather big (up to 50 mb) and hence want to avoid this.)

$record is not set, generating an error of level NOTICE. If you have error_reporting to true, PHP will put this error in the output before sending the headers.
Set error_reporting to false and keep an eye on your logs instead.

Here my solution. I'm not going to mark it as answer for a while because maybe someone comes up with something better (simpler) than this.
First a comment about fwrite and fputcsv:
fputcsv has a complete different source and not much in common with fwrite (it does not call fwrite internally, it's a separate function in C source code). Since I don't know C I can't tell why they behave differently but they do.
Solution:
The generated files can be "large" depending on input and hence generating the whole file by string concatenation and keeping it in memory isn't a great solution.
I googled a bit and found mod_xsendfile for apache. This works by setting a custom header in php containing the path to the file to be sent to the user. The mod then removes that custom header and sends the file as response.
The problem with mod_xsendfile is that it is not compatible with mod_rewrite, which I use too. You will get 404 errors. To solve this you need to add
RewriteCond %{REQUEST_FILENAME} !-f
to the according place in apache config (don't rewrite if the request is for an actual physically existing file). However that's not enough. You need to set the header X-Sendfile in a php script that was not rewritten, is an actual existing php file.
So in the \Tonic\Resource class generating the file at the end I redirect to an above outlined script:
$response->__set('location', $url . "?fileName=" . urlencode($fileName));
$response->code = \Tonic\Response::FOUND;
return $response;
In the download script we redirect to in above snippet just do (validation stuff omitted):
$filePath = trim($_GET['fileName']);
header ('X-Sendfile: ' . $filePath);
header ('Content-Disposition: attachment; filename="' . $filePath . '"');
and the browser will display a download dialog for the generated file.
You will also need to create cron job to delete the generated files.
/usr/bin/find /path/to/generatedFiles/ -type f -mmin +60 -exec rm {} +
this will delete all files older than 60 min in directory /path/to/generatedFiles.
I use ubuntu server so you can add it to the file
/etc/cron.daily/standard
or generated a new file in that directory or generated a new file in /etc/cron.hourly containing that command.
Note:
I name the generated files after the sha1 hash of the input csv file so the name is unique and if someone repeats the same requests several times in a short period you can just return the already generated file a second time.

Related

PHP link/request to download file then delete it immediately

I face a case I never did, and I dont know how to properly do it.
I have a php script which generate files for clients. At the end of the script, I echo the path for them to download the file, simply.
How can I do to provide the file - or the path or any what - for downloading it, and be sure to delete the file once downloaded.
Widely, I'd like to make the file available for one/unique download only. How to ?
EDIT
I cannot use headers
There are a few components to getting this to work. Without knowing which framework you use, I'll use comments as placeholders.
There is no way to do it without using the header function, though.
Here is the source for a file that outlines the process:
<?php
$fileid = $_GET['fileid'];
$key = $_GET['key'];
// find the file in the database, and store it in $file
if ($keyMatches) {
// it is important for security to only use file paths from the database
$actualPath = $file->getPathOnDisk();
$fileInfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($fileInfo, $actualPath);
$fp = fopen($actualPath, 'rb');
header("Content-Type: " . $mime);
header("Content-Length: " . filesize($actualPath));
fpassthru($fp);
}
else
{
http_response_code(403); // forbidden
}
You'll use this by linking to download.php?fileid=1234&key=foobar, and generating the URL at the same time you generate the key and store it in the database.
For security, you'll keep the files outside of the web root, meaning they cannot be accessed through the web server without going through a script.
fpassthru is reasonably fast, and will not likely have a performance impact.
You must do a download file gateway, like download.php?id=XXX
Where XXX is the unique ID of each file you will store in DB. And of course, the file to be downloaded.
Then, each time a user will visit the page, you can :
- Check if he has already downloaded the file
- If no, redirect it to the real path of file
- If yes, display 403 message.
When a user download a file, update the DB, generate or copy the file to a new name, you play with headers, and delete file upon download or after a small timeout.

php get file content of downloadable file

Coult not find any similar problem solved on the web, so here's my situation:
I have a .jsp "webpage" that generates a .csv file based on specific parameters.
As an example, if I use my browser to open the site, I type in:
redownloadsubmitter.jsp?id=225&batch_id=2013_11_20&orgshort=NEP
The script then uses the data in the query string and generates the matching .csv file, named: NEP_DETAILS_2013_11_20.csv
Now what I want is to not manually having to use my browser, open the script and download the file to my local harddrive. Instead I want to use a PHP script that grabs the content and then can further format it, based on my needs.
I thought about the following code, but that did not work. Instead it returns nothing, empty website when I try it..
$download = file_get_contents('redownloadsubmitter.jsp?id=225&batch_id=2013_11_20&orgshort=NEP');
echo $download;
Any other ideas?
NOTE: just in case someone has this question: I have no access to the .jsp file and I therefore cannot change how it operates.
file_get_contents() isn't smart and doesn't know that's a URL you're passing in. It's trying to literally open a local file whose name is redownloadsubmitted.jsp.etc......
If you want f_g_c() to do an HTTP operation, then you'll have to include a full-blown URL:
$download = file_get_contents('http://example.com/redownloadsubmitter.jsp etc....');'
Try this code for download file.
<?php
/**
* $filename filename in server
* $downloadname filename when download file
*/
$filename = __FILE__;
$dowloadname = 'PHPDownload.php';
Header("content-type:application/octet-stream");
Header("Accept-Ranges: bytes");
Header("Accept-Length: ".filesize($filename));
Header("Content-Disposition: attachment; filename=".$dowloadname);
if(file_exists($filename) && $fp=fopen($filename,"r")) //file exists and open it
{
echo fread($fp,filesize($filename)); //read write to the browser
fclose($fp);
}
//End_php

Is it possible to read from protected directory using only PHP/HTML?

I have a directory containing data that should not be world-accessible until a certain date.
The directory, naturally, should not be directly world-readable with a web browser. I currently solve this with .htpasswd and .htaccess.
However, there is a world-readable .php file one directory level up. The PHP file, based on the date, conditionally generates basic HTML tags (e.g., <img .../>) that read from the protected directory.
Unfortunately, in my tests, the .php file requires authentication to load the data. My question is whether I'm trying to do something fundamentally impossible, or whether I can tweak it to make it work. Also, if it is possible, are there any additional issues (security or otherwise) that I should know about?
Additional information:
If possible, I would prefer not to use Javascript.
PHP 5.3 is available.
Any other ideas for a solution (I already thought of a cron-job, which I might yet do)?
I'm guessing a problem you might have is if you try to output <img src="protected.jpg" /> even from an unprotected php file, you'll be able to show the HTML but NOT the image file itself.
If i understand correctly what you're trying to do, you need either :
to write some kind of proxy script in PHP, in order to control access to each file (this is a bit tedious and requires generating the right headers + mime types).
to control access directly from .htaccess using time/date conditions, which might be your best option. see there : http://www.askapache.com/htaccess/time_hour-rewritecond-time.html
Edit : proxy example : i can't seem to find an example online so this is a function i often use when i wish to control access to a file from PHP (for instance this can be sensitive data whose access needs to be verified from $_SESSION or DB values) :
function send_binary_data($path, $mimetype, $filename = null){
#ob_clean();
if($filename === null) $filename = basename($path);
$size = filesize($path);
//no-cache
header('Cache-Control: no-cache, must-revalidate, public');
header('Pragma: no-cache');
//binary file
header('Content-Transfer-Encoding: binary');
//mimetype
header('Content-Type: ' . $mimetype);
header('Content-Length: ' . $size);
header('Content-Disposition: inline; filename=' . $filename);
header('Content-Description: ' . $filename);
$chunksize = 1 * (1024 * 1024);
$buffer = '';
$handle = fopen($path, 'rb');
if ($handle === false) {
return false;
}
while (!feof($handle)) {
$buffer = fread($handle, $chunksize);
print $buffer;
}
$result = fclose($handle);
unset($handle);
$handle = null;
die();
}
Of course you still need to restrict direct access from .htaccess but in case of a proxy you'll redirect all requests to your unprotected proxy script, like this :
RewriteEngine ON
RewriteRule ^(.*)$ /proxy.php?file=$1 [NC,QSA]
And proxy.php would contain something like :
if(!isset($_GET['file'])) die('file not set');
$file = $_GET['file'];
//perform all your custom checking, including security checks due to retrieving data from $_GET, and if access is granted :
$path = 'yourpath/'.$file;
$mimetype = 'defineregardingyour/ownrules';
send_binary_data($path, $mimetype);
.htaccess only serves as an access control directly from the Internet to the directory.
PHP access is controlled by the chmod permissions. Try chmodding it to 755. You can still put password-protection or any other kind of protection on it with the .htaccess-file.
Considering the added comments, I assume you're trying to include images in your output that are inside the protected directory. Naturally, an unauthenticated user cannot access them... Why else would you have protected them?
You can add the files that need to be world-accessible to your .htaccess file.

How to return a database dump as a download

I am working on a function for a PostgreSQL database, that when client issues a database dump, the dump is offered as a download. This snapshot could then later be used to restore the database with. However, I can't seem to figure out how to do it. When the user presses the button, an AJAX call to the server is made, as to which the server executes the following code:
if($_POST['command'] == 'dump'){
$dump = $table->Dump();
header("Content-type: application/octet-stream");
header('Content-Disposition: attachment; filename=/"'.$dump.'/"');
}
Where $table->Dump() looks like this:
public function Dump(){
$filename = dirname(__FILE__)."/db_Dump.out";
exec("pg_dump ".$this->name." > $filename");
return $filename;
}
The dump isn't made though. Any tips on this?
This approach however, doesn't work. I thought that setting the headers would be enough to cause a download, but apparently I was wrong. So what would be the correct way of creating a download?
Edit 1, #stevevls:
if($_POST['command'] == 'dump'){
$dump = $table->Dump();
$fh = fopen($dump, 'r') or die("Can't open file");
header("Content-type: application/octet-stream");
header('Content-Disposition: attachment; filename=/"'.$dump.'/"');
$dumpData = fread($fh, filesize($fh));
fclose($fh);
echo $dumpData;
}
I still don't get anything as a download though.
Edit 2, #myself
I have been able to get a return value, it seemed that the the check if the command given was 'dump' was never reached. I fixed that, and now I get an error on the pg_dump command. I now get
sh: cannot create ../database/db_Dump.sql: Permission denied
I bet this is due to php not being allowed to run pg_dump, but how could I get the system to allow it to be able to run it?
Edit 3, #myself
After resolving the issue with the pg__dump (I added www-data, Apaches user on my system, to the sudoers list, which resoved the issue. Also setting the correct permissions on the directory to write to is handy aswell.) I now get the db_Dump.sql as plain text instead of a save as dialog. Any ideas on that?
first of all check if dump file was created on disc.
Second, check if your PHP script has not reached time limit, because making dump can last long.
Third, you want to read whole dump into memory? You can easly reach memory limit, so do it part-by-part. On php.net you have example in fread manual:
$handle = fopen("http://www.example.com/", "rb");
$contents = '';
while (!feof($handle)) {
$contents .= fread($handle, 8192);
}
fclose($handle);
Turns out, it was all due to the fact of how I requested the download. It seems that it is impossible to get a download when you request it via Ajax, as the returned file get's accepted in the success method of the call. After Changing this to a direct link to the file, I was able to get a download.

How can I optimize this simple PHP script?

This first script gets called several times for each user via an AJAX request. It calls another script on a different server to get the last line of a text file. It works fine, but I think there is a lot of room for improvement but I am not a very good PHP coder, so I am hoping with the help of the community I can optimize this for speed and efficiency:
AJAX POST Request made to this script
<?php session_start();
$fileName = $_POST['textFile'];
$result = file_get_contents($_SESSION['serverURL']."fileReader.php?textFile=$fileName");
echo $result;
?>
It makes a GET request to this external script which reads a text file
<?php
$fileName = $_GET['textFile'];
if (file_exists('text/'.$fileName.'.txt')) {
$lines = file('text/'.$fileName.'.txt');
echo $lines[sizeof($lines)-1];
}
else{
echo 0;
}
?>
I would appreciate any help. I think there is more improvement that can be made in the first script. It makes an expensive function call (file_get_contents), well at least I think its expensive!
This script should limit the locations and file types that it's going to return.
Think of somebody trying this:
http://www.yoursite.com/yourscript.php?textFile=../../../etc/passwd (or something similar)
Try to find out where delays occur.. does the HTTP request take long, or is the file so large that reading it takes long.
If the request is slow, try caching results locally.
If the file is huge, then you could set up a cron job that extracts the last line of the file at regular intervals (or at every change), and save that to a file that your other script can access directly.
readfile is your friend here
it reads a file on disk and streams it to the client.
script 1:
<?php
session_start();
// added basic argument filtering
$fileName = preg_replace('/[^A-Za-z0-9_]/', '', $_POST['textFile']);
$fileName = $_SESSION['serverURL'].'text/'.$fileName.'.txt';
if (file_exists($fileName)) {
// script 2 could be pasted here
//for the entire file
//readfile($fileName);
//for just the last line
$lines = file($fileName);
echo $lines[count($lines)-1];
exit(0);
}
echo 0;
?>
This script could further be improved by adding caching to it. But that is more complicated.
The very basic caching could be.
script 2:
<?php
$lastModifiedTimeStamp filemtime($fileName);
if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
$browserCachedCopyTimestamp = strtotime(preg_replace('/;.*$/', '', $_SERVER['HTTP_IF_MODIFIED_SINCE']));
if ($browserCachedCopyTimestamp >= $lastModifiedTimeStamp) {
header("HTTP/1.0 304 Not Modified");
exit(0);
}
}
header('Content-Length: '.filesize($fileName));
header('Expires: '.gmdate('D, d M Y H:i:s \G\M\T', time() + 604800)); // (3600 * 24 * 7)
header('Last-Modified: '.date('D, d M Y H:i:s \G\M\T', $lastModifiedTimeStamp));
?>
First things first: Do you really need to optimize that? Is that the slowest part in your use case? Have you used xdebug to verify that? If you've done that, read on:
You cannot really optimize the first script usefully: If you need a http-request, you need a http-request. Skipping the http request could be a performance gain, though, if it is possible (i.e. if the first script can access the same files the second script would operate on).
As for the second script: Reading the whole file into memory does look like some overhead, but that is neglibable, if the files are small. The code looks very readable, I would leave it as is in that case.
If your files are big, however, you might want to use fopen() and its friends fseek() and fread()
# Do not forget to sanitize the file name here!
# An attacker could demand the last line of your password
# file or similar! ($fileName = '../../passwords.txt')
$filePointer = fopen($fileName, 'r');
$i = 1;
$chunkSize = 200;
# Read 200 byte chunks from the file and check if the chunk
# contains a newline
do {
fseek($filePointer, -($i * $chunkSize), SEEK_END);
$line = fread($filePointer, $i++ * $chunkSize);
} while (($pos = strrpos($line, "\n")) === false);
return substr($line, $pos + 1);
If the files are unchanging, you should cache the last line.
If the files are changing and you control the way they are produced, it might or might not be an improvement to reverse the order lines are written, depending on how often a line is read over its lifetime.
Edit:
Your server could figure out what it wants to write to its log, put it in memcache, and then write it to the log. The request for the last line could be fulfulled from memcache instead of file read.
The most probable source of delay is that cross-server HTTP request. If the files are small, the cost of fopen/fread/fclose is nothing compared to the whole HTTP request.
(Not long ago I used HTTP to retrieve images to dinamically generate image-based menus. Replacing the HTTP request by a local file read reduced the delay from seconds to tenths of a second.)
I assume that the obvious solution of accessing the file server filesystem directly is out of the question. If not, then it's the best and simplest option.
If not, you could use caching. Instead of getting the whole file, you just issue a HEAD request and compare the timestamp to a local copy.
Also, if you are ajax-updating a lot of clients based on the same files, you might consider looking at using comet (meteor, for example). It's used for things like chats, where a single change has to be broadcasted to several clients.

Categories