我正在构建一个web应用程序,它本质上是一个工作板——用户创建"工作/项目",然后作为一个全局的"待办事项"列表呈现给公司的其他人员。
我正在尝试实现的功能之一是"附件"功能,允许用户上传文件作为每个项目数据的一部分。
这个想法是允许用户安全地上传文件,然后允许其他用户下载附件。
例如,如果我们正在为客户创建产品包装,那么能够附加客户的徽标(.pdf或其他)作为项目数据的一部分将是很好的,这样任何使用我们的工作板查看项目的设计师都可以下载该文件。
结合常用的上传技术并参考PHP书籍(PHP和MySQL for Dynamic Websites - by Larry Ullman),我构建了以下PHP脚本:
[..]
// check if the uploads form has been submitted:
if($_SERVER['REQUEST_METHOD'] == 'POST') {
//check if the $_FILES global has been set:
if (isset($_FILES['upload'])) {
//create a function to rewrite the $_FILES global (for readability):
function reArrayFiles($file) {
$file_ary = array();
$file_count = count(array_filter($file['name']));
$file_keys = array_keys($file);
for ($i=0; $i<$file_count; $i++) {
foreach ($file_keys as $key) {
$file_ary[$i][$key] = $file[$key][$i];
}
}
return $file_ary;
}
//create a variable to contain the returned data & call the function
//**Quick note: I thought simply stating 'reArrayFiles($_FILES['upload']);' would be enough, but I guess not
$file_ary = reArrayFiles($_FILES['upload']);
//establish an array of allowed MIME file types for the uploads:
$allowed = array(
'image/pjpeg', //.jpeg
'image/jpeg',
'image/JPG',
'image/X-PNG', //.png
'image/PNG',
'image/png',
'image/x-png',
'image/gif', //.gif
'application/pdf', //.pdf
'application/msword', //.doc
'application/vnd.openxmlformats-officedocument.wordprocessingml.document', //.docx
'application/vnd.ms-excel', //.xls
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', //.xlsx
'text/csv', //.csv
'text/plain', //.txt
'text/rtf', //.rtf
);
//these are two arrays for containing statements and errors that occured for each individual file upload
//so I can choose exactly where these "errors" print on page, rather than printing where the script as a whole is called
$statement = array();
$upload_error = array();
//multi-file upload, so perform checks and actions on EACH file upload individually:
foreach ($file_ary as $upload) {
//validate the uploaded file's MIME type using finfo:
$fileinfo = finfo_open(FILEINFO_MIME_TYPE); //open handle
//read the file's MIME type (using magic btyes), then check if it is w/i the allowed file types array
if ( in_array((finfo_file($fileinfo, $upload['tmp_name'])), $allowed) ) {
//check the file's MIME type AGAIN, but this time using the rewritten $_FILES['type'] global
//it may be redundant to check the file type twice, but I felt this was necessary because some files made it past the first conditional
if ( in_array($upload['type'], $allowed) && ($upload['size'] < 26214400) ) {
//set desired file structure to store files:
//the tmp directory is one level outside my webroot
//the '$job_data[0]' variable/value is the unique job_id of each project
//here, it is used to create a folder for each project's uploads -- in order to keep them organized
$structure = "../tmp/uploads/job_" . $job_data[0] . "/";
//check if the folder exists:
if (file_exists($structure) && is_dir($structure)) {
//if directory already exists, get file count: (files only - no directories or subdirectories)
$i = 0;
if (($handle = opendir($structure))) {
while (($file = readdir($handle)) !== false) {
if (!in_array($file, array('.','..')) && !is_dir($structure.$file))
$i++;
}
closedir($handle);
$file_count = $i;
}
} else {
//directory does not exist, so create it
//files are NOT counted b/c new directories shouldn't have any files) -- '$file_count == 0'
mkdir($structure);
}
//if file count is less than 10, allow file upload:
//this limits the project so it can only have a maximum of 10 attachments
if ($file_count < 10) {
if (move_uploaded_file($upload['tmp_name'], "$structure{$upload['name']}")) {
$statement[] = '<p>The file has been uploaded!</p>';
} else {
$statement[] = '<p class="error">The file could not be transfered from its temporary location -- Possible file upload attack!</p>';
}
} else if ($file_count >= 10) {
//if there are already 10 or more attachments, DO NOT upload files, return statement/error
$statement[] = '<p class="error">Only 10 attachments are allowed per Project.</p>';
}
//ELSE FOR 2ND FILE TYPE CHECK:
} else {
$statement[] = '<p class="error">Invalid basic file type.</p>';
}
//set an error msg to $upload_error array if rewritten $_FILES['error'] global is not 0
//this section of code omitted; literally every upload script does this
if ($upload['error'] > 0) {
switch ($upload['error']) {
[...]
}
}
//remove the temp file if it still exists after the move/upload
if ( file_exists($upload['tmp_name']) && is_file($upload['tmp_name']) ) {
unlink ($upload['tmp_name']);
}
//ELSE FOR 1ST FILE TYPE CHECK
} else {
$statement[] = '<p class="error">Invalid MIME file type.</p>';
}
//close the finfo module
finfo_close($fileinfo);
} //END OF FOREACH
} //END OF isset($_FILES['upload']) conditional
}//END OF $_SERVER['REQUEST_METHOD'] == 'POST' conditional
我的HTML是这样的:
<form enctype="multipart/form-data" action="edit-job.php" method="post">
<input type="hidden" name="MAX_FILE_SIZE" value="26214400"/>
<fieldset>
<legend>Upload Project Files</legend>
<input type="file" name="upload[]"/>
<input type="file" name="upload[]"/>
<input type="file" name="upload[]"/>
<input type="file" name="upload[]"/>
<input type="file" name="upload[]"/>
<p>Max Upload Size = 25MB</p>
<p><b>Supported file types:</b> .jpeg, .png, .gif, .pdf, .doc, .docx, .xls, .xlsx, .csv, .txt, .rtf</p>
</fieldset>
<input type="submit" name="submit" value="Edit Job"/>
</form>
总之,我提供了一个多文件上传PHP脚本(带验证)和附带的HTML。
我的方法不使用MySQL数据库,其中一个表等于项目id与相关的附件/文件,因为我看到其他上传方法使用。
它只是为每个项目的附件创建一个独特的文件夹,放在web浏览器之外的一个公共位置,因为这样被认为更安全。
在这一点上,我承认这一切看起来相当不正统,但它工作得很好,直到我不得不担心用户下载!无论如何,以下是我的问题:
(1)如何允许用户使用我的结构(在webroot之外)下载文件?
我最初尝试创建文件的基本链接,像这样:
<a href="../tmp/uploads/{unique_folder}/{file_name}" target="_blank">{file_name}</a>';
但由于限制/固有安全性,这显然不起作用。然后,我发现最好使用单独的"download.php"文件(如果我错了请纠正我),像这样:
'<a href="download.php?id=' . $job_data[0] . '&file_name=' . $file . '" target="_blank">' . $file . '</a>';
(将变量传递给单独的.php文件)
但是。php文件应该包含什么呢?我读过关于php的header()函数的各种东西,从tmp原始文件重新创建。pdf文件等。
我就是搞不懂这一切。
这里有一个链接到我正在谈论的内容:
http://web-development-blog.com/archives/php-download-file-script听起来你是用PHP来访问文件,而不是让用户的浏览器来访问。有人能验证这个资源吗?
(2)我做错了什么吗?
- 我应该使用MySQL数据库的附件(如我提到的)?
- 我的上传脚本有明显的安全漏洞吗?
- 应该有一个加载条机制为缓慢的上传?等。
我担心我的web应用程序作为一个整体的完整性;我不想受到SQL注入或其他黑客方法的影响。
但更重要的是,我想清除作为一个新手开发人员可能存在的任何不良实践。
非常感谢您的反馈;
关于(1):
理论是:您将文件存储在web浏览器之外的某个地方,以防止直接访问并防止在服务器端执行。当用户提供正确的参数时,您必须能够找到它。(但是,您应该小心,文件是如何呈现给用户的?如果安全是一个问题,你必须确保用户不能访问他不应该访问的文件(当他有正确的下载链接,但没有权限,例如,因为到目前为止你的下载链接似乎不是很神秘)。
你在你的问题中提到的脚本是相当好的,虽然我可能只是使用fpassthru
而不是feof-fread-echo-loop。这里的想法基本上是找出mime类型,将其添加到报头中,然后将内容转储到输出流中。
使用数据库特别是预处理语句是相当安全的,并提供了一些额外的可能性。(比如在附件中添加一些注释,时间戳,文件大小,重新排序,…)
你没有检查upload_name
,这很可能是../../your webroot/index.php
或类似的东西。我的建议是将上传的文件存储为"file_ID",并将该id与原始文件名一起存储在数据库中。您可能还应该删除任何前导多个点,斜杠("directory")和类似的。
Loading bars…我想这就是品味。