I am using the following code to download files that are stored outside of the public folder.
$mime_type = mime_content_type("{$_GET['file']}");
define("IMG_LOC","/var/www/domain.com/upload/");
$filename = $_GET['file'];
header('Content-Description: File Transfer');
header('Content-Type: '.$mime_type);
header('Content-Disposition: attachment; filename='.basename(IMG_LOC.$filename));
header('Expires: 0');
header('Cache-Control: must-revalidate');
header('Pragma: public');
header('Content-Length: ' . filesize($filename));
readfile($filename);
exit;
The problem is, file downloaded using this script is not usable. Excel is opening empty, powerpoint tells "there is an error reading" and word tells its missing a converter. Whereas, if I download the same files using ftp and open them manually, the files open properly, showing that the files are not corrupt.
For info, this is getting called from another page as : file.php?file='. $filename
Any help will be welcome. Thanks for your time.
You seem to be missing the path to your file:
header('Content-Length: ' . filesize(IMG_LOC . $filename));
readfile(IMG_LOC . $filename);
You should also add validation for the filename to avoid security problems.
If you still have a problem, you should also check the exact output of the script, perhaps there are php warnings or messages before your file.
I'm deducing that $filename is not the absolute path to the file you're seeking and hence why you define the IMG_LOC constant with a path. It's clear from there that filesize($filename)and readfile($filename) will not likely give you what you want.
Try concatenating the constant before the $filename variable like so...
header('Content-Length: ' . filesize(IMG_LOC . $filename));
readfile(IMG_LOC . $filename);
Also, consider that this code is susceptible to header-injection attacks as well as other security issues such as the user supplying you with a filename on your server that you may not want them to see. For example if I call your script with the query string ?file=yourscript.php I will be able to download your actual PHP code and potentially see any sensitive information you might not want exposed like your database password, or worse.
Also, mime_content_type is a deprecated function and should be replaced with the Fileinfo extension instead.
You script has various issues which all in all will prevent it from properly working. I roughly go through the lines and leave some comments, write a little summary then and offer another code-example with the comments incorporated:
$mime_type = mime_content_type("{$_GET['file']}");
You don't need to wrap the $_GET superglobal in curly brackets and then into double quotes. It's just not necessary for that parameter. You seem to be distracted at this point.
Anyway, this mime-type thing isn't necessary as the mime-type is not interesting if you want to offer the download. You take application/octet-stream instead and you can take care later on for a more specific mime-type:
$mime_type = "application/octet-stream";
Then at the wrong position you define the IMG_LOC constant:
define("IMG_LOC", "/var/www/domain.com/upload/");
This belongs at the very top of the script instead as you define the configuration by that.
In the line:
$filename = $_GET['file'];
you don't do any further error checking this opens up your script to directory traversal and path injection attacks which actually turns the script as you have it into a backdoor. Any file the script has access to on that server can be downloaded.
The next two lines are more or less correct then:
header('Content-Description: File Transfer');
header('Content-Type: '.$mime_type);
For the next header:
header('Content-Disposition: attachment; filename='.basename(IMG_LOC.$filename));
I would extract the basename earlier and just pass a variable here. Same for the content-length header later:
header('Content-Length: ' . filesize($filename));
Then you have this block of caching headers, as you serve the file from disk I don't think those are actually necessary, so I would remove them:
header('Expires: 0');
header('Cache-Control: must-revalidate');
header('Pragma: public');
The readfile line seems ok, you could do some error checking however:
readfile($filename);
And the last line I don't understand, as the script is at the end anyway, why exit?
exit;
My suggestions after this little review:
Gather the information which files should be served and how they must be named. Gathering such information will allow you to close the directory traversal issue which you have to close first.
Second putting the logic part above the output (and the configuration above the logic) should allow you to order the script in a more useful manner allowing you to handle issues with the mime-type for example easier when you maintain the script (or the caching if it is really an issue).
<?php
/**
* download a file
*
* parameter:
*
* file - name of the relative to upload folder
*/
const IMG_LOC = "/var/www/domain.com/upload";
// validate filename input
if (!isset($_GET['file'])) {
return;
}
$filename = $_GET['file'];
$path = realpath(IMG_LOC . '/' . $filename);
if (0 !== strpos($path, IMG_LOC)) {
return;
}
if (!is_readable($filename)) {
return;
}
// obtain data
$basename = basename($filename);
$mime_type = "application/octet-stream"; # can be improved later
$size = filesize($path);
// output
header('Content-Description: File Transfer');
header('Content-Type: ' . $mime_type);
header('Content-Disposition: attachment; filename=' . $basename);
header('Content-Length: ' . $size);
readfile($filename);
Related
I'm at a bit of a loss as to why this folder is not being found. I have a script that, after searching a database to find the $filename of someone's purchase based on a stored random code, should simply return their file. My code looks like this (including the trailing end of the db query):
$stmt_2 -> bind_result($filename);
$stmt_2 -> fetch();
$stmt_2 -> close();
// For .zip files
$filepath='/media-files/Label/' . $filename;
if (headers_sent()) {
echo 'HTTP header already sent';
} else {
if (!is_file($filepath)) {
header($_SERVER['SERVER_PROTOCOL'].' 404 Not Found');
echo 'File not found.';
} else if (!is_readable($filepath)) {
header($_SERVER['SERVER_PROTOCOL'].' 403 Forbidden');
echo 'File not readable.';
} else {
header('Content-Type: application/zip');
header('Content-Disposition: attachment; filename="' . basename($filepath) . '"');
header('Content-Length: ' . filesize($filepath));
readfile($filepath);
exit;
}
}
When I run this code, I receive "File not found." so !is_file($filepath) is where it is getting tripped up -- However, the path is correct and the zip is definitely there, so I'm not sure what is wrong here.
In terms of debugging, I've tried removing the checks, going directly to the headers and readfile, which returns an empty zip folder. What does work is if I navigate directly to the file by URL...
UPDATE
The file path issue has been fixed, but I am still not able to download the file. In all attempts I get either ERR_INVALID_RESPONSE or if I try to brute force download the file, it returns an empty file. I tried using these headers with no success:
header_remove();
ob_end_clean();
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');
header('Pragma: public');
header('Content-Length: ' . filesize($filepath));
readfile($filepath);
ob_end_flush();
exit;
They are large audio files, which appears to be causing the issue...
You have two types of pathes:
(a) The path of an URL. You have a web-adress which defines the root of your webpage.
e.g. https://www.stackoverflow.com is the start of the site. If you adress /questions at this site you always have the path https://www.stackoverflow.com/questions
(b) The path of the drive where the webpage is located. It is the filesystem-root.
e.g. /home/httpd/html/MyWebPage/questions
If you try to use /questions in (b) it will fail because you need the whole path.
So, this said you need to know where '/media-files/Label/'.$filename is located. It seems to me that /media-files is not at root-level of your filesystem (b).
Maybe it is at the web-root but this is not enough for your system to find the file. Therefore you need something like this:
'/root/httpd/MyWebPage/media-files/Label/'.$filename
Nico Haase was absolutely correct, this is an issue with misunderstanding of paths. Here is a link to an article that should clear things up:
https://phpdelusions.net/articles/paths
Currently your script is trying to find the file in:
/media-files/Label/file.zip
not:
/var/www/myproject/media-files/Label/file.zip
The linked article should provide you with all the neccesary information.
TLDR;
use:
$filepath=$_SERVER['DOCUMENT_ROOT'].'/media-files/Label/' . $filename;
UPDATE
With the file size issue it might be that PHP runs out of allowed memory when trying to load the whole file. We could try something like:
flush();
$file = fopen($filepath, "r");
while(!feof($file)) {
// send the current file part to the browser
print fread($file, round(10 * 1024));
// flush the content to the browser
flush();
}
fclose($file);
There are some issues with flush() but it's a good shot I think. You can have a read on: https://www.php.net/manual/en/function.flush
Other then that there is always the possibility to split the file into smaller chunks.
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
Problem:
After download, the file doesn't contain the data.
i.e it become blank.
So please help me for this.
<?php
session_start();
include_once 'oesdb.php';
$id=$_REQUEST['id'];
if(isset($_REQUEST['id']))
{
$sql=executeQuery("SELECT * FROM file where id=$id");
$rows = mysql_fetch_array($sql);
$file =$rows['file'];
header('Content-Description: File Transfer');
header('Content-Type: application/vnd.ms-excel');
header('Content-Disposition: attachment; filename='.basename($file));
header('Content-Transfer-Encoding: binary');
header('Expires: 0');
header('Cache-Control: must-revalidate');
header('Pragma: public');
header('Content-Length: ' . filesize($file));
ob_clean();
flush();
readfile('uploads/'.$file);
exit;
}
?>
Why not create a HTACCESS file in uploads folder then states
Allow From 127.0.0.1
Deny From All
Then just create a URL, use HTML5's new download feature, do something like this:
click to download
It saves time trying to use PHP to make a download script.
try replacing this:
$file =$rows['file'];
by this:
$file = "uploads/".$rows['file'];
and this:
readfile('uploads/'.$file);
by this
readfile($file);
if still not working put the value returned by the readfile function
IMPORTANT
Please take in consideration the sql injection issues (see comment of Ondřej Mirtes)
The problem is here:
header('Content-Length: ' . filesize($file));
Content-Length receives zero value and browser downloads zero-length file, as you told him. If $file is path relative to upload/, you should do this:
header('Content-Length: ' . filesize('upload/'.$file));
Be sure that filezise() returns correct size and readfile() realy outputs it.
But the other problem is that you mentioned UPLOAD folder and using uploads. They are not same and case is important. Also, may be using relative paths in 'uploads/'.$file is not a good idea, it is better to use absolute path. For example, '/var/www/upload/'.$file.
I have a script which automatically downloads a file.
It works perfectly to download the file, but the problem is that 50% or more of the time, it downloads a corrupt file.
Usually deleting and downloading again works, but not always.
How can I make this download 100% of the time perfectly always, not corrupted?
The file size changes depending on the file being downloaded.
<?php
// Automatically Start File Download
if (isset($_GET['filename'])):
$filename = $_GET['filename'];
$domain = "http://www.domain.com";
$filepath = "/addons/downloads/websites/";
//BUILD THE FILE INFORMATION
$file = $domain . $filepath . $filename;
// echo $filepath . $filename;
// echo $file;
//CREATE/OUTPUT THE HEADER
if (file_exists("/home/unrealde/public_html/ebook/domain.com/".$filepath . $filename)):
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);
else:
$errorMsg = "<b>Download Error: File $filename Doesnt Exist!</b> <br />Please Contact <a href='mailto:support#domain.com'>support#domain.com</a>";
endif;
echo $errorMsg;
else:
// don't download any file
endif;
?>
My hunch is that something in your program is outputting some data other than the file itself.
Have you looked at the corrupt file in a binary editor and compared it with a non-corrupt version? What you'll find is that either at the beginning or the end of the file, you have some unexpected data, and this is what is corrupting the file.
If you look that file this way, it may become very obvious what the problem is. For example, you may have the file, followed by an error message, in which case maybe your line echo $errorMsg; is the culprit.
Alternatively you may have some blank space. This could also be the same error message, or it could be that your PHP tags have blank lines above or below them, which are being printed.
My first suggestion would be, since the program is effectively finished when the file is output, to put an explicit die; function immediately after the readfile(); line. This will categorically prevent any further spurious data being output once the file has been sent.
That won't help if the bad data is being sent before the readfile();, but it does rule out half the possible problems in one swoop.
Can't you just tar/gzip/zip the contents and provide a tar/gzip/zip file for download instead ?
Smaller file transfer increase chances of success over http transfer,
and more importantly, you can provide checksum for user to verify against
Try adding error_reporting(0); at the beginning of the script. Just for fun. If you check php.net for readfile, others have reported that this helps.
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