HTMHell
HTMHell

Reputation: 6016

Browser downloads the file with the PHP file's name who runs the download

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: I see it for about half second Then I see the correct file name

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;

Upvotes: 1

Views: 1690

Answers (2)

HTMHell
HTMHell

Reputation: 6016

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!

Upvotes: 1

Giacomo1968
Giacomo1968

Reputation: 26076

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.

Upvotes: 0

Related Questions