許多 CMS (如 DedeCMS) 內建檔案管理器,支援圖片/檔案上傳、目錄瀏覽、檔案刪除、重命名衝突處理,並能整合編輯器 (CKEditor、FCKeditor) 回填路徑。本文架構一個輕量級 PHP 檔案管理模組,採用單一進入點、Action 分發模式,核心功能包括:
file(1).jpg)<?php
// config.php - 集中配置
define('ROOT', str_ireplace('/manager/' . basename(__FILE__), '', str_replace('\\', '/', __FILE__)));
define('UPLOAD_ROOT', '/uploadfile/'); // 相對網站根目錄
define('ALLOWED_TYPES', 'gif|jpg|png|txt|doc|xls|ppt|pdf|rar|zip|7z|chm|mp3|mp4|mpg|mpeg|flv|swf');
define('MAX_SIZE', ini_get('upload_max_filesize')); // 讀取 php.ini 設定
// 依據 act 參數分發
$act = $_GET['act'] ?? '';
switch ($act) {
case '': renderFrame(); break; // 主框架 (含左側目錄樹、右側 iframe)
case 'left': renderLeftTree(); break; // 左側目錄樹
case 'right': handleRightPanel(); break; // 右側列表/上傳/刪除邏輯
default: http_response_code(404); exit('Invalid Action');
}
function renderFrame() {
// 輸出 frameset 或現代 layout (建議使用 CSS Grid/Flex + iframe)
// 左側 src="?act=left",右側 src="?act=right"
}
?>
function handleRightPanel() {
// 1. 處理上傳 POST 請求
if (!empty($_POST['upfolder']) && $_SERVER['REQUEST_METHOD'] === 'POST') {
uploadFile();
exit;
}
$dir = $_GET['folder'] ?? '';
$targetDir = $dir === '' ? UPLOAD_ROOT : $dir;
// 2. 處理刪除
if (($del = $_GET['del_file'] ?? '') !== '') {
deleteFile($targetDir, $del);
// 刪除後重導回列表
header("Location: ?act=right&folder=" . urlencode($dir));
exit;
}
// 3. 渲染檔案列表
renderFileList($targetDir, $dir);
}
function uploadFile() {
$upDir = ROOT . UPLOAD_ROOT . ($_POST['upfolder'] ?? '');
if (!is_dir($upDir)) mkdir($upDir, 0755, true);
$file = $_FILES['newfile'] ?? null;
if (!$file || $file['error'] !== UPLOAD_ERR_OK) {
exit(json_encode(['error' => '上傳失誤或無檔案']));
}
// 副檔名驗證 (雙重防護)
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (!preg_match('/^(' . ALLOWED_TYPES . ')$/i', $ext)) {
exit(json_encode(['error' => '禁止的檔案類型']));
}
// MIME 類型二次驗證 (建議使用 finfo)
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($file['tmp_name']);
$allowedMimes = ['image/gif', 'image/jpeg', 'image/png', 'application/pdf', /* ... */];
if (!in_array($mime, $allowedMimes)) {
exit(json_encode(['error' => '檔案內容類型不符']));
}
// 檔案大小檢查
if ($file['size'] > parseSize(MAX_SIZE)) {
exit(json_encode(['error' => '檔案過大']));
}
// 衝突重命名: file.jpg -> file(1).jpg
$baseName = pathinfo($file['name'], PATHINFO_FILENAME);
$dest = $upDir . '/' . $file['name'];
$counter = 1;
while (file_exists($dest)) {
$dest = $upDir . '/' . $baseName . '(' . $counter++ . ').' . $ext;
}
if (move_uploaded_file($file['tmp_name'], $dest)) {
@chmod($dest, 0644);
// 回傳給編輯器所需格式 (視編輯器 API 調整)
exit(json_encode(['url' => UPLOAD_ROOT . basename($dest), 'name' => basename($dest)]));
}
exit(json_encode(['error' => '移動檔案失敗']));
}
function parseSize($str) { // 將 2M, 50K 轉換為位元組
$unit = strtolower(substr($str, -1));
$val = (int)substr($str, 0, -1);
return match($unit) { 'k' => $val * 1024, 'm' => $val * 1024 * 1024, 'g' => $val * 1024 * 1024 * 1024, default => $val };
}
function renderFileList($absPath, $relPath) {
$items = [];
$dh = opendir($absPath);
while (($file = readdir($dh)) !== false) {
if ($file === '.' || $file === '..') continue;
$full = $absPath . '/' . $file;
$isDir = is_dir($full);
$items[] = [
'name' => $file,
'is_dir' => $isDir,
'size' => $isDir ? '-' : formatSize(filesize($full)),
'mtime' => date('Y-m-d H:i', filemtime($full)),
'url' => $relPath . '/' . $file, // 相對網站根目錄 URL
'abs' => $full
];
}
closedir($dh);
// 排序: 目錄優先,按名稱自然排序
usort($items, fn($a, $b) => ($a['is_dir'] === $b['is_dir']) ? strnatcasecmp($a['name'], $b['name']) : ($b['is_dir'] ? 1 : -1));
// 輸出 HTML (或 JSON 供前端 Vue/React 渲染)
// 此處省略 HTML 模板,重點為資料結構
// 刪除連結: ?act=right&folder={$relPath}&del_file={$file}
// 進入子目錄: ?act=right&folder={$relPath}/{$file}
}
function formatSize($bytes) { /* ... */ }
| 風險 | 對策 |
|---|---|
| 路徑穿越 (../etc/passwd) | 使用 realpath() 正規化路徑,嚴格檢查是否在 UPLOAD_ROOT 之下 |
| 腳本執行攻擊 | 上傳目錄設定 Nginx/Apache 禁止 PHP 執行 (location ~ \.php$ { deny all; }) |
| MIME 偽裝 | 使用 finfo 檢測真實內容類型,不信任 $_FILES['type'] |
| 未授權存取 | 整合 CMS 權限系統,驗證登入狀態與角色 (admin/editor) |
| CSRF | 上傳/刪除表單加入 Token 驗證 |
現代專案建議採用 前後端分離:後端提供 RESTful API (GET /api/files?dir=..., POST /api/upload, DELETE /api/files/{id}),前端使用 Vue 3 + Element Plus 或 React + Ant Design 實作檔案管理器介面,支援拖曳上傳、多選刪除、右鍵選單、縮圖預覽等豐富交互。若需整合 CKEditor 5,可使用其官方 CKBox 或自訂 FileRepository 接口對接此 API。
檔案管理模組看似簡單,實則涉及路徑安全、權限控管、編碼相容、前端整合等多層面向。採用「單一進入點 + Action 分發 + 服務層解耦」的結構,配合嚴格的輸入驗證與輸出編碼,可構建出安全且可維護的檔案管理核心,作為 CMS、後台系統、編輯器插件的堅實基礎設施。