I have a web application using PHP and PDO with SQLSRV prepared statements to display links to files for users to download. The back-end PHP script ‘download.php’ checks various items before serving the PDF to the user to download. The download.php file then should update a few SQL tables, and serve the PDF file to the user.
Please read my
Previous Question
and the troubleshooting completed there, if you need more information.
After troubleshooting, the error I thought was occurring (and thus the previous question I had asked) was incorrect. My download script is getting executed more than once for every file download.
I have searched the server logs and while debugging with Firebug, I can see my download.php script making multiple GET requests to the server. Sometimes the script completes only once as expected. Other times the script executes three to four request for the one click of the download link.
Now that I more fully understand what error is occurring, I need a bit of help fixing it.
I need to prevent the script from running multiple times, and thus updating the SQL table with records that are within a few milliseconds of each other.
The view page checks the SQL database for files the current user is allowed access to, and displays a list of links:
<a href='download.php?f={$item['name']}&t={$type}' target='_blank'>{$item['name']}</a>
Because the values are needed for the download.php script to work, I cannot change the request to a $_POST instead of $_GET.
What I have tried:
-
Checking/setting a session variable for ‘downloading’ state, before the
getfile()which unsets right before theexit(0) -
Putting the SQL statements in a separate PHP file and require’ing that
-
Adding a
sleep(1)after thegetfile() -
Commenting out the header/PDF information
The first three measures did not work to prevent the double/triple execution of the PHP download script. However, the last measure DOES prevent the double/triple execution of the PHP script, but of course the PDF is never delivered to the client browser!
Question: How can I ensure that only ONE insert/update PER DOWNLOAD is inserted into the database, or at the least, how can I prevent the PHP script from being executed multiple times?
UPDATE
Screenshot of issue in firebug:
One request:

Two requests:

download.php script
<?php
session_start();
require("cgi-bin/auth.php");
// Don't timeout when downloading large files
@ignore_user_abort(1);
@set_time_limit(0);
//error_reporting(E_ALL);
//ini_set('display_errors',1);
function getfile() {
if (!isset($_GET['f']) || !isset($_GET['t'])) {
echo "Nothing to do!";
exit(0);
}
require('cgi-bin/connect_db_pdf.php');
//Update variables
$vuname = strtolower(trim($_SESSION['uname']));
$file = trim(basename($_GET['f'])); //Filename we're looking for
$type = trim($_GET['t']);//Filetype
if (!preg_match('/^[a-zA-Z0-9_\-\.]{1,60}$/', $file) || !preg_match('/^av|ds|cr|dp$/', $type)) {
header('Location: error.php');
exit(0);
}
try {
$sQuery = "SELECT TOP 1 * FROM pdf_info WHERE PDF_name=:sfile AND type=:stype";
$statm = $conn->prepare($sQuery);
$statm->execute(array(':sfile'=>$file,':stype'=>$type));
$result = $statm->fetchAll();
$count = count($result);
$sQuery = null;
$statm = null;
if ($count == 1 ){ //File was found in the database so let them download it. Update the time as well
$result = $result[0];
$sQuery = "INSERT INTO access (PDF_name,PDF_type,PDF_time,PDF_access) VALUES (:ac_file, :ac_type, GetDate(), :ac_vuname); UPDATE pdf_info SET last_view=GetDate(),viewed_uname=:vuname WHERE PDF_name=:file AND PDF_type=:type";
$statm = $conn->prepare($sQuery);
$statm->execute(array( ':ac_vuname'=>$vuname, ':ac_file'=>$file, ':ac_type'=>$type,':vuname'=>$vuname, ':file'=>$file, ':type'=>$type));
$count = $statm->rowCount();
$sQuery = null;
$statm = null;
//$result is the first element from the SELECT query outside the 'if' scope.
$file_loc = $result['floc'];
$file_name = $result['PDF_name'];
// Commenting from this line to right after the exit(0) updates the database only ONCE, but then the PDF file is never sent to the browser!
header("Content-Type: application/pdf");
header("Pragma: no-cache");
header("Cache-Control: no-cache");
header("Content-Length: " . filesize($file_loc));
header("Accept-Ranges: bytes");
header("Content-Disposition: inline; filename={$file_name}");
ob_clean();
flush();
readfile($file_loc);
exit(0);
} else { //We did not find a file in the database. Redirect the user to the view page.
header("Location: view.php");
exit(0);
}
} catch(PDOException $err) {//PDO SQL error.
//echo $err;
header('Location: error.php');
exit(0);
}
}
getfile();
?>
If you really need to make sure that a link only creates an event once, then you need to implement a token system, where when a hyperlink (or a form post target) is generated, a use once token is generated and stored (in the session or wherever), and then is checked in the calling script.
So your hyperlink may look like this:
On the php side this is a really simplified idea of what you might do:
This would solve the effects of your problem, though not the double running itself. However since you want to make sure a link is firing an event only once NO MATTER WHAT, you probably implement this in some form or another as it’s the only way to guarantee that any link generated has a one real use life that I can think of.
When generating the link you would do something like this in your code:
I’d also somewhere put a cleanup routine to remove all used tokens. Also, it’s not a bad idea to expire tokens beyond a certain age, but I think this explains it.