Use the header() function to send the correct headers to download a file from a link in a webpage while blocking any attempt to explore the file system.
- [Instructor] Hi, I'm David Powers, and welcome to this week's edition of PHP Tips, Tricks, and Techniques, designed to help you become a smarter, more productive PHP developer. Creating a download link for images or documents is a common requirement on a website. The basic process is very straightforward. Instead of linking directly to the file, you link to a download script and append the filename to the URL in a query string. The download script sends a series of headers to the browser and then reads and outputs the file.
Because the download script is generic, it can be used with any file. And therein lies a serious risk. Query strings are easy to manipulate. So if you're not careful, the download script could be tricked into exposing sensitive files, such as your service password store. To prevent this happening, you need to block any attempt to explore your file system. With that in mind, let's get to the code.
The exercise files for this video contain a simple webpage with two links. Both links point to a file called download.php. And then that's followed by a query string, with a name value pair. File equals and then the name of the file that we want to download. The first one is an image and the second one is a Word document. There's also a file called error.php. This contains a simple error message and the download script itself goes in download.php.
So far the script consists of two statements, defining the location of the error page and of the downloads folder. To block any attempt to explore the file system, the file path to the downloads folder needs to be an absolute path like this. I've set both values to match my setup, but you need to change them to match your setup. Next, I'm going to create a variable for the filename. So simply, filename and initialize it to null.
That's a keyword, so it's not in quotes. And doing this is a fail-safe technique. We want to make sure that nothing is downloaded, unless the next test is satisfied. We can get the filename from the query string, using the superglobal get array. And we need to make sure the value exists. So if isset, then the get array and the other word we're looking for is file. But that on its own is not enough.
For example, something like this could result in downloading all usernames and passwords on a Linux server. We need to make sure that the value of file is simply a filename and not a relative path. We can do that by parsing get file to the basename function. So if we have & then basename and we parse that get file, and we compare it with the raw value of get file, what the basename function does is it returns the filename at the end of a path.
So if these two are both the same, we know that nobody is trying to explore the file system. We've just got the filename. So if that's the case, inside our curly braces, we assign to filename the value of get file. But if they're not the same, we need to redirect to the error message. So an else block, and inside there the header function, then in double quotes, Location, error.
And we also need to immediately exit the script. But assuming the value in get file isn't attempting to explore the file system, we can go ahead with the download. As a precaution, we'll use a conditional statement to make sure that filename isn't still null. So, if filename. Then we can build an absolute path to the requested file by concatenating the name of the path that we defined at the top of the script.
So we'll create a variable called abs_path. Then we'll have this file path from up here. Concatenate onto that a PHP constant directory separator. This will insert a back slash or a forward slash, depending on the operating system, and then concatenating onto the end of that, the actual filename. So that locates our file. We need to make sure that it exists and that it's readable.
So if file exists, we'll parse it to the absolute path. And is readable, again parsing it back to absolute path. If we've got a file that we can work with, we can then send some headers to the browser to download it. The first header. That will be Content-type, colon application, slash octet-stream.
This simply streams that file to the browser in binary form. Then we need the size of the file, so another header. This one will be Content-length. And we can get the size using the PHP filesize function and parsing it, the absolute path to our file. Another header. This one will be Content-disposition.
And it will be an attachment. We need a semicolon after that. Then filename equals. Now, we can concatenate onto the end of that the actual filename itself. So this will download the file with its original name. And the last header that we want. That's Content-transfer-encoding and it needs to be binary.
So with all those headers being sent to the browser, we can finally output the file using readfile. We simply parse it to the absolute path. But if the file didn't exist or wasn't readable, we need to redirect to the error message. So an else block here. And again, we use the header function and our location is error. And that completes the download script, so let's go our browser and test this.
I've got index.php lined up here. Click this first link here, Tunnel View: Yosemite. I immediately get this download dialog box and the filename is already in there. It's Tunnel_view.jpg. So just save that. And let's just make sure that it's been downloaded successfully. I'm going to click that, there it is. There's Tunnel_view.jpg. That's fine. And try it with the Word document.
Again, we get the download dialog box, Word.docx. Just save that. And we'll quickly open it. There's the test file, so everything is working fine. This week, we looked at creating a link to download files, such as images or documents. Create a link to a download script and append the filename to the URL in a query string.
The download script must block any attempt to explore the file system. The simple way to do this is to parse the value from the query string to the basename function and compare it with the raw value. If they're different, terminate the script. But if they're the same and the file exists, send a series of headers to the browser and output the file. If the file doesn't exist or if there's an attempt to explore the file system, redirect to an error page. This script relies on sending headers to the browser.
So you might get the dreaded Headers Already Sent error, preventing the script from working. To avoid this, there must be no whitespace before the opening PHP tag in the download script. Nor must there be any output before calling the header function. The presence of the byte order mark at the beginning of a script can also trigger the Headers Already Sent error. This is an invisible character, but most good editing programs have an option to remove the BOM.
That's it for this edition of PHP Tips, Tricks, and Techniques. Thanks for watching.
Note: The exercise files are free to all members. The code is commented to enhance your learning, but you will need database connectivity for some files to run as intended.