第十二章-PHP文件上傳
一,文件上傳原理
一、HTTP協議與文件上傳
1. 請求體結構
-
當表單設置
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--
2. 數據分塊傳輸
- 大文件上傳時,HTTP協議支持分塊傳輸(
Transfer-Encoding: chunked
),但PHP會自動重組完整數據。
二、PHP服務端處理機制
1. 接收與解析
- 數據流處理:PHP通過
SAPI
(Server API)接收原始HTTP請求數據。 - 臨時文件生成:
- PHP將上傳的文件內容寫入臨時目錄(
sys_get_temp_dir()
),默認路徑由php.ini
的upload_tmp_dir
指定。 - 臨時文件名隨機生成(如
/tmp/phpA3b4cD
),與原始文件名無關。
- PHP將上傳的文件內容寫入臨時目錄(
2. $_FILES
數組結構
-
PHP自動解析請求體,提取文件信息并填充到
$_FILES
數組中:$_FILES['userfile'] = ['name' => 'photo.jpg', // 客戶端原始文件名'type' => 'image/jpeg', // 瀏覽器提供的MIME類型(可能被篡改)'tmp_name' => '/tmp/phpA3b4cD', // 臨時文件路徑'error' => UPLOAD_ERR_OK, // 錯誤碼'size' => 102400 // 文件大小(字節) ];
3. 臨時文件生命周期
- 自動清理:如果未調用
move_uploaded_file()
,腳本結束時PHP自動刪除臨時文件。 - 手動管理:可通過
register_shutdown_function()
自定義清理邏輯。
三、核心安全機制
1. move_uploaded_file()
的安全性
- 防路徑注入:自動檢查目標路徑是否包含
../
等非法字符。 - 防偽造上傳:驗證文件是否通過HTTP POST上傳(避免直接操作臨時文件)。
2. 文件類型驗證
-
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("非法文件擴展名"); }
3. 防目錄遍歷攻擊
-
使用
basename()
過濾文件名中的路徑符號:$safeFilename = basename($_FILES['file']['name']);
四、服務器配置詳解(php.ini)
配置項 | 默認值 | 作用 |
---|---|---|
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
格式。 - 分塊傳輸至服務器(對用戶透明)。
- 瀏覽器將文件編碼為
- 服務端接收數據:
- Web服務器(如Nginx/Apache)接收原始數據流。
- PHP SAPI解析請求體,生成臨時文件。
- PHP腳本處理:
- 訪問
$_FILES
獲取文件信息。 - 執行錯誤檢查、安全驗證、文件移動。
- 訪問
- 文件持久化存儲:
- 使用
move_uploaded_file()
將文件移至安全目錄。 - 建議存儲路徑與Web根目錄分離(如
/var/uploads/
)。
- 使用
六、高級話題
1. 大文件上傳優化
-
調整配置:
upload_max_filesize = 2G post_max_size = 2G max_execution_time = 3600
-
分片上傳:通過JavaScript實現文件分片,服務端重組。
2. 異步上傳
-
使用AJAX +
FormData
對象實現無刷新上傳:let formData = new FormData(); formData.append('file', fileInput.files[0]); fetch('/upload.php', { method: 'POST', body: formData });
3. 防御0day漏洞
- 禁用危險函數:確保上傳目錄不可執行PHP代碼。
- 內容二次渲染:對圖片文件進行GD庫處理,破壞潛在惡意代碼。
七、錯誤處理深度解析
-
自定義錯誤消息:
$phpFileUploadErrors = [0 => '成功',1 => '文件超過php.ini限制',2 => '文件超過表單限制',3 => '文件僅部分上傳',4 => '未選擇文件',6 => '缺少臨時文件夾',7 => '寫入磁盤失敗',8 => 'PHP擴展阻止了上傳', ];
-
錯誤觸發場景:
UPLOAD_ERR_INI_SIZE
:文件大小超過upload_max_filesize
。UPLOAD_ERR_PARTIAL
:網絡中斷導致上傳不完整。
二,表單制作
一、基礎表單結構
1.必要屬性配置
<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客戶端必填驗證
2.多文件上傳支持
<input type="file" name="files[]" multiple accept=".jpg,.png">
特性說明:
multiple
:允許選擇多個文件accept
:限制可選文件類型(客戶端過濾)
二、高級表單功能
1. 文件類型限制
<!-- 僅允許圖片文件 -->
<input type="file" accept="image/*"><!-- 指定具體擴展名 -->
<input type="file" accept=".pdf,.doc,.docx">
2. 文件大小提示
<input type="file" onchange="checkSize(this)">
<script>
function checkSize(input) {const maxSize = 2 * 1024 * 1024; // 2MBif (input.files[0].size > maxSize) {alert('文件大小超過限制');input.value = ''; // 清空選擇}
}
</script>
3. 拖拽上傳實現
<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>
三、安全增強配置
1. 隱藏域Token驗證
<input type="hidden" name="csrf_token" value="<?= $_SESSION['token'] ?>">
后端驗證:
if ($_POST['csrf_token'] !== $_SESSION['token']) {die("非法請求");
}
2. 文件名過濾處理
// 刪除特殊字符
$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 // 文件大小(字節)]
];
二、核心字段深度解析
1. name
字段
-
來源:客戶端文件系統原始名稱
-
風險:可能包含特殊字符或路徑信息(如
../../shell.php
) -
安全處理:
// 過濾非法字符并提取安全文件名 $safe_name = basename($_FILES['file']['name']); $clean_name = preg_replace('/[^\w\.-]/', '', $safe_name);
2. type
字段
-
來源:瀏覽器根據文件擴展名猜測的類型
-
可靠性:極易偽造(如將.exe文件重命名為.jpg)
-
驗證方法:
$finfo = finfo_open(FILEINFO_MIME_TYPE); $real_mime = finfo_file($finfo, $_FILES['file']['tmp_name']); finfo_close($finfo);
3. tmp_name
字段
- 特性:
- 臨時文件路徑由
php.ini
的upload_tmp_dir
配置決定 - 文件命名規則為
phpXXXXXX
(X為隨機字符)
- 臨時文件路徑由
- 生命周期:
- 腳本執行結束后自動刪除
- 必須使用
move_uploaded_file()
轉移文件
4. 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']]); }
5. size
字段
-
單位:字節(1MB = 1,048,576字節)
-
驗證示例:
$max_size = 5 * 1024 * 1024; // 5MB if ($_FILES['file']['size'] > $max_size) {die("文件大小超過5MB限制"); }
三、保存上傳文件
1. is_uploaded_file()
核心作用
- 驗證指定文件是否通過HTTP POST上傳
- 防止偽造文件路徑攻擊
函數原型
bool is_uploaded_file(string $filename)
使用場景
// 驗證臨時文件合法性
if (!is_uploaded_file($_FILES['file']['tmp_name'])) {die("非法文件來源");
}
安全機制
- 檢查文件路徑是否在
upload_tmp_dir
目錄下 - 驗證文件名匹配PHP臨時文件命名規則(如
phpXXXXXX
) - 防止攻擊者通過偽造路徑訪問系統文件
典型錯誤用法
// 錯誤:直接使用$_FILES中的原始名稱
$tmp = '/tmp/' . $_FILES['file']['name'];
if (file_exists($tmp)) { ... } // 存在路徑注入風險
2. 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;
}
四、調試技巧
1. 打印完整結構
echo '<pre>' . print_r($_FILES, true) . '</pre>';
2. 臨時文件檢查
if (file_exists($_FILES['file']['tmp_name'])) {echo '臨時文件大小: ' . filesize($_FILES['file']['tmp_name']);
} else {echo '臨時文件已消失';
}
3. 上傳限制檢測
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"
- 檢查Web服務器配置(如Nginx的
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
中的客戶端數據 - 必須進行雙重驗證(MIME類型+擴展名+內容檢查)
- 使用
move_uploaded_file()
而非copy()
或rename()
- 上傳目錄設置為不可執行(
chmod 755 uploads/
) - 定期清理舊文件(通過cron作業)
四,多文件上傳
一、前端表單設置
<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
:限制可選文件類型(客戶端過濾)
二、后端文件數據結構
1. 原生$_FILES結構
$_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] // 大小數組]
];
2. 重組為易用格式
$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]
];
三、完整處理流程
1. 驗證上傳狀態
if (empty($_FILES['user_files']['tmp_name'][0])) {die("未選擇任何文件");
}
2. 遍歷處理每個文件
$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'];}
}
3. 輸出結果
echo json_encode(['total' => count($files),'success' => count(array_filter($uploadResults, fn($item) => $item['status'] === 'success')),'results' => $uploadResults
]);
四、高級處理技巧
1. 并發上傳優化
// 使用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);
2. 進度監控實現
// 前端JavaScript
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', e => {const percent = Math.round((e.loaded / e.total) * 100);progressBar.style.width = percent + '%';
});
3. 斷點續傳支持
實現步驟:
- 前端分片文件(使用Blob.slice())
- 服務端記錄已接收分片
- 合并分片文件:
// 合并示例
$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);
}
五、安全防護策略
1. 防御DDoS攻擊
// 限制并發上傳數量
if (count($files) > 10) {http_response_code(429);die("一次最多上傳10個文件");
}
2. 病毒掃描集成
// 使用ClamAV掃描
$clamscan = '/usr/bin/clamscan';
$output = shell_exec("$clamscan --no-summary $targetPath");
if (strpos($output, 'OK') === false) {unlink($targetPath);throw new Exception("文件感染病毒");
}
3. 敏感內容檢測
// 檢查圖片是否包含裸露內容(示例使用NSFW.js)
$imageData = file_get_contents($targetPath);
$nsfwCheck = shell_exec("node nsfw-check.js $imageData");
if ($nsfwCheck > 0.7) {unlink($targetPath);throw new Exception("檢測到違規內容");
}
六、服務器配置優化
php.ini關鍵參數
; 允許同時上傳的文件數
max_file_uploads = 20; 單個文件最大尺寸
upload_max_filesize = 50M; POST數據最大尺寸
post_max_size = 55M; 腳本最大執行時間
max_execution_time = 1800
Nginx配置示例
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');
五,函數封裝
一、函數封裝理論體系
1. 抽象層次模型
- 協議抽象:封裝multipart/form-data解析細節
- 資源管理:統一處理臨時文件生命周期
- 正交設計:驗證邏輯與存儲邏輯解耦
2. 設計模式應用
- 策略模式:可插拔的驗證規則(MIME檢測策略、病毒掃描策略)
- 工廠模式:根據文件類型創建不同的處理器(圖片處理器、文檔處理器)
- 裝飾器模式:動態添加功能(日志記錄、內容過濾)
- 觀察者模式:實現上傳進度通知機制
3. SOLID原則映射
原則 | 實現方式 |
---|---|
單一職責 | 分離驗證、存儲、后處理模塊 |
開閉原則 | 通過繼承擴展功能而非修改源碼 |
里氏替換 | 子類處理器保持父類接口兼容 |
接口隔離 | 定義UploadValidator獨立接口 |
依賴倒置 | 依賴抽象接口而非具體實現 |
二、核心技術實現
1. 安全防御技術棧
-
深度防御模型:
- 文件名消毒:正則過濾
/[^a-z0-9\-_.]/i
- 雙驗證機制:文件簽名+MIME類型
- 沙箱檢測:使用QEMU虛擬環境執行可疑文件
- 權限最小化:上傳目錄
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("非法文件來源");}} }
2. 異步處理架構
-
分片上傳算法:
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)
3. 可觀測性設計
-
指標收集:
# 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"} }
三,FileUploader 類設計
<?php
/*** 安全文件上傳處理器* * 功能特性:* 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)];}
}