Force download with PHP then redirect

20,649

Solution 1

  1. You can't hide a file location. It'll be plainly visible to anybody determined enough to find it, by the very necessity that the browser needs to know the URL to download the file.
  2. You can't do it with two header redirects in succession, as you said. You can only redirect to a different page after some timeout using Javascript.

There really isn't much choice. If your primary goal is to hide the URL, that's a lost cause anyway. For good usability, you usually include the plain link on the page anyway ("Download doesn't start? Click here..."), since the user may accidentally cancel the redirect at just the wrong time to irrevocably kill the download.


You cannot output anything other than the file itself in the same request/response. You could try multi-part HTTP responses as suggested by @netcoder, but I'm not really sure how well that's supported. Just start with the assumption that there's one "wasted" request/response in which only the file is downloaded and nothing else happens. The way things usually work with this restriction is like this:

  • User clicks "download" link or submits form with his email address or whatever is required to initiate the download process.
  • Server returns the "Thank you for downloading from us! Your download will start shortly..." page.
  • This page contains Javascript or a <meta> refresh or HTTP Refresh header that causes a delayed redirect to the URL of the file.
  • The "Thank you" page will "redirect" to the file location, but since this causes the file to download, the page will not visibly change, only the download will be initiated.

Look at http://download.com for an example of this in action.

You can make the download location for the file be a script that only returns the file if the user is allowed to download the file. You can pass some temporary token between the "Thank you" page and the file download page to verify that the download is allowed.

Solution 2

Ok, first thing first, the browser need to know the file location to download it. Anyone opening a standard browser dev tool like Firebug will be able to see in plain text the URL of your file.

Now, I suppose you want to protect your file from unauthorised download. If that is what you want, there is a way for that using session.

On your first page, you will put your code to check if the download is authorized. You will then put in session the current with something that identify the file. Anything that is unique for the file will do, like the database id.

$_SESSION['download_key'] = time();

Then, you redirect to a page with a html meta like this

<meta http-equiv="refresh" content="5;/download.php?file=download_key" />

This is the page where you will say "Thank you fine lad for downloading my awesome file". Note that you can also put the content of the "content" attribute in a header file if you like, like this

header('Refresh: 5;/download.php?file=download_key');

Note that the 5 is the number of second until the download file dialog box appear.

Then on download.php you will do the following :

1- Check which file was requested using the $_GET['file'].

2- Then you check if the download_key exists in session. If not, you exit the script like that

if (!isset($_SESSION['download_key'])) die('Unauthorized');

3- Then you check if the timestamps is older than some arbitrary time limit. Here with 30 sec

if ($_SESSION['download_key'] - time() > 30) die('Unauthorized');

4- Finally, if all check out, you send the file like that

header('Content-disposition: attachment; filename=myfile.ext');
header('Content-type: bin/x-file-type'); //Change for the correct mimetype
readfile('myfile.ext');

After the readfile, you will put the code to set the download to 1 in the database.

And that's it, protected file download, and anybody using the URL directly will be greeted by a big "unauthorized" text.

I'd also like to add that if you have a big file (more than say a few kilobyte), you may be better off disabling output buffering, since that will mean that php will keep a copy of the file in memory for the whole duration of the download. With the readfile function, php will send it to the browser as it read it on the disk and thus will use less memory (and will start to send data sooner).

EDIT : What make it work is the following

I actually inverted the sequence : the visitor is first redirected to the thank you page which contain a Refresh header/tag. The magic of the Refresh header is that it redirect AFTER the content is loaded. Once on the thank you page, the browser seeing that header then wait for the specified time while showing the page, then redirect to the download. Once redirected, the browser see that its a file to be downloaded and instead of changing the page, just show the download file dialog. Once the user click OK, the download will start but he will stay on the same page.

In a nutshell, you don't need to redirect after the file download, since you are already on the thank you page! I don't think it's even possible to redirect after a file download. Just look at what happen when you click on a link that point to a direct file on the webserver. The browser prompt for download, but does not cut your navigation. Once the download is started, you can happily click away from the page with the link. By the time the download is over, you may be on a completely different Website. That is why you can only show the thank you page before. But if you put a zero for the refresh header/tag, the download prompt will appear as soon as the page is loaded, so it's almost as if the two were simultaneous (in the eye of the visitor)

Solution 3

The only way in PHP I know of (to have a download, then to redirect) is to use a multi-part HTTP response. Something like that:

define('MP_BOUNDARY', '--'.sha1(microtime(true)));

header('Content-Type: multipart/x-mixed-replace; boundary="'.MP_BOUNDARY.'"');
flush();

echo "Content-Type: application/zip\r\n";
echo "Content-Disposition: attachment; filename=foo.zip\r\n";
echo "\r\n";
readfile('foo.zip');
echo MP_BOUNDARY;
flush();

echo "Content-Type: text/html\r\n";
echo "\r\n";
echo '<html><script type="text/javascript">location.href="http://www.google.ca";</script></html>';
echo MP_BOUNDARY.'--';
flush();

In your case, you could just output the "Thanks for downloading" page content instead of the JavaScript redirect.

I'm unsure whether it works on all/major browsers or not.

Share:
20,649
RichieAHB
Author by

RichieAHB

I work as a front end web developer and tend to answer / ask about Javascript, PHP, HTML, CSS and Wordpress. SOreadytohelp

Updated on May 06, 2020

Comments

  • RichieAHB
    RichieAHB almost 4 years

    I know this question has been asked many times before but I can't find an answer to suit my needs.

    I need to find a way to force the download of a file and then, after the download has started, redirect to a "thanks for downloading" page.

    So far I have:

    <?php
    ob_start();
    
    $token = $_POST['validationCode'];
    
    if(isset($token)){
    
        $connect = mysql_connect('localhost', 'root', 'root');
        $db = mysql_select_db('mydb');
    
        if (!$connect || !$db){
            die('Connect Error (' . mysql_connect_errno() . ') '
                    . mysql_connect_error());
        }
    
        $sql = mysql_query("SELECT * FROM emailaddresses WHERE token='$token'");
        $result = mysql_fetch_array($sql);
        if($result){
            header('Location: complete.php');
            header('Content-type: application/mp3');
            header('Content-Disposition: attachment; filename=track.mp3');
            $f = file_get_contents('downloads/track.mp3');
            print $f;
            $sql = "UPDATE emailaddresses SET download=1 WHERE token='$token'";
            $result = mysql_query($sql);
        }
        else{
            echo "There was a problem downloading the file" . mysql_error();
        }
    }
    
    ob_end_flush();
    
    ?>
    

    It's important to hide the download file's location otherwise I would have just created an HTML link to the file.

    I obviously can't put a redirect header below the other headers as it just won't work. I can't really see where to go from here apart from opening this in a pop-up and directing the main window to the "thank you" page - but that is a LAST resort.

    Can anyone give any suggestions?

    Cheers,

    Rich

  • RichieAHB
    RichieAHB about 13 years
    I understand that the file can't be hidden per se but it's purely to stop extremely easy to the file, as all someone needs to do is input their email address to download the file. Would there be a way of putting the file outside of the document root, downloading that and redirecting. You're telling me I can't do this but given the situation could you advise the general outline of how you would best hide the file location, force it to download once an email validation code has been input and validated and then redirect to another page after the download has started?
  • Gromski
    Gromski about 13 years
    @richie Then you need actual validation for the file download itself. As you say, put it outside the webroot, provide access to it through a script and let that script check whether the user is supposed to access it or not. The redirect-after-download still stands, you have to do it with Javascript client-side.
  • RichieAHB
    RichieAHB about 13 years
    That looks promising but I've been coding PHP for two months and I'm by no means familiar with multipart HTTP responses! If I can't get an answer I need that is similar to my current areas of knowledge then I guess this'll be another area for me to bone up on! Thanks, Rich.
  • netcoder
    netcoder about 13 years
    @richieahb: Why not learn it? It's not that hard. If you know how to handle headers and content, then you know how to handle multi-part responses. I just made an example right there. Try it. :)
  • RichieAHB
    RichieAHB about 13 years
    Thanks for that, there's a lot in there to help me refine what I've got. However, what I don't understand is what happens when you get to this download page. Because you can't echo anything before the header and you can't echo anything after the header as it is read as part of the file, how can you redirect/link the user back to a page of any substance without them using the back arrow!
  • RichieAHB
    RichieAHB about 13 years
    My problem is echoing something - in this case javascript - on a page where the header describes the content type as an application and thus you can't echo before the header or after it with or without output buffering. I'm a total noob at this so I am probably making a fundamental mistake in my assumptions but I cannot work it out. Thanks for your help so far though - I'm sure I'll work it out in the cold light of day tomorrow with these answers :)
  • RichieAHB
    RichieAHB about 13 years
    That sounds like the ticket thanks! I've had it a couple of times - whilst messing about with headers - where the file downloads but it stayed on the validation input page rather than visibly move to any download page. I never thought to use that one step down the line at the Thank You page. I'm sure it'll take a bit of fiddling but I feel like I'll have it sorted. Thanks!
  • Laurent Bourgault-Roy
    Laurent Bourgault-Roy about 13 years
    I tried to add some more details in the answer. Just tell me if there is still some unclear points.
  • RichieAHB
    RichieAHB about 13 years
    Thanks Laurent - that's exactly the way I've gone with now. DOing it in reverse and just getting the scrip to change the header rather than the actual page. And thanks for the tip about ob-flush with big files - hadn't thought of that!