enctype="multipart/form-data" — 必須,否則 $_FILES 為空。name="files[]" — 陣列命名,PHP 自動整理為結構化陣列。multiple 屬性 (HTML5) — 允許使用者一次選擇多個檔案。<form action="upload.php" method="post" enctype="multipart/form-data">
<input type="file" name="files[]" multiple accept="image/*,.pdf">
<button type="submit">上傳</button>
</form>
<?php
// upload.php
const UPLOAD_DIR = __DIR__ . '/uploads/';
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'application/pdf'];
const ALLOWED_EXTS = ['jpg', 'jpeg', 'png', 'gif', 'pdf'];
// 確保目錄存在
if (!is_dir(UPLOAD_DIR)) mkdir(UPLOAD_DIR, 0755, true);
$errors = [];
$uploaded = [];
// 標準化 $_FILES 結構 (處理單檔/多檔、多輸入欄位)
$files = [];
foreach ($_FILES as $fieldName => $fileData) {
if (isset($fileData['name'][0])) {
// 多檔案:name="files[]"
$count = count($fileData['name']);
for ($i = 0; $i < $count; $i++) {
$files[] = [
'name' => $fileData['name'][$i],
'type' => $fileData['type'][$i],
'tmp_name' => $fileData['tmp_name'][$i],
'error' => $fileData['error'][$i],
'size' => $fileData['size'][$i],
'field' => $fieldName,
];
}
} else {
// 單檔案:name="file"
$files[] = $fileData + ['field' => $fieldName];
}
}
foreach ($files as $file) {
// 1. 錯誤碼檢查
if ($file['error'] !== UPLOAD_ERR_OK) {
$errors[] = "{$file['name']}: " . upload_error_message($file['error']);
continue;
}
// 2. 檔案大小
if ($file['size'] > MAX_FILE_SIZE) {
$errors[] = "{$file['name']}: 檔案過大 (限制 " . (MAX_FILE_SIZE/1024/1024) . "MB)";
continue;
}
// 3. MIME 型別驗證 (建議搭配 finfo 實際檢測)
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($file['tmp_name']);
if (!in_array($mime, ALLOWED_TYPES, true)) {
$errors[] = "{$file['name']}: 檔案類型不允許 ($mime)";
continue;
}
// 4. 副檔名驗證 (雙重保險)
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (!in_array($ext, ALLOWED_EXTS, true)) {
$errors[] = "{$file['name']}: 副檔名不允許 ($ext)";
continue;
}
// 5. 產生安全檔名 (避免覆蓋、路徑遍歷、特殊字元)
$basename = pathinfo($file['name'], PATHINFO_FILENAME);
$safeName = preg_replace('/[^a-zA-Z0-9_\-]/', '', $basename);
$safeName = substr($safeName, 0, 50); // 限制長度
$newName = $safeName . '_' . bin2hex(random_bytes(8)) . '.' . $ext;
$dest = UPLOAD_DIR . $newName;
// 6. 移動檔案 (原子操作)
if (!move_uploaded_file($file['tmp_name'], $dest)) {
$errors[] = "{$file['name']}: 儲存失敗";
continue;
}
// 7. 可選:圖片二次驗證 (getimagesize) 、病毒掃描、縮圖生成
if (str_starts_with($mime, 'image/')) {
$imgInfo = @getimagesize($dest);
if (!$imgInfo) {
unlink($dest);
$errors[] = "{$file['name']}: 非有效圖片檔案";
continue;
}
}
$uploaded[] = [
'original' => $file['name'],
'stored' => $newName,
'path' => $dest,
'size' => $file['size'],
'mime' => $mime,
'url' => '/uploads/' . $newName, // 前端訪問路徑
];
}
// 回應
header('Content-Type: application/json');
echo json_encode([
'success' => empty($errors),
'uploaded' => $uploaded,
'errors' => $errors,
], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
function upload_error_message(int $code): string {
return match ($code) {
UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE => '超過大小限制',
UPLOAD_ERR_PARTIAL => '上傳不完整',
UPLOAD_ERR_NO_FILE => '未選擇檔案',
UPLOAD_ERR_NO_TMP_DIR => '伺服器臨時目錄缺失',
UPLOAD_ERR_CANT_WRITE => '寫入失敗',
UPLOAD_ERR_EXTENSION => '擴充模組中斷上傳',
default => '未知錯誤',
};
}
?>
<script>
document.querySelector('form').addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const res = await fetch('upload.php', { method: 'POST', body: formData });
const data = await res.json();
if (data.success) {
alert(`上傳成功 ${data.uploaded.length} 個檔案`);
data.uploaded.forEach(f => {
// 顯示縮圖/連結
const div = document.createElement('div');
div.innerHTML = `${f.original} (${(f.size/1024).toFixed(1)} KB)`;
document.body.appendChild(div);
});
} else {
alert('上傳失敗:\n' + data.errors.join('\n'));
}
});
</script>
對於 >100MB 檔案,需前端切片 (Blob.slice)、後端合併:
Uppy、Dropzone、FilePond、Resumable.js。league/flysystem + 自定義分塊合併邏輯,或使用 tus-php 實作 tus.io 標準。| 層面 | 措施 |
|---|---|
| 檔案型別 | finfo 偵測真實 MIME + 副檔名白名單 |
| 檔案大小 | PHP upload_max_filesize/post_max_size + 程式碼二次檢查 |
| 檔名安全 | 隨機重命名、移除特殊字元、禁止路徑遍歷 (../) |
| 儲存位置 | 非 Web 可直接執行目錄、或 Nginx location /uploads { internal; } |
| 執行權限 | 上傳目錄禁用腳本執行 (PHP、CGI 等) |
| 病毒掃描 | ClamAV 整合 (clamav 擴充或 clamscan 呼叫) |
| CSRF | 表單 Token / SameSite Cookie |
| 速率限制 | IP/用戶上傳頻率與總量限制 |
$_FILES 為空 → 檢查 enctype、post_max_size、upload_max_filesize、表單 method="POST"。max_execution_time、memory_limit、Nginx client_max_body_size。PHP 多檔案上傳的核心在於正確構建 $_FILES 陣列結構、嚴格驗證 (MIME/大小/副檔名)、安全重命名與儲存。現代開發建議採用 單一 multiple 輸入欄位 + AJAX 非同步上傳 + JSON 回應,前端體驗佳、後端邏輯簡潔。大檔案需求請採用分塊上傳庫 (Uppy/tus-php)。安全性方面,**MIME 偵測 + 非執行目錄 + 病毒掃描** 三層防護為基礎標準。