This is an old revision of the document!
Table of Contents
A Simple PHP/SQLite Download Counter
Summary
If you host downloadable content on SDF, you may want to get an idea of how many people have downloaded things and which are the most popular. In this tutorial, I will focus on an example of tracking PDF formatted ebooks. After completing this tutorial, you should be able to modify things to suit your needs, even if your downloads are not ebooks. You will need to have PHP access to do this, so check your membership level before you get started.
The Download Script
For the impatient and the PHP gurus in the audience, I will show the entire script first. After that, I'll pick it apart and explain each section.
<?php // Set the location holding the download content. $content_dir = basename(__FILE__) . "/download"; // The query string passed indicates the filename. if (isset($_SERVER['QUERY_STRING'])) { $file = rawurldecode($_SERVER['QUERY_STRING']); $path = $content_dir . DIRECTORY_SEPARATOR . $file; // Deliver the file if it exists, otherwise error. if (file_exists($path)) { header("Content-type: application/pdf"); header("Cache-Control: no-store, no-cache"); header("Content-Disposition: inline; filename=\"$file\""); echo file_get_contents($path); // Record the download in the hit count database. $pdo = new PDO("sqlite:download.sl3"); if ($pdo) { $create = "CREATE TABLE IF NOT EXISTS tally (datetime VARCHAR(32), filename VARCHAR(256))"; $pdo->exec($create); $insert = "INSERT INTO tally (datetime, filename) VALUES (datetime('now'), :filename)"; $prepared_sql = $pdo->prepare($insert); $prepared_sql->bindValue("filename", $file); $prepared_sql->execute(); } } else { http_response_code(404); header("Content-type: text/plain"); header("Cache-Control: no-store, no-cache"); echo "404: Not Found"; } } ?>
Explanation of Parts
For a better understanding of how this works, I'll dissect the parts of the script one by one and explain each bit.
Where to Find the Files
First, there is the $content_dir
variable.
// Set the location holding the download content. $content_dir = basename(__FILE__) . "/download";
This needs to be set to where the downloadable files are kept. The way things are set up by default assumes you have a directory hierarchy that looks like this:
html/ |-- download.php |-- download/ |-- ebook.pdf
In other words, your downloadable content is all kept in a directory called download and it is a subdirectory residing next to the download.php script we're putting together.
$content_dir = basename(__FILE__) . "/download";
FILE in PHP means the filesystem path to the PHP script being executed. This is not the URL path, but the filesystem path. So it will not be http://login.sdf.org/download
. It will look more like /sdf/arpa/tz/l/login/html
and using the dot concatenation operator will tack /download
to the end of it.
So now you know where to store you content and what to change if it's located somewhere else.
Knowing What Was Requested
Next, there is the section that decodes the query string.
// The query string passed indicates the filename. if (isset($_SERVER['QUERY_STRING'])) { $file = rawurldecode($_SERVER['QUERY_STRING']); $path = $content_dir . DIRECTORY_SEPARATOR . $file;
Inside your HTML, you might have a hyperlink that looks like this: href=“download/ebook.pdf
. This is a simple example of fetching the file ebook.pdf
from the download
sub directory.
To use the download counter, the link needs to look like this: href=“download.php?ebook.pdf
. It's a minor change and easy to pull off en masse with a little RegEx.
With the new format, each hyperlink will call the download.php script and pass the name of the desired file as a query string. The PHP variable $_SERVER['QUERY_STRING']
is where we find this file name. It's just the name, not the full path, so we need to concatenate it with the $content_dir
for it to be useful.
You may have also noticed the rawurldecode()
function around the $_SERVER['QUERY_STRING']
. If you have experience with HTML and query strings, you might know there are certain rules that must be followed when passing information as part of a URL. One of those rules is no spaces allowed.
If you have an ebook called My Great American Novel.pdf
, it's not going to work, because there are spaces. You'll need to make the file name in your hyperlink look like this: My%20Great%20American%20Novel.pdf
The %20 is a URL safe encoding of the space character. Guess what rawurldecode()
does. If you said, 'change the %20s back to spaces' you are a winner.
Delivering the file
Now that we know what file the user wants, it's time to send it to their browser. That's what the next section of code is for.
// Deliver the file if it exists, otherwise error. if (file_exists($path)) { header("Content-type: application/pdf"); header("Cache-Control: no-store, no-cache"); header("Content-Disposition: inline; filename=\"$file\""); echo file_get_contents($path);
The first thing it will do is to check to make sure the file actually exists. If there's something wrong with the hyperlink, or the user types it in manually and makes a mistake, we need a way to tell them. For now though, let's pretend everything is happening as it should.
The script delivers three HTTP headers to let the user's browser know what's coming.
The first is the content type (aka MIME type.) I mentioned that I use this script for PDF ebooks, so my content type is hard coded to application/pdf
. Obviously, you'll want to change it if you are delivering other types of files.
If you are delivering more than one type of file, you could use an associative array to choose the content-type based on file extension. This is beyond the scope of the tutorial.
The next HTTP header tells the browser and any proxy servers in between, not to cache the content.
Finally, we have some specifics about the actual file.
One being that it should be displayed inline. (The alternative is having the browser prompt to save.) I use inline because most browsers have a PDF reader built in and it's less jarring to the user to have everything appear in the same window.
The other is the filename. This is a courtesy to the user and will pre-fill any save dialog with the filename if they choose to save it.