Link to home
Start Free TrialLog in
Avatar of noisecouk
noisecouk

asked on

php hiding download file address from user

I've created some code on my website which connects to MySQL which has the following stored information of the file: name, type, size and path. The file itself is stored inside a folder on my website which is hiden from view from the user.

The problem I'm having is that files such as .doc, .jpg, .gif... basically anything that isn't a text file is downloaded as garbled data. The files when linked directly open fine as in showing all the paths as a typical straight to file hyperlink, it's only when I use this script to hide the path data that it downloads it as garbled data. However, txt files seem to open ok. Any ideas??

Below is the php code I've used

                     include 'admin/config.php';
      include 'admin/opendb.php';

      $id      = $_GET['id'];
      $query   = "SELECT name, type, size, path FROM upload WHERE id = '$id'";
      $result  = mysql_query($query) or die('Error, query failed');
      list($name, $type, $size, $filePath) = mysql_fetch_array($result);

$id = $_GET['id'];
$query = "SELECT name, type, size, path FROM upload WHERE id = '$id'";
$result = mysql_query($query) or die('Error, query failed');
list($name, $type, $size, $filePath) = mysql_fetch_array($result);

header("Content-Disposition: attachment; filename=$name");
header("Content-Length: $size");
header("Content-Type: $type");

readfile($filePath);

include 'admin/closedb.php';
exit;
Avatar of Jason Minton
Jason Minton
Flag of United States of America image

It is a mime type issue...

Probably you are setting the content-type $type to the same thing for all files:

header("Content-Type: $type");

You need to set the type so it matches what you are downloading.
Avatar of jentulman
jentulman

I would guess that it is sending the wrong MIME type in the line#

header('Content-type: $type);

text files will appear to display correctly as the browser will attempt to show the 'text' version of whatever file is being downloaded, which with JPG or doc files will be shown as grabled text as the broswer is displaying the ascii interpretation of the binary file.

What do you have stored in the 'type' field?

With your code as is it should be something like

application/pdf
or
image/jpeg

see http://www.iana.org/assignments/media-types/ for a list of mime types.

It might be easier to use a switch statement to set the type based on the file extension.

$ext = substr($filename,-3);
switch($ext){
 case "doc":
    $type = "application/msword";
 break;
 case "jpg":
    $type = "image/jpeg";
 break;
}
Avatar of noisecouk

ASKER

under the "$type" field, it calls the file type stored in the MySQL database, such as image/jpeg, application/msword, or whatever the file type is.
make sure you echo TWO CR's after the content type.  Maybe try:

header("Content-Type: $type\n\n");
Tried header("Content-Type: $type\n\n"), it doesn't seem to work...

I've even tried renaming the header("Content-Type: image/jpeg"); and trying to download a jpg file. Still no luck.
have you used anything to examine the headers that are being sent when you access one of the files such as LiveHTTPHeaders for Firefox or Fiddlerfor  IE?

http://livehttpheaders.mozdev.org/

http://www.fiddlertool.com/fiddler/

The symptoms do really sound like a MIME type problem so maybe something is getting mangled in the type variable.

or you could just change it to

echo("Content-Disposition: attachment; filename=$name");
echo("Content-Length: $size");
echo("Content-Type: $type");
and see what it's spitting out.

Additionally

Check that there is nothing before or after the <? and ?> php tags, extra whitespace either side fo the tags can cause additional headers to be sent.
Just to be ion the safe side put the call to close the DB before you output any of the headers (shouldn;t make any difference though).
It is definitly an issue with the Content-Type...  Just need to figure out what exactly.

Try removing the other header lines except for Content-Type.
Removing the other headers except for Content-Type just returns a white page displaying this:
http://mywebsite.org/downloads.php?id=5
It does look like some kind of a header issue. Check if it works in Mozilla and not in IE, or vice versa. The browsers are very fickle with the headers. I've never had any luck including the "attachment" clause in the content-disposition header.

I answered a similar question to this earlier, where someone was trying to hide the location of a downloadable file from their server. This sample code seemed to work for him:

        $fullpath = "yourfile.txt"
        $fd = fopen ($fullPath, "r")
        $fsize = filesize($fullPath);
        $path_parts = pathinfo($fullPath);
        $ext = strtolower($path_parts["extension"]);
        header("Content-type: application/octet-stream");
        header("Content-Disposition: filename=\"".$path_parts["basename"]."\"");
        header("Content-length: $fsize");
        header("Cache-control: private"); //use this to open files directly
        while(!feof($fd)) {
            $buffer = fread($fd, 2048);
            echo $buffer;
        }
        fclose ($fd);
        exit;

Now, you've used ReadFile() which should be fine, I re-invented the wheel a bit here. But my content type was an octet stream, not any other MIME type. This might have been okay because he probably was only using .ZIP and .EXE files with the script, but I think you should try changing your headers to mimic what's above and see if it works.
Frosty555 is it possible to rewrite my header content for me from my original code? I've had a go and can't get it to work. Cheers.
> make sure you echo TWO CR's after the content type.  Maybe try:
don't do that!
Beside it is wrong syntax (\r\r which should be \r\n) it will break the HTTP header.

Please use LiveHttpHeader (or something similar) as suggested and post the complete HTTP header send by the server.
ASKER CERTIFIED SOLUTION
Avatar of Frosty555
Frosty555
Flag of Canada image

Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
Avatar of Zoppo
Hi noisecouk,

I wrote a small test-php to download a JPG and found that anything outside the code section is even written to the file - so even one empty line or a space bevor or after the <?php or after ?> corrupts the file. So, this code without any trailing or leading space or line worked for me:

<?php
$name="test.jpg";
$size=-1;
$type="image/jpeg";

header("Content-Disposition: attachment; filename=$name");
header("Content-Length: $size");
header("Content-Type: $type");
readfile( "test.jpg" );
?>

Maybe you should compare the file downloaded with the original to find what's the difference ...

ZOPPO
try putting quotes around the file name in the content disposition line

header("Content-Disposition: attachment; filename="filename");

so in php it's

header('Content-Disposition: attachment; filename="'.$name.'"');
Just in case anyone is interested in understanding my problem, I'm following this tutorial: http://www.php-mysql-tutorial.com/upload-to-file-server.php
> ...upload-to-file-server.php
are you talking about file uploads now while you asked for sending a file in your question?
The file uploads bit is fine. I've managed to create a system where a user when logged in can upload a file to the server. The file is stored in a folder, and its location, name, info, type and size is stored in a MySQL databse.

The bit that isn't working however is when the user tries to download the file. The MySQL database acts like a directory directing the file to the user. Whilst the file is able to be located and downloaded, the file becomes garbled up and unreadable when the user tries to open it, unless it is a text file.
If you right click the link and choose "save as" or "save link as" (whichever it is in your browser) what filename does the browser try to save the file as?
The file name it gives to save as is the same as variable $name.

The variable $filePath however is the actual file name and includes the path to the folder. The $name and $filePath variables can be totally different. For example $filePath = "upload/file1.jpg (this is the actual file) and $name = "Picture-of-something.jpg" this is what is displayed to the user.
Hey!

Few weeks ago i had (probably) the same problems -> spent long hours to find the solution and the only thing that worked for me (in IE and FF) with headers was this:

header('Cache-Control: maxage=3600'); //Adjust maxage appropriately
header('Pragma: public');
header("Content-length: $filesize");
header("Content-type: $mimeType");
header("Content-Disposition: attachment; filename=$title");
echo $melodyData;

Hope it will help you solve your problem too.
Greets
Ljubiccica
OK guy! I've been woking on this for a long time now, finally got it to work although I would like some help with inprovements using sessions().

// The if statement detects if there is an "id" variable in the URL... That is www.address.com?id=1
// The id values are unique numbers which are related to the row on the MySQL table.
// When an id value is inputed, the if statement runs opening the table called "upload" and the row with associated id.

// The file fetches the variable data >> name, type, size, and filePath from the SQL table.
// It then posts these variables to a new page which it forwards to...

// The new page GETs the variables from the URL bar and runs the script to download the file.

Now would it be possible to instead of using >> header("Location: download-test.php?name=$name&type=$type&size=$size&filePath=$filePath"); I can use some sort of session() to remember the variables and post them to the next page, and if so how?


// download.php
<?php
error_reporting(E_ALL);
if(isset($_GET['id']))
{
      include 'admin/config.php';
      include 'admin/opendb.php';

      $id      = $_GET['id'];
      $query   = "SELECT name, type, size, path FROM upload WHERE id = '$id'";
      $result  = mysql_query($query) or die('Error, query failed');
      list($name, $type, $size, $filePath) = mysql_fetch_array($result);
      
header("Location: download-test.php?name=$name&type=$type&size=$size&filePath=$filePath");
}
?>


//download-test.php
<?php
//Get the variable information from the URL bar      
$name = $_GET['name'];
$size = $_GET['size'];
$type = $_GET['type'];
$filePath = $_GET['filePath'];

//remove any backslashes from " or ' from PHP code
$name = stripslashes($name);
$size = stripslashes($size);
$type = stripslashes($type);
$filePath = stripslashes($filePath);

//header information containing the file info
header("Content-length: $size");
header("Content-type: $type");
header("Content-Disposition: attachment; filename=$name");

//download file from the file path
readfile($filePath);
?>


Cheers, Dan.
SOLUTION
Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
I guess you have some rubbish stored in your database, or your php code scrambles the strings when writing to the HTTP header.
Please do as suggested multiple times and post the HTTP header verbatim (use LiveHttpHeader or whatever you want).
ljubiccica, doesn't seem to work... know what i'm doing wrong??
//downloads.php
<?php
session_start();
$_SESSION['name']=$name;
$_SESSION['type']=$type;
$_SESSION['size']=$size;
$_SESSION['filePath']=$filePath;
error_reporting(E_ALL);
if(isset($_GET['id']))
{
      include 'admin/config.php';
      include 'admin/opendb.php';

      $id      = $_GET['id'];
      $query   = "SELECT name, type, size, path FROM upload WHERE id = '$id'";
      $result  = mysql_query($query) or die('Error, query failed');
      list($name, $type, $size, $filePath) = mysql_fetch_array($result);
      
header("Location: download-test.php");
}
?>

//download-test.php
<?php
session_start();
$name = $_SESSION['name'];
$type= $_SESSION['type'];
$size= $_SESSION['size'];
$filePath= $_SESSION['filePath'];

//Get the variable information from the URL bar      
$name = $_POST['name'];
$size = $_POST['size'];
$type = $_POST['type'];
$filePath = $_POST['filePath'];

//remove any backslashes from " or ' from PHP code
$name = stripslashes($name);
$size = stripslashes($size);
$type = stripslashes($type);
$filePath = stripslashes($filePath);

//header information containing the file info
header("Content-length: $size");
header("Content-type: $type");
header("Content-Disposition: attachment; filename=$name");

//download file from the file path
readfile($filePath);

exit;
?>
BTW, I'd never ever use such a code with totally unchecked $_GET values 'cause that make your script vulnerable to countless attacks (including SQL injection).
ahoffmann, cheers for bringing that to my attention, what ways are there to reduce the risk of SQL injection? Had a quick brief flick through the wiki page.
SOLUTION
Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
SOLUTION
Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
liubiccica, I've done what you said and didn't manage to get it to work, I did however find the source of why the sessions() were being lost.

I removed header("Location: download-test.php"); from downloads.php and when I manually type download-test.php in the address bar it works. Is there a way round the header("Location:") problem when trying to set a session??
I finally got everything to work, downloads.php was missing this line:
session_write_close();         // Writes the session data and closes before redirect

the final code is now this:

----downloads.php---
<?php
session_start();
error_reporting(E_ALL);
if(isset($_GET['id']))
{
      include 'admin/config.php';
      include 'admin/opendb.php';

      $id      = $_GET['id'];
      $query   = "SELECT name, type, size, path FROM upload WHERE id = '$id'";
      $result  = mysql_query($query) or die('Error, query failed');
      list($name, $type, $size, $filePath) = mysql_fetch_array($result);

$_SESSION['name']=$name;
$_SESSION['type']=$type;
$_SESSION['size']=$size;
$_SESSION['filePath']=$filePath;
session_write_close();         // Writes the session data and closes before redirect      
header("Location: download-test.php");
}
?>

---download-test.php
<?php
session_start();
//Set variables against the session variables
$name = $_SESSION['name'];
$type= $_SESSION['type'];
$size= $_SESSION['size'];
$filePath= $_SESSION['filePath'];

//remove any backslashes from " or ' from PHP code
$name = stripslashes($name);
$size = stripslashes($size);
$type = stripslashes($type);
$filePath = stripslashes($filePath);

//header information containing the file info
header("Content-length: $size");
header("Content-type: $type");
header("Content-Disposition: attachment; filename=$name");

//download file from the file path
readfile($filePath);

exit;
?>

Cheers for all your help guys!
Yes, forgot to write it... Sorry...

One more thing (might come in handy someday)...
When you use header("Location: someurl.php") in some clauses (like if or while etc.) it is a good practice to write the following:

session_write_close();    
header("Location: someurl.php");
die();

Greets
Ljubiccica
SOLUTION
Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial