189 lines
6.7 KiB
PHP
189 lines
6.7 KiB
PHP
<?php
|
||
// upload.php
|
||
session_start();
|
||
|
||
// 配置参数
|
||
$config = [
|
||
'max_file_size' => 50 * 1024 * 1024, // 50MB
|
||
'allowed_types' => [
|
||
'image/jpeg',
|
||
'image/png',
|
||
'application/pdf',
|
||
'text/plain'
|
||
],
|
||
'upload_dir' => __DIR__ . '/uploads/',
|
||
'csrf_token' => 'secure_token_here' // 生产环境应使用更安全的生成方式
|
||
];
|
||
|
||
// 处理文件上传
|
||
$error = '';
|
||
$success = '';
|
||
|
||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||
// CSRF 验证
|
||
if (!isset($_POST['csrf_token']) || $_POST['csrf_token'] !== $config['csrf_token']) {
|
||
$error = '无效的请求令牌';
|
||
} else {
|
||
try {
|
||
// 验证上传文件
|
||
$uploadedFile = $_FILES['file'] ?? null;
|
||
|
||
if (!$uploadedFile || $uploadedFile['error'] !== UPLOAD_ERR_OK) {
|
||
throw new RuntimeException(handleUploadError($uploadedFile['error']));
|
||
}
|
||
|
||
// 验证文件大小
|
||
if ($uploadedFile['size'] > $config['max_file_size']) {
|
||
throw new RuntimeException('文件大小超过50MB限制');
|
||
}
|
||
|
||
// 验证文件类型
|
||
$finfo = new finfo(FILEINFO_MIME_TYPE);
|
||
$mimeType = $finfo->file($uploadedFile['tmp_name']);
|
||
|
||
if (!in_array($mimeType, $config['allowed_types'])) {
|
||
throw new RuntimeException('不支持的文件类型');
|
||
}
|
||
|
||
// 创建上传目录
|
||
if (!is_dir($config['upload_dir']) && !mkdir($config['upload_dir'], 0755, true)) {
|
||
throw new RuntimeException('无法创建上传目录');
|
||
}
|
||
|
||
// 生成安全文件名
|
||
$extension = pathinfo($uploadedFile['name'], PATHINFO_EXTENSION);
|
||
$safeName = sprintf('%s.%s', bin2hex(random_bytes(8)), $extension);
|
||
$targetPath = $config['upload_dir'] . $safeName;
|
||
|
||
// 移动文件
|
||
if (!move_uploaded_file($uploadedFile['tmp_name'], $targetPath)) {
|
||
throw new RuntimeException('文件保存失败');
|
||
}
|
||
|
||
$success = sprintf('文件上传成功!文件名:%s', htmlspecialchars($safeName));
|
||
} catch (RuntimeException $e) {
|
||
$error = $e->getMessage();
|
||
}
|
||
}
|
||
}
|
||
|
||
function handleUploadError($code) {
|
||
switch ($code) {
|
||
case UPLOAD_ERR_INI_SIZE:
|
||
case UPLOAD_ERR_FORM_SIZE:
|
||
return '文件大小超过服务器限制';
|
||
case UPLOAD_ERR_PARTIAL:
|
||
return '文件只有部分被上传';
|
||
case UPLOAD_ERR_NO_FILE:
|
||
return '没有文件被上传';
|
||
case UPLOAD_ERR_NO_TMP_DIR:
|
||
return '缺少临时文件夹';
|
||
case UPLOAD_ERR_CANT_WRITE:
|
||
return '写入磁盘失败';
|
||
default:
|
||
return '未知上传错误';
|
||
}
|
||
}
|
||
?>
|
||
|
||
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>安全文件上传系统</title>
|
||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||
<style>
|
||
.upload-container {
|
||
max-width: 800px;
|
||
margin: 2rem auto;
|
||
padding: 2rem;
|
||
background: #f8f9fa;
|
||
border-radius: 10px;
|
||
box-shadow: 0 0 15px rgba(0,0,0,0.1);
|
||
}
|
||
.preview-box {
|
||
border: 2px dashed #6c757d;
|
||
border-radius: 8px;
|
||
padding: 1.5rem;
|
||
text-align: center;
|
||
margin: 1rem 0;
|
||
transition: all 0.3s ease;
|
||
}
|
||
.preview-box:hover {
|
||
border-color: #0d6efd;
|
||
background: rgba(13, 110, 253, 0.05);
|
||
}
|
||
#file-input {
|
||
opacity: 0;
|
||
position: absolute;
|
||
z-index: -1;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<div class="upload-container">
|
||
<h2 class="mb-4 text-center">安全文件上传系统</h2>
|
||
|
||
<?php if ($error): ?>
|
||
<div class="alert alert-danger"><?= htmlspecialchars($error) ?></div>
|
||
<?php endif; ?>
|
||
|
||
<?php if ($success): ?>
|
||
<div class="alert alert-success"><?= $success ?></div>
|
||
<?php endif; ?>
|
||
|
||
<form method="post" enctype="multipart/form-data">
|
||
<input type="hidden" name="csrf_token" value="<?= $config['csrf_token'] ?>">
|
||
|
||
<div class="preview-box">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor" class="bi bi-cloud-upload mb-3" viewBox="0 0 16 16">
|
||
<path d="M4.406 1.342A5.53 5.53 0 0 1 8 0c2.69 0 4.923 2 5.166 4.579C14.758 4.804 16 6.137 16 7.773 16 9.569 14.502 11 12.687 11H10a.5.5 0 0 1 0-1h2.688C13.979 10 15 8.988 15 7.773c0-1.216-1.02-2.228-2.313-2.228h-.5v-.5C12.188 2.825 10.328 1 8 1a4.53 4.53 0 0 0-2.941 1.1c-.757.652-1.153 1.438-1.153 2.055v.448l-.445.049C2.064 4.805 1 5.952 1 7.318 1 8.785 2.23 10 3.781 10H6a.5.5 0 0 1 0 1H3.781C1.708 11 0 9.366 0 7.318c0-1.763 1.266-3.223 2.942-3.593.143-.863.698-1.723 1.464-2.383z"/>
|
||
<path d="M7.646 4.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1-.708.708L8.5 5.707V14.5a.5.5 0 0 1-1 0V5.707L5.354 7.854a.5.5 0 1 1-.708-.708l3-3z"/>
|
||
</svg>
|
||
<h5>选择要上传的文件</h5>
|
||
<p class="text-muted">支持格式:JPEG, PNG, PDF, TXT</p>
|
||
<label for="file-input" class="btn btn-primary mt-2">
|
||
<i class="bi bi-folder2-open"></i> 浏览文件
|
||
</label>
|
||
<div id="file-name" class="mt-2"></div>
|
||
</div>
|
||
|
||
<input
|
||
type="file"
|
||
name="file"
|
||
id="file-input"
|
||
class="form-control"
|
||
required
|
||
accept=".jpg,.jpeg,.png,.pdf,.txt"
|
||
>
|
||
|
||
<div class="d-grid gap-2 mt-4">
|
||
<button type="submit" class="btn btn-success btn-lg">
|
||
<i class="bi bi-upload"></i> 开始上传
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// 文件选择交互
|
||
document.getElementById('file-input').addEventListener('change', function(e) {
|
||
const fileName = e.target.files[0]?.name || '未选择文件';
|
||
document.getElementById('file-name').textContent = `已选择文件:${fileName}`;
|
||
});
|
||
|
||
// 前端大小验证
|
||
document.querySelector('form').addEventListener('submit', function(e) {
|
||
const fileInput = document.getElementById('file-input');
|
||
if (fileInput.files[0]?.size > <?= $config['max_file_size'] ?>) {
|
||
alert('文件大小超过50MB限制');
|
||
e.preventDefault();
|
||
}
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|