構建一個類似文件管理器的 Web 界面,用戶可通過點擊目錄鏈接逐級深入瀏覽服務器文件系統。本文實現一個輕量級的無限級目錄瀏覽器,利用 chdir() 切換工作目錄,結合 opendir()/readdir() 讀取內容,通過 GET 參數傳遞當前路徑,實現無狀態的目錄導航。
<?php
/**
* 無限級目錄瀏覽器
* 安全提示:生產環境必須添加路徑白名單驗證,防止目錄遍歷攻擊
*/
// 設置編碼
header('Content-Type: text/html; charset=utf-8');
// 定義允許訪問的根目錄(安全基線)
$ALLOWED_ROOT = realpath(__DIR__ . '/public_files');
if (!$ALLOWED_ROOT) {
die('配置錯誤:根目錄不存在');
}
// 獲取並驗證請求路徑
$requestPath = $_GET['path'] ?? '.';
$requestPath = urldecode($requestPath);
// 安全驗證:確保路徑在允許範圍內
$realPath = realpath($ALLOWED_ROOT . DIRECTORY_SEPARATOR . $requestPath);
if ($realPath === false || strpos($realPath, $ALLOWED_ROOT) !== 0) {
die('非法路徑訪問');
}
// 切換工作目錄
chdir($realPath);
$currentDir = getcwd();
// 計算相對路徑(用於生成鏈接)
$relativePath = substr($realPath, strlen($ALLOWED_ROOT) + 1);
$relativePath = $relativePath === '' ? '.' : $relativePath;
// 讀取目錄內容
$entries = [];
$handle = opendir('.');
if ($handle) {
while (($entry = readdir($handle)) !== false) {
if ($entry === '.' || $entry === '..') continue;
$fullPath = $entry; // 當前工作目錄下的相對路徑
$isDir = is_dir($fullPath);
$entries[] = [
'name' => $entry,
'is_dir' => $isDir,
'path' => $relativePath === '.' ? $entry : $relativePath . '/' . $entry,
'size' => $isDir ? '-' : filesize($fullPath),
'modified' => date('Y-m-d H:i:s', filemtime($fullPath)),
];
}
closedir($handle);
}
// 排序:目錄優先,然後按名稱字母序
usort($entries, function($a, $b) {
if ($a['is_dir'] !== $b['is_dir']) {
return $b['is_dir'] ? -1 : 1; // 目錄在前
}
return strnatcasecmp($a['name'], $b['name']);
});
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>目錄瀏覽器 - <?= htmlspecialchars($relativePath) ?></title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; margin: 20px; }
.breadcrumb { margin-bottom: 20px; padding: 10px; background: #f5f5f5; border-radius: 4px; }
.breadcrumb a { margin-right: 8px; color: #0066cc; text-decoration: none; }
.breadcrumb a:hover { text-decoration: underline; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 10px; text-align: left; border-bottom: 1px solid #eee; }
th { background: #fafafa; font-weight: 600; }
tr:hover { background: #f9f9f9; }
.dir-name { font-weight: 500; color: #0066cc; }
.file-size { color: #666; font-family: monospace; }
.empty { text-align: center; color: #999; padding: 40px; }
.back-link { display: inline-block; margin-bottom: 15px; color: #0066cc; }
</style>
</head>
<body>
<!-- 麵包屑導航 -->
<div class="breadcrumb">
<strong>當前位置:</strong>
<a href="?path=">根目錄</a>
<?php
if ($relativePath !== '.') {
$parts = explode('/', $relativePath);
$buildPath = '';
foreach ($parts as $part) {
$buildPath .= ($buildPath ? '/' : '') . $part;
echo ' / ' . htmlspecialchars($part) . '';
}
}
?>
</div>
<!-- 返回目錄鏈接 -->
<?php if ($relativePath !== '.'): ?>
<a href="?path== urlencode(dirname($relativePath)) ?>" class="back-link">← 返回上級目錄</a>
<?php endif; ?>
<!-- 文件列表 -->
<?php if (empty($entries)): ?>
<div class="empty">此目錄為空</div>
<?php else: ?>
<table>
<thead>
<tr>
<th>名稱</th>
<th>類型</th>
<th>大小</th>
<th>修改時間</th>
</tr>
</thead>
<tbody>
<?php foreach ($entries as $entry): ?>
<tr>
<td>
<?php if ($entry['is_dir']): ?>
<a href="?path== urlencode($entry['path']) ?>" class="dir-name">
📁 = htmlspecialchars($entry['name']) ?>
</a>
<?php else: ?>
📄 = htmlspecialchars($entry['name']) ?>
<?php endif; ?>
</td>
<td>= $entry['is_dir'] ? '目錄' : '文件' ?></td>
<td class="file-size">
<?php
if (!$entry['is_dir']) {
echo formatBytes($entry['size']);
}
?>
</td>
<td>= $entry['modified'] ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
<?php
// 字節格式化輔助函數
function formatBytes(int $bytes): string {
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
$pow = floor(($bytes > 0 ? log($bytes) : 0) / log(1024));
$pow = min($pow, count($units) - 1);
return round($bytes / (1024 ** $pow), 2) . ' ' . $units[$pow];
}
?>
</body>
</html>
<?php
// 雙重驗證機制
$realPath = realpath($ALLOWED_ROOT . DIRECTORY_SEPARATOR . $requestPath);
// 1. realpath 解析符號鏈接、相對路徑,返回標準絕對路徑
// 2. strpos($realPath, $ALLOWED_ROOT) === 0 確保不越界
⚠️ 切勿直接使用用戶輸入路徑,必須經過 realpath() 規範化並驗證前綴。
chdir($realPath) 將進程工作目錄切換到目標目錄opendir('.')、is_dir()、filesize() 均基於當前工作目錄getcwd() 獲取當前絕對路徑用於顯示將絕對路徑轉換為相對於根目錄的路徑,作為 GET 參數傳遞:
$relativePath = substr($realPath, strlen($ALLOWED_ROOT) + 1);
// 例如:/var/www/public_files/images/avatar.jpg -> images/avatar.jpg
| 功能 | 實現思路 |
|---|---|
| 文件下載 | 新增 download.php?path=...,使用 readfile() + Content-Disposition |
| 圖片預覽 | 判斷 MIME 類型,圖片直接 <img src="?path=...&preview=1"> |
| 新建目錄 | POST 表單提交,調用 mkdir(),需驗證權限 |
| 上傳文件 | enctype="multipart/form-data 表單,move_uploaded_file() 處理 |
| 重命名/刪除 | AJAX 請求,rename()/unlink()/rmdir(),需 CSRF 保護 |
| 搜索過濾 | 前端 JS 過濾或後端 glob() 模糊匹配 |
open_basedir 限制 PHP 訪問範圍