我有一个使用PHP和PDO的Web应用程序,带有SQLSRV预处理语句,用于显示文件的链接供用户下载。后端PHP脚本'download.php'在将PDF提供给用户下载之前检查各种项目。然后,download.php文件应该更新几个SQL表,并将PDF文件提供给用户。
请阅读我的
Previous Question
如果您需要更多信息,可以在那里完成故障排除。
排除故障后,我认为发生的错误(因此我之前提出的问题)是不正确的。每次下载文件时,我的下载脚本都会被执行多次。
我搜索了服务器日志,在使用Firebug进行调试时,我可以看到我的download.php脚本向服务器发出多个GET请求。有时,脚本只按预期完成一次。其他时候,脚本只需单击一次下载链接即可执行三到四个请求。
现在我更全面地了解发生了什么错误,我需要一些帮助来解决它。 我需要阻止脚本多次运行,从而使用彼此在几毫秒内的记录更新SQL表。
视图页面检查SQL数据库中是否允许当前用户访问的文件,并显示链接列表:
<a href='download.php?f={$item['name']}&t={$type}' target='_blank'>{$item['name']}</a>
因为download.php脚本需要使用这些值,所以我无法将请求更改为$ _POST而不是$ _GET。
我尝试过:
检查/设置“下载”状态的会话变量,在getfile()
exit(0)
之前将SQL语句放在单独的PHP文件中并要求
在sleep(1)
getfile()
评论标题/ PDF信息
前三项措施无法阻止PHP下载脚本的双/三次执行。但是,最后一项措施是否会阻止PHP脚本的双重/三重执行,但当然PDF永远不会传送到客户端浏览器!
问题:如何确保只有一个插入/更新PER DOWNLOAD插入到数据库中,或者至少,如何防止PHP脚本多次执行?
更新
firebug中问题的截图:
一个要求:
两个要求:
download.php脚本
<?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();
?>
答案 0 :(得分:4)
如果确实需要确保链接只创建一次事件,那么您需要实现一个令牌系统,当生成超链接(或表单发布目标)时,会生成并存储一次使用令牌(在会话中或任何地方),然后在调用脚本中进行检查。
所以你的超链接可能如下所示:
<a href='download.php?token={some-token}&f={$item['name']}&t={$type}' target='_blank'>{$item['name']}</a>
在php方面,这是一个非常简化的想法,你可以做什么:
<?php
session_start();
if (!isset($_REQUEST['token']) die(); // or fail better
if (!isset($_SESSION['oneTimeTokens'][$_REQUEST['token']) die(); // or fail better
if ($_SESSION['oneTimeTokens'][$_REQUEST['token']=='used') die(); // or fail better
$_SESSION['oneTimeTokens'][$_REQUEST['token']='used';
// we're good from this point
这可以解决你的问题的影响,虽然不是双重运行本身。但是,既然你想确保一个链接只触发一次事件,那么你可能会以某种形式实现它,因为这是保证生成的任何链接都有一个我能想到的实际使用寿命的唯一方法。
生成链接时,您可以在代码中执行以下操作:
<?php
$tokenID = {random id generation here};
$_SESSION['oneTimeTokens'][$tokenID] = 'not used';
我还会在某个地方放置一个清理例程来删除所有使用过的令牌。此外,超过一定年龄的令牌到期并不是一个坏主意,但我认为这解释了它。