I'm trying to force a link to download a file as an attachment. This is stuff I have done before but something is going wrong and I'm not sure what it is. The file types I am trying to send as attachments are all mp4 files. Here is what I am doing:
<?php
if(isset($_GET['file']))
{
$file_path = dirname(__FILE__) . "/videos/$_GET[file]";
if(file_exists($file_path))
{
$file_url = "/videos/$_GET[file]";
header ("Content-type: video/mp4");
header("Content-Transfer-Encoding: binary");
header("Content-disposition: attachment; filename=\"".basename($_GET['file']) ."\"");
header("Content-Length: ".filesize($file_path));
echo readfile($file_url);
}
}
?>
As you can see I am checking to see if the file exists before trying to serve it. What happens is I get the file, it's type is correct(MP4 Video) but the file is only around 100 btyes in size. As I was debugging I tested to see if the filesize is correct, it is. I've changed the readfile() line to:
header("location:/videos/$_GET[file]");
and it takes me to the video. I've done a fair bit of Googling and I havn't come up with a reason for this behavoiur. Has anyone seen this type of thing before? Any ideas on what could be causing this? Any help would be very much appreciated, thanks much!
If you open the contents of that file you downloaded in a text editor, you will no doubt see an error message. This is probably because your path starts with /. When you open files from disk, the doc root of your server path is meaningless. You need to specify the real path.
Also, your script is terribly insecure! Anyone can download any file they want off your server. Make sure to check that the file is within the doc root before serving it up. Never let a user just specify any file they want without restriction. You don't want some doing ?file=../../../etc/passwd.
Use these functions, it help you.
function getRemoteFilesize($url)
{
$file_headers = #get_headers($url, 1);
if($size =getSize($file_headers)){
return $size;
} elseif($file_headers[0] == "HTTP/1.1 302 Found"){
if (isset($file_headers["Location"])) {
$url = $file_headers["Location"][0];
if (strpos($url, "/_as/") !== false) {
$url = substr($url, 0, strpos($url, "/_as/"));
}
$file_headers = #get_headers($url, 1);
return getSize($file_headers);
}
}
return false;
}
function getSize($file_headers){
if (!$file_headers || $file_headers[0] == "HTTP/1.1 404 Not Found" || $file_headers[0] == "HTTP/1.0 404 Not Found") {
return false;
} elseif ($file_headers[0] == "HTTP/1.0 200 OK" || $file_headers[0] == "HTTP/1.1 200 OK") {
$clen=(isset($file_headers['Content-Length']))?$file_headers['Content-Length']:false;
$size = $clen;
if($clen) {
switch ($clen) {
case $clen < 1024:
$size = $clen . ' B';
break;
case $clen < 1048576:
$size = round($clen / 1024, 2) . ' KiB';
break;
case $clen < 1073741824:
$size = round($clen / 1048576, 2) . ' MiB';
break;
case $clen < 1099511627776:
$size = round($clen / 1073741824, 2) . ' GiB';
break;
}
}
return $size;
}
return false;
}
usage:
echo getRemoteFilesize('https://stackoverflow.com/questions/14679268/downloading-files-as-attachment-filesize-incorrect');
Related
I am using this below code to stream a movie on my localhost. The movie which is lesser then 2GB works fine but when it comes to movie greater than 2gb then the movie doesn't play.
Please help me what should I do to read larger files.
This is the code I am using for streaming movie
<?php
//Determine file path according to extension
if (!isset($_GET['ext']) || $_GET['ext'] == 'mp4') {
$path ='movies/'.$_GET['movie'];
}
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($path);
header('Content-Type: ' . $mime);
$size = filesize($path);
if (isset($_SERVER['HTTP_RANGE'])) {
list($specifier, $value) = explode('=', $_SERVER['HTTP_RANGE']);
if ($specifier != 'bytes') {
header('HTTP/1.1 400 Bad Request');
return;
}
list($from, $to) = explode('-', $value);
if (!$to) {
$to = $size - 1;
}
$fp = fopen($path, 'rb');
if (!$fp) {
header('HTTP/1.1 500 Internal Server Error');
return;
}
header('HTTP/1.1 206 Partial Content');
header('Accept-Ranges: bytes');
header('Content-Length: ' . ($to - $from));
header("Content-Range: bytes {$from}-{$to}/{$size}");
fseek($fp, $from);
while(true){
if(ftell($fp) >= $to){
break;
}
echo fread($fp, 8192);
// Flush do buffer
ob_flush();
flush();
}
}
else {
header('Content-Length: ' . $size);
readfile($path);
}
I have hosted the files on localhost using xammpp as a server.
This could have different reasons.
fopen() reads a file into your RAM. So the limitation seems to come from there.
First, make sure you have min 4GB installed in your system.
If you need more also make sure you dont use a Win 32-Bit system since this only supports up to 4GB ram.
Then you can try to increase the memory_limit of PHP. Open the xampp/php/php.ini file and change the memory_limit to what ever you need OR set the ini value directly in your php file like this
ini_set(”memory_limit”,”16M”);
Hope this helps. For a more specific answer it would be helpful if you have any error message.
I use PHP to send download to the browser. I run the code on a file named download.php.
The problem: sometimes instead of download the file, it's downloads the same file (same size), but with the name download.php.
I also noticed that when I try to download it with Internet Download Manager, I see the name download.php for about half second, and then the name changes to the real name.
Pictures to explain:
The code:
//First, see if the file exists
if (!is_file($file)) {
header("HTTP/1.1 400 Invalid Request");
die("<h3>File Not Found</h3>");
}
//Gather relevent info about file
$size = filesize($file);
$fileinfo = pathinfo($file);
//workaround for IE filename bug with multiple periods / multiple dots in filename
//that adds square brackets to filename - eg. setup.abc.exe becomes setup[1].abc.exe
$filename = (isset($_SERVER['HTTP_USER_AGENT']) && strstr($_SERVER['HTTP_USER_AGENT'], 'MSIE')) ?
preg_replace('/\./', '%2e', $fileinfo['basename'], substr_count($fileinfo['basename'], '.') - 1) :
$fileinfo['basename'];
$file_extension = strtolower($fileinfo['extension']);
//This will set the Content-Type to the appropriate setting for the file
switch($file_extension)
{
case 'exe': $ctype='application/octet-stream'; break;
case 'zip': $ctype='application/zip'; break;
case 'mp3': $ctype='audio/mpeg'; break;
case 'mpg': $ctype='video/mpeg'; break;
case 'avi': $ctype='video/x-msvideo'; break;
default: $ctype='application/force-download';
}
//check if http_range is sent by browser (or download manager)
if($is_resume && isset($_SERVER['HTTP_RANGE']))
{
$arr = explode('=', $_SERVER['HTTP_RANGE'], 2);
if(isset($arr[1]))
list($size_unit, $range_orig) = $arr;
else list($size_unit) = $arr;
if ($size_unit == 'bytes')
{
//multiple ranges could be specified at the same time, but for simplicity only serve the first range
//http://tools.ietf.org/id/draft-ietf-http-range-retrieval-00.txt
$arr3 = explode(',', $range_orig, 2);
if(isset($arr3[1]))
list($range, $extra_ranges) = $arr3;
else
list($range) = $arr3;
}
else
{
$range = '';
}
}
else
{
$range = '';
}
//figure out download piece from range (if set)
$arr2 = explode('-', $range, 2);
if(isset($arr2[1]))
list($seek_start, $seek_end) = $arr2;
else
list($seek_start) = $arr2;
//set start and end based on range (if set), else set defaults
//also check for invalid ranges.
$seek_end = (empty($seek_end)) ? ($size - 1) : min(abs(intval($seek_end)),($size - 1));
$seek_start = (empty($seek_start) || $seek_end < abs(intval($seek_start))) ? 0 : max(abs(intval($seek_start)),0);
//add headers if resumable
if ($is_resume)
{
//Only send partial content header if downloading a piece of the file (IE workaround)
if ($seek_start > 0 || $seek_end < ($size - 1))
{
header('HTTP/1.1 206 Partial Content');
}
header('Accept-Ranges: bytes');
header('Content-Range: bytes '.$seek_start.'-'.$seek_end.'/'.$size);
}
//headers for IE Bugs (is this necessary?)
//header("Cache-Control: cache, must-revalidate");
//header("Pragma: public");
header('Content-Type: ' . $ctype);
header('Content-Disposition: attachment; filename="' . $filename . '"');
header('Content-Length: '.($seek_end - $seek_start + 1));
//reset time limit for big files
set_time_limit(0);
ignore_user_abort(true);
//open the file
$fp = fopen($file, 'rb');
//seek to start of missing part
fseek($fp, $seek_start);
//start buffered download
while(!feof($fp))
{
print(fread($fp, 1024*8));
flush();
ob_flush();
}
fclose($fp);
exit;
I found the solution!
I did an htaccess RewriteRule. For example, if the file id is a1a2, and the file name is foo.mp4, I changed the URL to: download/a1a2/foo.mp4.
htaccess code:
RewriteEngine on
RewriteRule ^download/(.*)/(.*)?$ download.php?id=$1&fileName=$2 [QSA,NC,L]
That simple!
You can do this by setting proper headers as explained in the official PHP manual here:
// We'll be outputting a PDF
header('Content-type: application/pdf');
// It will be called downloaded.pdf
header('Content-Disposition: attachment; filename="downloaded.pdf"');
That said looking at your code it seems like you already the header stuff covered here:
header('Content-Type: ' . $ctype);
header('Content-Disposition: attachment; filename="' . $filename . '"');
header('Content-Length: '.($seek_end - $seek_start + 1));
So the only thing I think could be the issue is related to the list mime-types you have in your code:
//This will set the Content-Type to the appropriate setting for the file
switch($file_extension)
{
case 'exe': $ctype='application/octet-stream'; break;
case 'zip': $ctype='application/zip'; break;
case 'mp3': $ctype='audio/mpeg'; break;
case 'mpg': $ctype='video/mpeg'; break;
case 'avi': $ctype='video/x-msvideo'; break;
default: $ctype='application/force-download';
}
It could be that the mime-type (aka in your code as: $ctype) you are setting is not recognized by the browser or the IDM download software you are using. So it is defaulting to application/force-download.
I would also highly recommend refactoring your switch to work like this using simple associative array logic like this:
// This will set the Content-Type to the appropriate setting for the file
$ctype_array = array();
$ctype_array['exe'] = 'application/octet-stream';
$ctype_array['zip'] = 'application/zip';
$ctype_array['mp3'] = 'audio/mpeg';
$ctype_array['mpg'] = 'video/mpeg';
$ctype_array['avi'] = 'video/x-msvideo';
// Check if the file extension is in $ctype_array & return the value. If not, send default.
$ctype = array_key_exists($file_extension, $ctype_array) ? $ctype_array[$file_extension] : 'application/force-download';
That way you can easily add more items to the $ctype_array without having to juggle switch/case logic.
I'm running a localhost mysql server with PhpMyAdmin version 4.2. I created a table with a MEDIUMBLOB column to store file. I can upload *.doc files correctly, but when I click the file link, it downloads it as a "tablename-columnname.bin" bin file.
Then I tried to manually rename this "tablename-columnname.bin" to "original_file.doc" and the doc can be opened correctly.
Question: Why phpmyadmin does not download the file as is? What can I do to fix it? I know a bit of php
Thanks!
Use the 'header' before echo the file
header('Content-type: application/msword');
for dynamic way to find the 'Content type' use to store the 'file name' with 'extension' in database.
To make it a bit dynamic (based on MIME) I did the following change:
In tbl_get_field.php file, last few lines:
$mime = PMA_detectMIME($result);
switch($mime){
case 'image/jpeg':
$extension = 'jpg';
case 'image/gif':
$extension = 'gif';
case 'image/png':
$extension = 'png';
case 'application/pdf':
$extension = 'pdf';
default:
$extension = 'bin';
}
PMA_downloadHeader(
$table . '-' . $_GET['transform_key'] . '.' . $extension,
$mime,
strlen($result)
);
echo $result;
Code above should output the file with correct extension instead of always ".bin".
In mime.lib.php file, we need to detect more MIME cases:
function PMA_detectMIME(&$test)
{
$len = strlen($test);
if ($len >= 2 && $test[0] == chr(0xff) && $test[1] == chr(0xd8)) {
return 'image/jpeg';
}
if ($len >= 3 && substr($test, 0, 3) == 'GIF') {
return 'image/gif';
}
if ($len >= 4 && substr($test, 0, 4) == "\x89PNG") {
return 'image/png';
}
return 'application/octet-stream';
}
I want to add pdf and word doc in this, but I dont know how to determine their MIME type base on the stream characters
I have a php script that streams an mp3 file to the browser. I was wondering if there was a way for the browser to cache the song once played and then if not, then stream the file. I have included the function that I use to stream the mp3 files. As well as the HTML in how I implement the php script. I use a .htaccess file to get the ID of the song and then stream that based one what song it is in a database. I was imaging something to do with 304 headers correct? If anyone could possibly guide me? Thanks.
PHP:
private function send_file($file, $name, $contenttype = "application/octet-stream") {
// Make sure the files exists, otherwise we are wasting our time
if (!file_exists($file)) {
error_log('file not found');
header("HTTP/1.1 404 Not Found");
exit;
}
// Get the 'Range' header if one was sent
if (isset($_SERVER['HTTP_RANGE'])) $range = $_SERVER['HTTP_RANGE']; // IIS/Some Apache versions
else if ($apache = apache_request_headers()) { // Try Apache again
$headers = array();
foreach ($apache as $header => $val) $headers[strtolower($header)] = $val;
if (isset($headers['range'])) $range = $headers['range'];
else $range = FALSE; // We can't get the header/there isn't one set
} else $range = FALSE; // We can't get the header/there isn't one set
// Get the data range requested (if any)
$filesize = filesize($file);
if ($range) {
$partial = true;
list($param,$range) = explode('=',$range);
if (strtolower(trim($param)) != 'bytes') { // Bad request - range unit is not 'bytes'
error_log('range unit not in bytes');
header("HTTP/1.1 400 Invalid Request");
exit;
}
$range = explode(',',$range);
$range = explode('-',$range[0]); // We only deal with the first requested range
if (count($range) != 2) { // Bad request - 'bytes' parameter is not valid
error_log('bytes range parameter is not valid');
header("HTTP/1.1 400 Invalid Request");
exit;
}
if ($range[0] === '') { // First number missing, return last $range[1] bytes
$end = $filesize - 1;
$start = $end - intval($range[0]);
} else if ($range[1] === '') { // Second number missing, return from byte $range[0] to end
$start = intval($range[0]);
$end = $filesize - 1;
} else { // Both numbers present, return specific range
$start = intval($range[0]);
$end = intval($range[1]);
if ($end >= $filesize || (!$start && (!$end || $end == ($filesize - 1)))) $partial = false; // Invalid range/whole file specified, return whole file
}
$length = $end - $start + 1;
} else $partial = false; // No range requested
// Send standard headers
header("Content-Type: $contenttype");
header("Content-Length: $filesize");
header('Content-Disposition: attachment; filename="'.$name.'.mp3"');
header('Accept-Ranges: bytes');
// if requested, send extra headers and part of file...
if ($partial) {
header('HTTP/1.1 206 Partial Content');
header("Content-Range: bytes $start-$end/$filesize");
if (!$fp = fopen($file, 'r')) { // Error out if we can't read the file
error_log('can\'t read the file');
header("HTTP/1.1 500 Internal Server Error");
exit;
}
if ($start) fseek($fp,$start);
while ($length) { // Read in blocks of 8KB so we don't chew up memory on the server
$read = ($length > 8192) ? 8192 : $length;
$length -= $read;
print(fread($fp,$read));
}
fclose($fp);
} else readfile($file); // ...otherwise just send the whole file
// Exit here to avoid accidentally sending extra content on the end of the file
exit;
}
HTML:
<audio src="/media/play/12345" loop />
I use a php script to check if a user is logged in before serving images or videos. The actual files are stored in a folder that is not able to be accessed directly. If the authentication is successful, the php script will relay/output the file. My goal is to have the file served through the php script behave as closely as possible as a direct link to the actual file.
So, here's the deal. Images work fine. Videos (mp4) work with a few caveats. I'm not able to pseudo stream using the h264.code-shop.com streaming module and the video only successfully plays through once on an iphone. Once the video reaches the end i cannot replay the video without refreshing the page and i receive a "video could not be loaded" error (JW player). If i bypass the php script and directly link to the video file, everything works properly. Therefore it is apparent there is something different between the output generated from my php script and the output you would normally get from directly accessing the file. So, to all you experts out there, what could i possibly be missing? The correct http headers? What can i do to make my script output a file the exact same way the file would be sent if accessed directly?
Here's the script i'm using:
<?php
if (!isset($_GET['f'])){die(header('location:../login.php'));}
if (!isset($_GET['onlyHappensFromHTACCESS'])) {
$_GET['f'] = "../protectedFolder/".$_GET['f'];
$file = realpath($_GET['f']);
$type = getFileType($file);
if (acceptableType($type))
{
if (goodTiming())
{
//this function used to allow navigation away from the page while video has not completely loaded
session_write_close();
$fs = stat($file);
header("Content-Type: $type");
header("Etag: ".sprintf('"%x-%x-%s"', $fs['ino'], $fs['size'],base_convert(str_pad($fs['mtime'],16,"0"),10,16)));
if (isset($_SERVER['HTTP_RANGE']))
{ // do it for any device that supports byte-ranges not only iPhone
rangeDownload($file);
}
else
{
$size = filesize($file); // File size
header("Content-Length: $size");
header("Last-Modified: " .gmdate("D, d M Y H:i:s")." GMT");
header("Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0");
header("Pragma: no-cache");
header("Keep-Alive: timeout=5, max=100");
header("Connection: Keep-Alive");
$fh = fopen($file, "rb");
while ( ($buf=fread( $fh, 1024 * 8 )) != '' )
{
set_time_limit(0); // Reset time limit for big files
echo $buf;
flush();
}
fclose($fh);
}
}
die();
}
header('HTTP/1.1 403 Forbidden');
die(header('location:../login.php'));
}
function getFileType($file) {
if (function_exists("finfo_open")) {
$finfo = finfo_open(FILEINFO_MIME_TYPE);
if ($file==false){$file=realpath("../authorization_failure.html");}
$type = finfo_file($finfo, $file);
finfo_close($finfo);
return $type;
}
else {
$types = array(
'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'pjpeg' => 'image/jpeg', 'png' => 'image/png',
'gif' => 'image/gif', 'bmp' => 'image/bmp', 'flv' => 'video/x-flv', 'mp4' => 'video/mp4'
);
$ext = substr($file, strrpos($file, '.') + 1);
if (key_exists($ext, $types)) return $types[$ext];
return "unknown";
}
}
function acceptableType($type) {
$array = array("image/jpeg", "image/jpg", "image/png", "image/png", "video/x-flv", "video/mp4");
if (in_array($type, $array))
return true;
return false;
}
function goodTiming() {
$n = time();
session_start();
if ($n - $_SESSION['lastcheck'] > 15 )
return false;
return true;
}
function rangeDownload($file) {
$fp = #fopen($file, 'rb');
$size = filesize($file); // File size
$length = $size; // Content length
$start = 0; // Start byte
$end = $size - 1; // End byte
// Now that we've gotten so far without errors we send the accept range header
/* At the moment we only support single ranges.
* Multiple ranges requires some more work to ensure it works correctly
* and comply with the spesifications: http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html#sec19.2
*
* Multirange support annouces itself with:
* header('Accept-Ranges: bytes');
*
* Multirange content must be sent with multipart/byteranges mediatype,
* (mediatype = mimetype)
* as well as a boundry header to indicate the various chunks of data.
*/
header("Accept-Ranges: 0-$length");
// header('Accept-Ranges: bytes');
// multipart/byteranges
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html#sec19.2
if (isset($_SERVER['HTTP_RANGE'])) {
$c_start = $start;
$c_end = $end;
// Extract the range string
list(, $range) = explode('=', $_SERVER['HTTP_RANGE'], 2);
// Make sure the client hasn't sent us a multibyte range
if (strpos($range, ',') !== false) {
// (?) Shoud this be issued here, or should the first
// range be used? Or should the header be ignored and
// we output the whole content?
header('HTTP/1.1 416 Requested Range Not Satisfiable');
header("Content-Range: bytes $start-$end/$size");
// (?) Echo some info to the client?
exit;
}
// If the range starts with an '-' we start from the beginning
// If not, we forward the file pointer
// And make sure to get the end byte if spesified
if ($range== '-') {
// The n-number of the last bytes is requested
$c_start = $size - substr($range, 1);
}
else {
$range = explode('-', $range);
$c_start = $range[0];
$c_end = (isset($range[1]) && is_numeric($range[1])) ? $range[1] : $size;
}
/* Check the range and make sure it's treated according to the specs.
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
*/
// End bytes cannot be larger than $end.
$c_end = ($c_end > $end) ? $end : $c_end;
// Validate the requested range and return an error if it's not correct.
if ($c_start > $c_end || $c_start > $size - 1 || $c_end >= $size) {
header('HTTP/1.1 416 Requested Range Not Satisfiable');
header("Content-Range: bytes $start-$end/$size");
// (?) Echo some info to the client?
exit;
}
$start = $c_start;
$end = $c_end;
$length = $end - $start + 1; // Calculate new content length
fseek($fp, $start);
header('HTTP/1.1 206 Partial Content');
}
// Notify the client the byte range we'll be outputting
header("Content-Range: bytes $start-$end/$size");
header("Content-Length: $length");
// Start buffered download
$buffer = 1024 * 8;
while(!feof($fp) && ($p = ftell($fp)) <= $end) {
if ($p + $buffer > $end) {
// In case we're only outputtin a chunk, make sure we don't
// read past the length
$buffer = $end - $p + 1;
}
set_time_limit(0); // Reset time limit for big files
echo fread($fp, $buffer);
flush(); // Free up memory. Otherwise large files will trigger PHP's memory limit.
}
fclose($fp);
}
header('location:../login.php');
?>
I use mod_xsendfile for this
https://tn123.org/mod_xsendfile/
Let Apache deal with serving the file, rather than trying to replicate it all in PHP :)
Yes, its easy to do. No need to set those headers manually. Let the server do it automatically.
Heres a working script which I wrote for a video streaming proxy -
ini_set('memory_limit','1024M');
set_time_limit(3600);
ob_start();
**// do any user checks here - authentication / ip restriction / max downloads / whatever**
**// if check fails, return back error message**
**// if check succeeds, proceed with code below**
if( isset($_SERVER['HTTP_RANGE']) )
$opts['http']['header']="Range: ".$_SERVER['HTTP_RANGE'];
$opts['http']['method']= "HEAD";
$conh=stream_context_create($opts);
$opts['http']['method']= "GET";
$cong= stream_context_create($opts);
$out[]= file_get_contents($real_file_location_path_or_url,false,$conh);
$out[]= $http_response_header;
ob_end_clean();
array_map("header",$http_response_header);
readfile($real_file_location_path_or_url,false,$cong);