?
Solved

php hiding download file address from user

Posted on 2007-08-06
32
Medium Priority
?
312 Views
Last Modified: 2013-12-13
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;
0
Comment
Question by:noisecouk
  • 12
  • 6
  • 4
  • +4
32 Comments
 
LVL 17

Expert Comment

by:jasonsbytes
ID: 19637906
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.
0
 
LVL 4

Expert Comment

by:jentulman
ID: 19637921
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;
}
0
 

Author Comment

by:noisecouk
ID: 19637954
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.
0
Technology Partners: We Want Your Opinion!

We value your feedback.

Take our survey and automatically be enter to win anyone of the following:
Yeti Cooler, Amazon eGift Card, and Movie eGift Card!

 
LVL 17

Expert Comment

by:jasonsbytes
ID: 19638013
make sure you echo TWO CR's after the content type.  Maybe try:

header("Content-Type: $type\n\n");
0
 

Author Comment

by:noisecouk
ID: 19638047
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.
0
 
LVL 4

Expert Comment

by:jentulman
ID: 19638064
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).
0
 
LVL 17

Expert Comment

by:jasonsbytes
ID: 19638073
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.
0
 

Author Comment

by:noisecouk
ID: 19638099
Removing the other headers except for Content-Type just returns a white page displaying this:
http://mywebsite.org/downloads.php?id=5
0
 
LVL 31

Expert Comment

by:Frosty555
ID: 19638101
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.
0
 

Author Comment

by:noisecouk
ID: 19638314
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.
0
 
LVL 51

Expert Comment

by:ahoffmann
ID: 19639082
> 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.
0
 
LVL 31

Accepted Solution

by:
Frosty555 earned 300 total points
ID: 19639118
I'm thinking something like
 
     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);

$fsize = filesize($filePath);
$path_parts = pathinfo($filePath);

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

readfile($filePath);

include 'admin/closedb.php';
exit;
0
 
LVL 31

Expert Comment

by:Zoppo
ID: 19639148
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
0
 
LVL 4

Expert Comment

by:jentulman
ID: 19639605
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.'"');
0
 

Author Comment

by:noisecouk
ID: 19640389
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
0
 
LVL 51

Expert Comment

by:ahoffmann
ID: 19640836
> ...upload-to-file-server.php
are you talking about file uploads now while you asked for sending a file in your question?
0
 

Author Comment

by:noisecouk
ID: 19640971
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.
0
 
LVL 4

Expert Comment

by:jentulman
ID: 19641188
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?
0
 

Author Comment

by:noisecouk
ID: 19641278
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.
0
 
LVL 7

Expert Comment

by:ljubiccica
ID: 19641417
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
0
 

Author Comment

by:noisecouk
ID: 19641735
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.
0
 
LVL 7

Assisted Solution

by:ljubiccica
ljubiccica earned 600 total points
ID: 19641825
On both pages the first thing has to be:

<?php
session_start();

and after that...

$_SESSION['name']=$name;
$_SESSION['type']=$type;
$_SESSION['size']=$size;
$_SESSION['filePath']=$filePath;

and on the second page:

$name = $_SESSION['name'];
$type= $_SESSION['type'];
$size= $_SESSION['size'];
$filePath= $_SESSION['filePath'];

Greets
Ljubiccica
0
 
LVL 51

Expert Comment

by:ahoffmann
ID: 19641886
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).
0
 

Author Comment

by:noisecouk
ID: 19641892
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;
?>
0
 
LVL 51

Expert Comment

by:ahoffmann
ID: 19641895
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).
0
 

Author Comment

by:noisecouk
ID: 19641984
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.
0
 
LVL 51

Assisted Solution

by:ahoffmann
ahoffmann earned 600 total points
ID: 19642087
check each and every input to your script against a whitlist (might be a regex too), then reject the complete request if anthing does not match.
If you'd uye perl that's as simple as
  die "wrong parameter" if ($_GET[id]=~m/[^a-zA-Z0-9_-]+/g);
in PHP you have to fiddle around with preg_match() ...
0
 
LVL 7

Assisted Solution

by:ljubiccica
ljubiccica earned 600 total points
ID: 19643292
Yes i know___

You are overwtitting $things you got from session with this:

$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'];

Comment these lines and try again...

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

Greets
Ljubiccica

P.S.: If you need something from URL, you need to use $_GET instead of $_POST...
0
 

Author Comment

by:noisecouk
ID: 19643977
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??
0
 

Author Comment

by:noisecouk
ID: 19644030
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!
0
 
LVL 7

Expert Comment

by:ljubiccica
ID: 19644049
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
0
 
LVL 51

Assisted Solution

by:ahoffmann
ahoffmann earned 600 total points
ID: 19644182
> header("Location: someurl.php");
may work in most browser, but it is technically not correct. The location header should always be a full URL including the schema.
0

Featured Post

Concerto Cloud for Software Providers & ISVs

Can Concerto Cloud Services help you focus on evolving your application offerings, while delivering the best cloud experience to your customers? From DevOps to revenue models and customer support, the answer is yes!

Learn how Concerto can help you.

Question has a verified solution.

If you are experiencing a similar issue, please ask a related question

This article discusses four methods for overlaying images in a container on a web page
This article discusses how to implement server side field validation and display customized error messages to the client.
The viewer will learn how to count occurrences of each item in an array.
The viewer will learn how to create a basic form using some HTML5 and PHP for later processing. Set up your basic HTML file. Open your form tag and set the method and action attributes.: (CODE) Set up your first few inputs one for the name and …
Suggested Courses
Course of the Month15 days, 3 hours left to enroll

840 members asked questions and received personalized solutions in the past 7 days.

Join the community of 500,000 technology professionals and ask your questions.

Join & Ask a Question