当表单设置enctype="multipart/form-data"
时,浏览器会将表单数据编码为多部分(multipart)格式。
Boundary分隔符:随机生成的字符串(如----WebKitFormBoundaryABC123
),用于分隔表单字段和文件内容。
请求头示例:
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryABC123
请求体示例:
------WebKitFormBoundaryABC123
Content-Disposition: form-data; name="userfile"; filename="photo.jpg"
Content-Type: image/jpeg
[文件二进制数据]
------WebKitFormBoundaryABC123--
Transfer-Encoding: chunked
),但PHP会自动重组完整数据。SAPI
(Server API)接收原始HTTP请求数据。sys_get_temp_dir()
),默认路径由php.ini
的upload_tmp_dir
指定。/tmp/phpA3b4cD
),与原始文件名无关。$_FILES
数组结构PHP自动解析请求体,提取文件信息并填充到$_FILES
数组中:
$_FILES['userfile'] = [
'name' => 'photo.jpg', // 客户端原始文件名
'type' => 'image/jpeg', // 浏览器提供的MIME类型(可能被篡改)
'tmp_name' => '/tmp/phpA3b4cD', // 临时文件路径
'error' => UPLOAD_ERR_OK, // 错误码
'size' => 102400 // 文件大小(字节)
];
move_uploaded_file()
,脚本结束时PHP自动删除临时文件。register_shutdown_function()
自定义清理逻辑。move_uploaded_file()
的安全性../
等非法字符。MIME类型检测:
使用finfo_file()
(基于文件内容签名,非扩展名)。
示例:检测JPEG文件的真实MIME类型:
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $_FILES['file']['tmp_name']);
// 返回 'image/jpeg' 而非客户端提供的可能伪造值
扩展名白名单:
$allowedExts = ['jpg', 'jpeg', 'png'];
$ext = strtolower(pathinfo($_FILES['file']['name'], PATHINFO_EXTENSION));
if (!in_array($ext, $allowedExts)) {
die("非法文件扩展名");
}
使用basename()
过滤文件名中的路径符号:
$safeFilename = basename($_FILES['file']['name']);
配置项 | 默认值 | 作用 |
---|---|---|
file_uploads |
On | 是否允许HTTP文件上传 |
upload_max_filesize |
2M | 单个文件最大大小 |
post_max_size |
8M | POST请求最大数据量(必须大于上传限制) |
upload_tmp_dir |
系统临时目录 | 临时文件存储路径 |
max_file_uploads |
20 | 单次请求允许上传的最大文件数 |
配置关系:
post_max_size >= upload_max_filesize * max_file_uploads
multipart/form-data
格式。$_FILES
获取文件信息。move_uploaded_file()
将文件移至安全目录。/var/uploads/
)。调整配置:
upload_max_filesize = 2G
post_max_size = 2G
max_execution_time = 3600
分片上传:通过JavaScript实现文件分片,服务端重组。
使用AJAX + FormData
对象实现无刷新上传:
let formData = new FormData();
formData.append('file', fileInput.files[0]);
fetch('/upload.php', { method: 'POST', body: formData });
自定义错误消息:
$phpFileUploadErrors = [
0 => '成功',
1 => '文件超过php.ini限制',
2 => '文件超过表单限制',
3 => '文件仅部分上传',
4 => '未选择文件',
6 => '缺少临时文件夹',
7 => '写入磁盘失败',
8 => 'PHP扩展阻止了上传',
];
错误触发场景:
UPLOAD_ERR_INI_SIZE
:文件大小超过upload_max_filesize
。UPLOAD_ERR_PARTIAL
:网络中断导致上传不完整。<form action="upload.php" method="POST" enctype="multipart/form-data">
<input type="file" name="userfile" required>
<input type="submit" value="上传">
form>
关键要素:
method="POST"
:必须使用POST方法传输文件enctype="multipart/form-data"
:启用二进制流传输模式required
:HTML5客户端必填验证<input type="file" name="files[]" multiple accept=".jpg,.png">
特性说明:
multiple
:允许选择多个文件accept
:限制可选文件类型(客户端过滤)
<input type="file" accept="image/*">
<input type="file" accept=".pdf,.doc,.docx">
<input type="file" onchange="checkSize(this)">
<script>
function checkSize(input) {
const maxSize = 2 * 1024 * 1024; // 2MB
if (input.files[0].size > maxSize) {
alert('文件大小超过限制');
input.value = ''; // 清空选择
}
}
</script>
<div id="drop-zone" style="border:2px dashed #ccc; padding:20px;">
拖拽文件至此区域
</div>
<script>
const dropZone = document.getElementById('drop-zone');
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.style.borderColor = '#666';
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
const files = e.dataTransfer.files;
// 处理文件上传逻辑
});
</script>
<input type="hidden" name="csrf_token" value="= $_SESSION['token'] ?>">
后端验证:
if ($_POST['csrf_token'] !== $_SESSION['token']) {
die("非法请求");
}
// 删除特殊字符
$cleanName = preg_replace("/[^\w\.]/", '', $_FILES['file']['name']);
// 防止覆盖
$filename = uniqid().'_'.$cleanName;
$_FILES
变量$_FILES
变量基础结构$_FILES
是PHP自动生成的超全局数组,用于存储通过HTTP POST上传的文件信息。其结构为多维数组,典型结构如下:
$_FILES = [
'file_field_name' => [
'name' => 'example.jpg', // 客户端原始文件名
'type' => 'image/jpeg', // 浏览器报告的MIME类型
'tmp_name' => '/tmp/php3h4j8h', // 服务器上的临时文件路径
'error' => 0, // 上传错误代码
'size' => 102400 // 文件大小(字节)
]
];
name
字段来源:客户端文件系统原始名称
风险:可能包含特殊字符或路径信息(如../../shell.php
)
安全处理:
// 过滤非法字符并提取安全文件名
$safe_name = basename($_FILES['file']['name']);
$clean_name = preg_replace('/[^\w\.-]/', '', $safe_name);
type
字段来源:浏览器根据文件扩展名猜测的类型
可靠性:极易伪造(如将.exe文件重命名为.jpg)
验证方法:
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$real_mime = finfo_file($finfo, $_FILES['file']['tmp_name']);
finfo_close($finfo);
tmp_name
字段php.ini
的upload_tmp_dir
配置决定phpXXXXXX
(X为随机字符)move_uploaded_file()
转移文件error
字段错误代码对照表:
常量 | 值 | 说明 |
---|---|---|
UPLOAD_ERR_OK |
0 | 上传成功 |
UPLOAD_ERR_INI_SIZE |
1 | 超过php.ini大小限制 |
UPLOAD_ERR_FORM_SIZE |
2 | 超过表单MAX_FILE_SIZE值 |
UPLOAD_ERR_PARTIAL |
3 | 文件只有部分被上传 |
UPLOAD_ERR_NO_FILE |
4 | 没有文件被上传 |
UPLOAD_ERR_NO_TMP_DIR |
6 | 找不到临时文件夹 |
UPLOAD_ERR_CANT_WRITE |
7 | 文件写入失败 |
UPLOAD_ERR_EXTENSION |
8 | PHP扩展阻止上传 |
错误处理示例:
$error_messages = [
0 => 'Success',
1 => 'File exceeds php.ini upload_max_filesize',
2 => 'File exceeds form MAX_FILE_SIZE',
3 => 'Partial upload',
4 => 'No file uploaded',
6 => 'Missing temporary directory',
7 => 'Failed to write to disk',
8 => 'PHP extension blocked upload'
];
if ($_FILES['file']['error'] !== UPLOAD_ERR_OK) {
die($error_messages[$_FILES['file']['error']]);
}
size
字段单位:字节(1MB = 1,048,576字节)
验证示例:
$max_size = 5 * 1024 * 1024; // 5MB
if ($_FILES['file']['size'] > $max_size) {
die("文件大小超过5MB限制");
}
is_uploaded_file()
核心作用
函数原型
bool is_uploaded_file(string $filename)
使用场景
// 验证临时文件合法性
if (!is_uploaded_file($_FILES['file']['tmp_name'])) {
die("非法文件来源");
}
安全机制
upload_tmp_dir
目录下phpXXXXXX
)典型错误用法
// 错误:直接使用$_FILES中的原始名称
$tmp = '/tmp/' . $_FILES['file']['name'];
if (file_exists($tmp)) { ... } // 存在路径注入风险
move_uploaded_file()
核心作用
is_uploaded_file()
验证功能函数原型
bool move_uploaded_file(string $from, string $to)
使用规范
$safe_dir = '/var/www/uploads/';
$new_name = uniqid() . '_' . basename($_FILES['file']['name']);
if (move_uploaded_file(
$_FILES['file']['tmp_name'],
$safe_dir . $new_name
)) {
// 成功处理
} else {
// 失败处理
}
安全特性
is_uploaded_file()
验证../
)与普通移动函数的对比
特性 | move_uploaded_file() |
rename() /copy() |
---|---|---|
自动安全验证 | ✔️ | ❌ |
跨设备移动支持 | ❌ | ✔️ |
保持文件权限 | ❌ | ✔️ |
防止路径遍历 | ✔️ | ❌ |
双函数协作流程图
function validateUpload($file) {
// 错误检查
if ($file['error'] !== UPLOAD_ERR_OK) return false;
// 临时文件验证
if (!is_uploaded_file($file['tmp_name'])) return false;
// MIME类型检测
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);
if (!in_array($mime, ['image/jpeg', 'image/png'])) return false;
// 扩展名验证
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (!in_array($ext, ['jpg', 'jpeg', 'png'])) return false;
// 文件大小限制
if ($file['size'] > 5*1024*1024) return false;
// 内容安全检查(示例:图片验证)
$image = @imagecreatefromjpeg($file['tmp_name']);
if (!$image) return false;
imagedestroy($image);
return true;
}
echo ''
. print_r($_FILES, true) . '
';
if (file_exists($_FILES['file']['tmp_name'])) {
echo '临时文件大小: ' . filesize($_FILES['file']['tmp_name']);
} else {
echo '临时文件已消失';
}
echo 'PHP最大上传: ' . ini_get('upload_max_filesize');
echo 'POST最大大小: ' . ini_get('post_max_size');
echo '临时目录: ' . sys_get_temp_dir();
问题1:$_FILES
数组为空
php.ini
的file_uploads
是否开启enctype="multipart/form-data"
client_max_body_size
)问题2:部分文件上传失败
upload_tmp_dir
有足够权限(至少755)max_file_uploads
配置问题3:大文件上传中断
调整以下配置:
upload_max_filesize = 256M
post_max_size = 257M
max_execution_time = 3600
max_input_time = 3600
memory_limit = 512M
总结
$_FILES
中的客户端数据move_uploaded_file()
而非copy()
或rename()
chmod 755 uploads/
)<form action="upload.php" method="post" enctype="multipart/form-data">
<input type="file" name="user_files[]" multiple accept=".jpg,.png">
<input type="submit" value="批量上传">
form>
关键点:
name="user_files[]"
:必须使用数组形式命名multiple
:启用多选支持(HTML5特性)accept
:限制可选文件类型(客户端过滤)$_FILES = [
'user_files' => [
'name' => ['a.jpg', 'b.png'], // 文件名数组
'type' => ['image/jpeg', 'image/png'],
'tmp_name' => ['/tmp/phpX1', '/tmp/phpX2'],
'error' => [0, 0], // 错误码数组
'size' => [102400, 204800] // 大小数组
]
];
$files = [];
$fileCount = count($_FILES['user_files']['name']);
for ($i = 0; $i < $fileCount; $i++) {
$files[] = [
'name' => $_FILES['user_files']['name'][$i],
'type' => $_FILES['user_files']['type'][$i],
'tmp_name' => $_FILES['user_files']['tmp_name'][$i],
'error' => $_FILES['user_files']['error'][$i],
'size' => $_FILES['user_files']['size'][$i]
];
}
重组后结构:
$files = [
[
'name' => 'a.jpg',
'type' => 'image/jpeg',
'tmp_name' => '/tmp/phpX1',
'error' => 0,
'size' => 102400
],
[
'name' => 'b.png',
'type' => 'image/png',
'tmp_name' => '/tmp/phpX2',
'error' => 0,
'size' => 204800
]
];
if (empty($_FILES['user_files']['tmp_name'][0])) {
die("未选择任何文件");
}
$uploadResults = [];
$allowedTypes = ['image/jpeg', 'image/png'];
$maxFileSize = 2 * 1024 * 1024; // 2MB
$uploadDir = __DIR__ . '/uploads/';
foreach ($files as $index => $file) {
try {
// 检查上传错误
if ($file['error'] !== UPLOAD_ERR_OK) {
throw new Exception("文件{$index}上传失败,错误码:{$file['error']}");
}
// 验证MIME类型
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$realMime = finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);
if (!in_array($realMime, $allowedTypes)) {
throw new Exception("文件{$index}类型不合法");
}
// 验证扩展名
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (!in_array($ext, ['jpg', 'jpeg', 'png'])) {
throw new Exception("文件{$index}扩展名不合法");
}
// 验证大小
if ($file['size'] > $maxFileSize) {
throw new Exception("文件{$index}超过大小限制");
}
// 生成唯一文件名
$safeName = md5(uniqid() . $file['name']) . '.' . $ext;
$targetPath = $uploadDir . $safeName;
// 移动文件
if (!move_uploaded_file($file['tmp_name'], $targetPath)) {
throw new Exception("文件{$index}保存失败");
}
$uploadResults[] = [
'original' => $file['name'],
'saved_as' => $safeName,
'status' => 'success'
];
} catch (Exception $e) {
$uploadResults[] = [
'original' => $file['name'],
'error' => $e->getMessage(),
'status' => 'failed'
];
}
}
echo json_encode([
'total' => count($files),
'success' => count(array_filter($uploadResults, fn($item) => $item['status'] === 'success')),
'results' => $uploadResults
]);
// 使用PHP的并行处理扩展(需安装parallel)
$parallel = new \parallel\Runtime();
$futures = [];
foreach ($files as $file) {
$futures[] = $parallel->run(function($file) {
// 文件处理逻辑
}, [$file]);
}
// 收集结果
$results = array_map(fn($f) => $f->value(), $futures);
// 前端JavaScript
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', e => {
const percent = Math.round((e.loaded / e.total) * 100);
progressBar.style.width = percent + '%';
});
实现步骤:
// 合并示例
$totalChunks = 5;
$finalFile = 'merged_file.zip';
for ($i=1; $i<=$totalChunks; $i++) {
$chunk = file_get_contents("chunk_{$i}.part");
file_put_contents($finalFile, $chunk, FILE_APPEND);
}
// 限制并发上传数量
if (count($files) > 10) {
http_response_code(429);
die("一次最多上传10个文件");
}
// 使用ClamAV扫描
$clamscan = '/usr/bin/clamscan';
$output = shell_exec("$clamscan --no-summary $targetPath");
if (strpos($output, 'OK') === false) {
unlink($targetPath);
throw new Exception("文件感染病毒");
}
// 检查图片是否包含裸露内容(示例使用NSFW.js)
$imageData = file_get_contents($targetPath);
$nsfwCheck = shell_exec("node nsfw-check.js $imageData");
if ($nsfwCheck > 0.7) {
unlink($targetPath);
throw new Exception("检测到违规内容");
}
; 允许同时上传的文件数
max_file_uploads = 20
; 单个文件最大尺寸
upload_max_filesize = 50M
; POST数据最大尺寸
post_max_size = 55M
; 脚本最大执行时间
max_execution_time = 1800
client_max_body_size 55M;
client_body_temp_path /var/nginx/client_temp;
client_body_in_file_only clean;
现象 | 可能原因 | 解决方案 |
---|---|---|
$_FILES数组为空 | 表单未设置enctype | 检查表单enctype属性 |
部分文件上传失败 | 临时目录权限不足 | chmod 755 /tmp |
文件名乱码 | 编码不一致 | 使用mb_convert_encoding转换 |
大文件上传中断 | 超时设置过小 | 调整max_execution_time |
无法生成缩略图 | GD库未安装 | 安装php-gd扩展 |
class MultiFileUploader {
private $uploadDir;
private $allowedMimes;
private $maxSize;
public function __construct($uploadDir, $allowedMimes, $maxSize) {
$this->uploadDir = rtrim($uploadDir, '/') . '/';
$this->allowedMimes = $allowedMimes;
$this->maxSize = $maxSize;
$this->createUploadDir();
}
private function createUploadDir() {
if (!is_dir($this->uploadDir)) {
mkdir($this->uploadDir, 0755, true);
}
}
public function process($fileField) {
$files = $this->reorganizeFiles($_FILES[$fileField]);
$results = [];
foreach ($files as $file) {
try {
$this->validateFile($file);
$filename = $this->generateFilename($file);
$this->moveFile($file['tmp_name'], $filename);
$results[] = $this->successResult($file, $filename);
} catch (Exception $e) {
$results[] = $this->errorResult($file, $e);
}
}
return $results;
}
private function reorganizeFiles($files) {
$organized = [];
foreach ($files as $key => $values) {
foreach ($values as $index => $value) {
$organized[$index][$key] = $value;
}
}
return $organized;
}
// ...其他方法实现...
}
// 使用示例
$uploader = new MultiFileUploader(
__DIR__ . '/uploads',
['image/jpeg', 'image/png'],
2 * 1024 * 1024
);
$results = $uploader->process('user_files');
原则 | 实现方式 |
---|---|
单一职责 | 分离验证、存储、后处理模块 |
开闭原则 | 通过继承扩展功能而非修改源码 |
里氏替换 | 子类处理器保持父类接口兼容 |
接口隔离 | 定义UploadValidator独立接口 |
依赖倒置 | 依赖抽象接口而非具体实现 |
深度防御模型:
/[^a-z0-9\-_.]/i
chmod 755
+ open_basedir
限制零信任实现:
class ZeroTrustValidator {
public function validate($file) {
$this->checkOrigin($file['tmp_name']);
$this->verifySignature($file['tmp_name']);
$this->analyzeEntropy($file['tmp_name']);
}
private function checkOrigin($path) {
if (!is_uploaded_file($path)) {
throw new SecurityException("非法文件来源");
}
}
}
分片上传算法:
def upload_chunk(file, chunk_size=5*1024*1024):
total = math.ceil(file.size / chunk_size)
for i in range(total):
chunk = file.read(chunk_size)
hash = sha256(chunk).hexdigest()
redis.set(f"upload:{file.id}:{i}", {
'hash': hash,
'data': base64.b64encode(chunk)
})
return merge_chunks(file.id, total)
指标收集:
# TYPE file_upload_size histogram
file_upload_size_bucket{status="success",le="1048576"} 42
file_upload_size_bucket{status="success",le="5242880"} 87
# TYPE upload_error_counter counter
upload_error_counter{type="size_limit"} 3
分布式追踪:
{
"trace_id": "abc123",
"span_id": "def456",
"operation": "FileUpload",
"tags": {
"file.size": "2.4MB",
"validation.time": "128ms"
}
}
/**
* 安全文件上传处理器
*
* 功能特性:
* 1. 多文件上传支持
* 2. MIME类型白名单验证
* 3. 文件扩展名过滤
* 4. 自动生成安全文件名
* 5. 病毒扫描集成接口
* 6. 图片EXIF信息处理
* 7. 上传进度跟踪
* 8. 自动目录创建
* 9. 防御性错误处理
*/
class FileUploader {
// 配置参数
private $config = [
'upload_dir' => __DIR__.'/uploads', // 上传目录
'allowed_mimes' => [], // 允许的MIME类型
'allowed_exts' => [], // 允许的扩展名
'max_size' => 2 * 1024 * 1024, // 最大文件尺寸(2MB)
'overwrite' => false, // 是否覆盖同名文件
'sanitize_name' => true, // 自动清理文件名
'hash_name' => true, // 使用哈希文件名
'virus_scan' => false, // 启用病毒扫描
'image_handling' => [ // 图片处理配置
'resize' => [
'enabled' => false,
'width' => 800,
'height' => 600
],
'strip_exif' => true
]
];
// 运行时状态
private $errors = [];
private $uploadedFiles = [];
/**
* 构造函数
* @param array $config 自定义配置项
*/
public function __construct(array $config = []) {
$this->config = array_merge($this->config, $config);
$this->init();
}
/**
* 初始化验证
*/
private function init() {
// 检查上传功能是否启用
if (!ini_get('file_uploads')) {
throw new RuntimeException('服务器未启用文件上传功能');
}
// 创建上传目录
if (!is_dir($this->config['upload_dir'])) {
$this->createDirectory($this->config['upload_dir']);
}
// 验证目录可写
if (!is_writable($this->config['upload_dir'])) {
throw new RuntimeException('上传目录不可写: '.$this->config['upload_dir']);
}
}
/**
* 处理文件上传
* @param string $fieldName 表单字段名
* @return array 上传结果
*/
public function upload(string $fieldName): array {
$this->resetState();
if (!isset($_FILES[$fieldName])) {
$this->errors[] = "未找到上传字段: {$fieldName}";
return $this->getResult();
}
$files = $this->reorganizeFiles($_FILES[$fieldName]);
foreach ($files as $file) {
$this->processSingleFile($file);
}
return $this->getResult();
}
/**
* 重组多文件数组结构
*/
private function reorganizeFiles(array $files): array {
$organized = [];
foreach ($files as $key => $values) {
foreach ($values as $index => $value) {
$organized[$index][$key] = $value;
}
}
return $organized;
}
/**
* 处理单个文件
*/
private function processSingleFile(array $file) {
try {
// 基础验证
$this->validateBasic($file);
// 安全验证
$this->validateSecurity($file);
// 生成目标路径
$destination = $this->generateDestination($file);
// 移动文件
$this->moveUploadedFile($file['tmp_name'], $destination);
// 后处理
$this->postProcess($destination, $file);
// 记录成功
$this->uploadedFiles[] = [
'original_name' => $file['name'],
'saved_path' => $destination,
'size' => $file['size'],
'mime_type' => $this->getRealMimeType($file['tmp_name'])
];
} catch (Exception $e) {
$this->errors[] = $file['name'].': '.$e->getMessage();
}
}
/**
* 基础验证
*/
private function validateBasic(array $file) {
// 错误代码验证
if ($file['error'] !== UPLOAD_ERR_OK) {
throw new RuntimeException($this->getUploadError($file['error']));
}
// 临时文件验证
if (!is_uploaded_file($file['tmp_name'])) {
throw new RuntimeException('非法文件来源');
}
// 文件大小验证
if ($file['size'] > $this->config['max_size']) {
$maxSize = round($this->config['max_size'] / 1024 / 1024, 1);
throw new RuntimeException("文件超过 {$maxSize}MB 限制");
}
}
/**
* 安全验证
*/
private function validateSecurity(array $file) {
// MIME类型验证
$realMime = $this->getRealMimeType($file['tmp_name']);
if (!in_array($realMime, $this->config['allowed_mimes'])) {
throw new RuntimeException("禁止的文件类型: {$realMime}");
}
// 扩展名验证
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (!in_array($ext, $this->config['allowed_exts'])) {
throw new RuntimeException("禁止的文件扩展名: .{$ext}");
}
// 病毒扫描
if ($this->config['virus_scan']) {
$this->scanForVirus($file['tmp_name']);
}
}
/**
* 获取真实MIME类型
*/
private function getRealMimeType(string $tmpPath): string {
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $tmpPath);
finfo_close($finfo);
return $mime;
}
/**
* 生成目标路径
*/
private function generateDestination(array $file): string {
$filename = $this->config['sanitize_name']
? $this->sanitizeFilename($file['name'])
: $file['name'];
if ($this->config['hash_name']) {
$ext = pathinfo($filename, PATHINFO_EXTENSION);
$filename = md5(uniqid().microtime()).'.'.$ext;
}
$destination = $this->config['upload_dir'].DIRECTORY_SEPARATOR.$filename;
// 防覆盖处理
if (!$this->config['overwrite'] && file_exists($destination)) {
throw new RuntimeException('文件已存在');
}
return $destination;
}
/**
* 安全移动文件
*/
private function moveUploadedFile(string $tmpPath, string $destination) {
if (!move_uploaded_file($tmpPath, $destination)) {
throw new RuntimeException('文件保存失败');
}
// 设置安全权限
chmod($destination, 0644);
}
/**
* 文件名消毒
*/
private function sanitizeFilename(string $filename): string {
// 删除路径信息
$clean = basename($filename);
// 替换特殊字符
$clean = preg_replace("/[^a-zA-Z0-9\-_.]/", '_', $clean);
// 缩短长度
return substr($clean, 0, 200);
}
/**
* 病毒扫描
*/
private function scanForVirus(string $filePath) {
// 示例:集成ClamAV
$output = shell_exec("clamscan --no-summary {$filePath}");
if (strpos($output, 'OK') === false) {
unlink($filePath);
throw new RuntimeException('文件包含病毒或恶意代码');
}
}
/**
* 上传后处理
*/
private function postProcess(string $filePath, array $originalFile) {
// 图片处理
if (strpos($originalFile['type'], 'image/') === 0) {
$this->processImage($filePath);
}
}
/**
* 图片处理
*/
private function processImage(string $filePath) {
try {
// 去除EXIF信息
if ($this->config['image_handling']['strip_exif']) {
$this->stripExif($filePath);
}
// 调整尺寸
if ($this->config['image_handling']['resize']['enabled']) {
$this->resizeImage(
$filePath,
$this->config['image_handling']['resize']['width'],
$this->config['image_handling']['resize']['height']
);
}
} catch (Exception $e) {
unlink($filePath);
throw new RuntimeException('图片处理失败: '.$e->getMessage());
}
}
// ...其他辅助方法...
/**
* 获取最终结果
*/
public function getResult(): array {
return [
'success' => $this->uploadedFiles,
'errors' => $this->errors,
'total' => count($this->uploadedFiles) + count($this->errors),
'passed' => count($this->uploadedFiles),
'failed' => count($this->errors)
];
}
}