PHP rmdir() 僅能刪除空目錄,刪除包含檔案/子目錄的目錄需遞迴處理。原始函數缺少路徑安全驗證,易導致誤刪系統關鍵目錄。
<?php
function dir_delete($file) {
if (file_exists($file)) {
if (is_dir($file)) {
$handle = opendir($file);
while (false !== ($filename = readdir($handle))) {
if ($filename != "." && $filename != "..")
$this->dir_delete($file . "/" . $filename); // $this 在函數外無效
}
closedir($handle);
rmdir($file);
return true;
} else {
unlink($file);
}
}
}
?>
缺陷:$this->dir_delete 在普通函數中報錯;無路徑正規化與基準目錄檢查;無錯誤處理;無符號連結處理。
<?php
/**
* 遞迴刪除目錄或檔案
* @param string $path 目標路徑
* @param string $baseDir 允許操作的基準目錄 (防路徑遍歷)
* @return bool 成功與否
* @throws InvalidArgumentException 路徑非法
*/
function recursiveDelete(string $path, string $baseDir = ''): bool {
// 1. 路徑正規化
$path = realpath($path);
if ($path === false) return false; // 不存在
// 2. 基準目錄檢查
if ($baseDir) {
$baseDir = realpath($baseDir);
if ($baseDir === false || strpos($path, $baseDir) !== 0) {
throw new InvalidArgumentException("路徑超出允許範圍: $path");
}
}
// 3. 符號連結:僅刪除連結本身,不遞迴目標
if (is_link($path)) {
return unlink($path);
}
// 4. 目錄遞迴
if (is_dir($path)) {
$files = array_diff(scandir($path), ['.', '..']);
foreach ($files as $file) {
$childPath = $path . DIRECTORY_SEPARATOR . $file;
if (!recursiveDelete($childPath, $baseDir)) {
// 可選:記錄失敗、繼續或拋出異常
error_log("刪除失敗: $childPath");
return false;
}
}
return rmdir($path);
}
// 5. 檔案刪除
if (is_file($path)) {
return unlink($path);
}
return false;
}
// 使用
try {
$deleted = recursiveDelete('/var/www/uploads/temp_123', '/var/www/uploads');
echo $deleted ? '刪除成功' : '刪除失敗';
} catch (InvalidArgumentException $e) {
echo '安全攔截: ' . $e->getMessage();
}
?>
| 防護 | 實作 |
|---|---|
| 路徑遍歷 | realpath() 解析後檢查 strpos($path, $baseDir) === 0 |
| 符號連結 | is_link() 僅 unlink() 連結檔,不跟隨目標 |
| 權限檢查 | 刪除前可加 is_writable() 或嘗試 touch() 驗證 |
| 錯誤處理 | 回傳 bool + error_log,或拋出異常讓上層決定 |
| 效能 | 大量檔案考慮 exec('rm -rf ' . escapeshellarg($path)) (需極謹慎驗證) |
<?php
use RecursiveIteratorIterator;
use RecursiveDirectoryIterator;
use FilesystemIterator;
function recursiveDeleteSPL(string $path, string $baseDir = ''): bool {
$path = realpath($path);
if (!$path) return false;
if ($baseDir && strpos($path, realpath($baseDir)) !== 0) {
throw new InvalidArgumentException("路徑非法");
}
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS),
RecursiveIteratorIterator::CHILD_FIRST // 先刪子項
);
foreach ($iterator as $file) {
if ($file->isDir()) {
rmdir($file->getPathname());
} else {
unlink($file->getPathname());
}
}
return rmdir($path);
}
?>
<?php
// 建立測試結構
$base = sys_get_temp_dir() . '/del_test_' . uniqid();
mkdir("$base/a/b/c", 0755, true);
file_put_contents("$base/a/file1.txt", 'test');
file_put_contents("$base/a/b/file2.txt", 'test');
symlink("$base/a", "$base/link_to_a");
recursiveDelete($base); // 應完全刪除,不影響 link_to_a 指向的原目錄 (已一同刪除)
// 若要保留符號連結目標,需在 is_link 判斷處理
?>
遞迴刪除是高風險操作,務必實作基準目錄鎖定、符號連結隔離、錯誤記錄與權限驗證。生產環境建議使用 SPL 迭代器版本,程式碼更簡潔、行為更可預期。