<?php
declare(strict_types=1);
// --- php<8 compatibility shim (deployment build; logic unchanged) ---
if (!function_exists('str_contains')) { function str_contains($h, $n) { return $n === '' || strpos($h, $n) !== false; } }
if (!function_exists('str_starts_with')) { function str_starts_with($h, $n) { return $n === '' || strncmp($h, $n, strlen($n)) === 0; } }
if (!function_exists('str_ends_with')) { function str_ends_with($h, $n) { return $n === '' || substr($h, -strlen($n)) === $n; } }
// --- end shim ---
@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' => 'File Manager',
'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');
}
$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);
}
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($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);
}
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
{
$m = [
UPLOAD_ERR_INI_SIZE => 'Exceeds php.ini upload_max_filesize',
UPLOAD_ERR_FORM_SIZE => 'Exceeds the size allowed by the form',
UPLOAD_ERR_PARTIAL => 'File was only partially uploaded',
UPLOAD_ERR_NO_FILE => 'No file selected',
UPLOAD_ERR_NO_TMP_DIR => 'Server is missing a temporary directory',
UPLOAD_ERR_CANT_WRITE => 'Server failed to write to disk',
UPLOAD_ERR_EXTENSION => 'Interrupted by the PHP extension',
];
return isset($m[$c]) ? $m[$c] : 'Unknown upload error(' . $c . ')';
}
$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;
}
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 true;
}
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');
$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('Form token validation failed; please refresh the page and retry', '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('Unknown action: ' . $act, 'err');
redirect_to($rel, $QS);
} elseif ($IS_POST) {
flash('Unknown action: ' . $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('Target directory not found or out of the allowed scope: ' . disp_path($target), 'err');
redirect_to($back, $QS);
}
if (!is_writable($dirAbs)) {
flash('Target directory not writable: ' . 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('Invalid file name', 'err');
redirect_to($back, $QS);
}
$dest = $dirAbs . '/' . $name;
if (!inside($dest)) {
flash('Target path out of bounds, blocked: ' . $name, 'err');
redirect_to($back, $QS);
}
if ($mode === 'replace') {
if (!is_file($dest)) {
flash('Target file not found, cannot replace: ' . $name, 'err');
redirect_to($back, $QS);
}
} elseif (file_exists($dest) && !gp_has('overwrite')) {
flash('File already exists, not overwritten (check "Overwrite existing" to force replace): ' . $name, 'err');
redirect_to($back, $QS);
}
if (!isset($_FILES['file']['tmp_name'])) {
flash('No uploaded file received', '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 failed: ' . upload_err_text($code), 'err');
redirect_to($back, $QS);
}
$tmp = (string)$_FILES['file']['tmp_name'];
if (!is_uploaded_file($tmp)) {
flash('Upload temp-file validation failed', 'err');
redirect_to($back, $QS);
}
$old = file_exists($dest) ? (int)@filesize($dest) : 0;
if (!@move_uploaded_file($tmp, $dest)) {
flash('Write failed (directory permission?): ' . $dest, 'err');
redirect_to($back, $QS);
}
@chmod($dest, 0644);
$new = (int)@filesize($dest);
flash(($mode === 'replace' ? 'Replace succeeded: ' : 'Upload succeeded: ') . $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('File not found: ' . disp_path($f), 'err');
redirect_to(parent_rel($f) ?? '', $QS);
}
if (!is_writable($abs)) {
flash('FileNot writable: ' . 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('Content exceeds the online edit limit ' . fmt_size($max), 'err');
redirect_to(parent_rel($f) ?? '', $QS);
}
$n = @file_put_contents($abs, $data, LOCK_EX);
if ($n === false) {
flash('Save failed: ' . disp_path($f), 'err');
} else {
flash('Saved: ' . basename($f) . ' (wrote ' . 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('Target directory not found', 'err');
redirect_to($dir, $QS);
}
if ($name === '' || $name === '.' || $name === '..') {
flash('Invalid directory name', 'err');
redirect_to($dir, $QS);
}
$dest = $abs . '/' . $name;
if (file_exists($dest)) {
flash('A file or directory with that name already exists: ' . $name, 'err');
redirect_to($dir, $QS);
}
if (!@mkdir($dest, 0755, true)) {
flash('Create directory failed (permission?): ' . $name, 'err');
redirect_to($dir, $QS);
}
flash('Directory created: ' . $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('Target directory not found', 'err');
redirect_to($dir, $QS);
}
if ($name === '' || $name === '.' || $name === '..') {
flash('Invalid file name', 'err');
redirect_to($dir, $QS);
}
$dest = $abs . '/' . $name;
if (file_exists($dest) && !gp_has('overwrite')) {
flash('File already exists (check overwrite to force write): ' . $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('Create file failed (permission?): ' . $name, 'err');
redirect_to($dir, $QS);
}
flash('File created: ' . $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('Target not found: ' . disp_path($f), 'err');
redirect_to($parent, $QS);
}
if (basename($abs) === basename(__FILE__)) {
flash('Cannot rename the manager itself', 'err');
redirect_to($parent, $QS);
}
$new = basename(str_replace('\\', '/', trim(gp('newname', ''))));
if ($new === '' || $new === '.' || $new === '..') {
flash('Invalid new name', 'err');
redirect_to($parent, $QS);
}
if ($new === basename($abs)) {
flash('Name unchanged', 'err');
redirect_to($parent, $QS);
}
$dest = dirname($abs) . '/' . $new;
if (file_exists($dest)) {
flash('Target name already exists: ' . $new, 'err');
redirect_to($parent, $QS);
}
if (!@rename($abs, $dest)) {
flash('Rename failed (permission?): ' . basename($f), 'err');
redirect_to($parent, $QS);
}
flash('Renamed: ' . 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('Target not found: ' . disp_path($f), 'err');
redirect_to($parent, $QS);
}
if (basename($abs) === basename(__FILE__)) {
flash('Cannot delete the manager itself', 'err');
redirect_to($parent, $QS);
}
if (!is_writable(dirname($abs))) {
flash('Parent directory not writable, cannot delete: ' . disp_path($f), 'err');
redirect_to($parent, $QS);
}
$isDir = is_dir($abs);
$ok = rm_rf($abs);
flash($ok ? ('Deleted' . ($isDir ? 'Directory' : 'File') . ': ' . basename($f))
: ('Delete failed (permission?): ' . basename($f)), $ok ? 'ok' : 'err');
redirect_to($parent, $QS);
}
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: File not found or unreadable\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;
}
$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">📁 File Manager</div>';
if ($ALLOW_ABOVE) {
echo '<div class="kv">Initial directory (this script\'s directory): <code>' . h($ROOT) . '</code>'
. ' · Can browse up to <code>/</code>, no root restriction</div>';
} else {
echo '<div class="kv">Root path (this script\'s directory): <code>' . h($ROOT) . '</code>'
. ' · Browsing is locked to this directory</div>';
}
echo '<div class="kv">Current directory: <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))) . '">⬆ Parent directory</a>';
} else {
echo '<span class="btn muted" style="opacity:.5;cursor:not-allowed">⬆ Parent directory (already at filesystem root /)</span>';
}
if ($rel !== $HOME_REL) {
echo '<a class="btn" href="' . h(url(array_merge(['p' => $HOME_REL], $QS))) . '">🏠 Initial directory</a>';
}
echo '<a class="btn" href="' . h(url(array_merge(['p' => $rel], $QS))) . '">🔄 Refresh</a>';
echo '<a class="btn" href="' . h(url(array_merge(['a' => 'mkdir', 'p' => $rel]))) . '">➕ New directory</a>';
echo '<a class="btn" href="' . h(url(array_merge(['a' => 'newfile', 'p' => $rel]))) . '">📄 New file</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="Enter a path to jump to, e.g. /etc or ../xxx">';
echo '<button class="btn" type="submit">Go</button>';
echo '</form>';
$sortLabels = ['name' => 'Name', 'size' => 'Size', 'time' => 'Time', 'type' => 'Type'];
echo '<span class="kv">Sort: </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>⬆ Upload a file into the current directory / Overwrite existing file</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="Save filename (leave empty= with the original name)">';
echo '<label class="chk"><input type="checkbox" name="overwrite" value="1"> Overwrite existing file</label>';
echo '<button class="btn p" type="submit">Upload</button>';
echo '</form>';
echo '<div class="kv">Upload target directory: <code>' . h(disp_path($rel)) . '</code> ('
. (is_writable(disp_path($rel)) ? 'Writable' : 'Not writable') . ')</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;
}
function render_list(string $rel): void
{
global $CFG, $QS, $HOME_REL;
$abs = resolve_path($rel);
if ($abs === null || !is_dir($abs)) {
flash('Directory not found or unreadable; returned to the initial directory', '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">Directory unreadable (insufficient permission): ' . 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">Total ' . count($rows) . ' items (directories ' . $nDir . ' / File ' . $nFile
. ', files total ' . h(fmt_size($nSize)) . ')</div>';
echo '<table><thead><tr>';
echo '<th style="width:46%">Name</th><th>Size</th><th>Modified</th><th>Perm</th><th style="width:26%">Actions</th>';
echo '</tr></thead><tbody>';
if (count($rows) === 0) {
echo '<tr><td colspan="5" class="muted" style="text-align:center;padding:22px">Empty directory</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="Download" 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="Not writable">🔒</span>') . '</td>';
echo '<td class="act">';
if ($r['dir']) {
echo '<a href="' . h(url(array_merge(['p' => $r['rel']], $QS))) . '">Enter</a>';
} else {
echo '<a href="' . h(url(['a' => 'view', 'f' => $r['rel']])) . '">View</a>';
echo '<a href="' . h(url(['a' => 'edit', 'f' => $r['rel']])) . '">Edit</a>';
echo '<a href="' . h(url(['a' => 'download', 'f' => $r['rel']])) . '">Download</a>';
echo '<a href="' . h(url(['a' => 'replace', 'f' => $r['rel']])) . '">Replace</a>';
}
echo '<a href="' . h(url(['a' => 'rename', 'f' => $r['rel']])) . '">Rename</a>';
echo '<a style="color:#ff9b9b" href="' . h(url(['a' => 'delete', 'f' => $r['rel']])) . '">Delete</a>';
echo '</td></tr>';
}
echo '</tbody></table>';
page_foot();
}
function render_view(string $f): void
{
global $CFG;
$abs = resolve_path($f);
if ($f === '' || $abs === null || !is_file($abs)) {
page_top('View');
msg_box();
echo '<div class="msg err">File not found: ' . h(disp_path($f)) . '</div>';
echo '<p><a class="btn" href="' . h(url(['p' => parent_rel($f) ?? ''])) . '">← Back to directory</a></p>';
page_foot();
return;
}
$name = basename($abs);
$size = (int)@filesize($abs);
page_top('View ' . $name);
msg_box();
echo '<div class="bar"><div class="brand">👁 View: ' . h($name) . '</div>';
echo '<div class="kv">Path: <code>' . h($abs) . '</code></div>';
echo '<div class="kv">Size: ' . h(fmt_size($size)) . ' · Modified: ' . h(fmt_time((int)@filemtime($abs)))
. ' · Perm: ' . h(perm_str($abs)) . ' · ' . (is_writable($abs) ? 'Writable' : 'Read-only') . '</div>';
echo '<div class="toolbar">';
echo '<a class="btn p" href="' . h(url(['p' => parent_rel($f) ?? ''])) . '">← Back to directory</a>';
echo '<a class="btn" href="' . h(url(['a' => 'download', 'f' => $f])) . '">⬇ Download</a>';
echo '<a class="btn" href="' . h(url(['a' => 'edit', 'f' => $f])) . '">✏️ Edit</a>';
echo '<a class="btn" href="' . h(url(['a' => 'replace', 'f' => $f])) . '">⬆ UploadReplace</a>';
echo '<a class="btn" href="' . h(url(['a' => 'rename', 'f' => $f])) . '">🏷 Rename</a>';
echo '<a class="btn d" href="' . h(url(['a' => 'delete', 'f' => $f])) . '">🗑 Delete</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">File exceeds ' . h(fmt_size($max)) . ', not previewed here; please download it.</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">Binary file, cannot preview as text. Please download it.</div>';
page_foot();
return;
}
echo '<div class="panel"><pre class="view">' . h($content) . '</pre></div>';
page_foot();
}
function render_edit(string $f): void
{
global $CFG;
$abs = resolve_path($f);
if ($f === '' || $abs === null || !is_file($abs)) {
page_top('Edit');
msg_box();
echo '<div class="msg err">File not found: ' . h(disp_path($f)) . '</div>';
echo '<p><a class="btn" href="' . h(url(['p' => parent_rel($f) ?? ''])) . '">← Back to directory</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('Edit ' . $name);
msg_box();
echo '<div class="bar"><div class="brand">✏️ Edit: ' . h($name) . '</div>';
echo '<div class="kv">Path: <code>' . h($abs) . '</code></div>';
echo '<div class="kv">Size: ' . h(fmt_size($size)) . ' · Modified: ' . h(fmt_time((int)@filemtime($abs)))
. ' · Perm: ' . h(perm_str($abs)) . ' · ' . (is_writable($abs) ? 'Writable' : 'Read-only (save will fail)') . '</div>';
echo '<div class="toolbar">';
echo '<a class="btn p" href="' . h(url(['p' => parent_rel($f) ?? ''])) . '">← Back to directory</a>';
echo '<a class="btn" href="' . h(url(['a' => 'view', 'f' => $f])) . '">👁 View</a>';
echo '<a class="btn" href="' . h(url(['a' => 'download', 'f' => $f])) . '">⬇ Download</a>';
echo '</div></div>';
if ($tooBig) {
echo '<div class="msg err">File exceeds the online edit limit ' . h(fmt_size($max)) . '; download and edit locally, then write it back with "Upload & replace".</div>';
page_foot();
return;
}
if ($binary) {
echo '<div class="msg err">Binary content detected; saving may corrupt the file. Use "Upload & replace" instead.</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">💾 Save</button>';
echo '<a class="btn" href="' . h(url(['a' => 'view', 'f' => $f])) . '">Cancel</a>';
echo '<span class="kv">Save as UTF-8, newlines normalized to LF</span></div>';
echo '</form>';
page_foot();
}
function render_replace(string $f): void
{
$abs = resolve_path($f);
if ($f === '' || $abs === null || !is_file($abs)) {
page_top('UploadReplace');
msg_box();
echo '<div class="msg err">File not found: ' . h(disp_path($f)) . '</div>';
echo '<p><a class="btn" href="' . h(url(['p' => parent_rel($f) ?? ''])) . '">← Back to directory</a></p>';
page_foot();
return;
}
$parent = parent_rel($f);
if ($parent === null) {
$parent = '';
}
page_top('UploadReplace ' . basename($abs));
msg_box();
echo '<div class="bar"><div class="brand">⬆ Upload & replace: ' . h(basename($abs)) . '</div>';
echo '<div class="kv">Target file: <code>' . h($abs) . '</code></div>';
echo '<div class="kv">Current size: ' . h(fmt_size((int)@filesize($abs))) . ' · Perm: ' . h(perm_str($abs))
. ' · ' . (is_writable($abs) ? 'Writable' : 'Not writable (replace will fail)') . '</div>';
echo '</div>';
echo '<div class="panel"><h3>Choose a local file to overwrite this file</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">Upload and replace</button>';
echo '<a class="btn" href="' . h(url(['a' => 'view', 'f' => $f])) . '">Cancel</a>';
echo '</form>';
echo '<div class="kv">The original file will be fully overwritten; this cannot be undone.</div>';
echo '</div>';
page_foot();
}
function render_mkdir_page(string $rel): void
{
global $QS;
page_top('New directory');
msg_box();
echo '<div class="bar"><div class="brand">➕ Create a directory below</div>';
echo '<div class="kv">Location: <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="Directory name" required autofocus>';
echo '<button class="btn p" type="submit">Create</button>';
echo '<a class="btn" href="' . h(url(array_merge(['p' => $rel], $QS))) . '">Back</a>';
echo '</form></div>';
page_foot();
}
function render_newfile_page(string $rel): void
{
global $QS;
page_top('New file');
msg_box();
echo '<div class="bar"><div class="brand">📄 Create a file below</div>';
echo '<div class="kv">Location: <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="file name, e.g. index.php" required autofocus>';
echo '<label class="chk"><input type="checkbox" name="overwrite" value="1"> Overwrite existing file</label></div>';
echo '<textarea name="content" spellcheck="false" placeholder="Initial content (optional)"></textarea>';
echo '<div class="toolbar"><button class="btn p" type="submit">Create</button>';
echo '<a class="btn" href="' . h(url(array_merge(['p' => $rel], $QS))) . '">Back</a></div>';
echo '</form></div>';
page_foot();
}
function render_rename_page(string $f): void
{
$abs = resolve_path($f);
if ($f === '' || $abs === null || !file_exists($abs)) {
page_top('Rename');
msg_box();
echo '<div class="msg err">Target not found: ' . h(disp_path($f)) . '</div>';
echo '<p><a class="btn" href="' . h(url(['p' => parent_rel($f) ?? ''])) . '">← Back to directory</a></p>';
page_foot();
return;
}
$parent = parent_rel($f);
if ($parent === null) {
$parent = '';
}
page_top('Rename');
msg_box();
echo '<div class="bar"><div class="brand">🏷 Rename</div>';
echo '<div class="kv">Original path: <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">Rename</button>';
echo '<a class="btn" href="' . h(url(['p' => $parent])) . '">Back</a>';
echo '</form><div class="kv">You can only rename within the current directory; moving to another directory is not allowed.</div></div>';
page_foot();
}
function render_delete_page(string $f): void
{
$abs = resolve_path($f);
if ($f === '' || $abs === null || !file_exists($abs)) {
page_top('Delete');
msg_box();
echo '<div class="msg err">Target not found: ' . h(disp_path($f)) . '</div>';
echo '<p><a class="btn" href="' . h(url(['p' => parent_rel($f) ?? ''])) . '">← Back to directory</a></p>';
page_foot();
return;
}
$parent = parent_rel($f);
if ($parent === null) {
$parent = '';
}
$isDir = is_dir($abs);
page_top('Confirm deletion');
msg_box();
echo '<div class="bar"><div class="brand">🗑 Confirm deletion</div>';
echo '<div class="kv">Target: <code>' . h($abs) . '</code>' . ($isDir ? ' (directory, including all its contents)' : '') . '</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">Confirm delete</button>';
echo '<a class="btn" href="' . h(url(['p' => $parent])) . '">Cancel</a>';
echo '</form><div class="kv">Deletion cannot be undone.</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;
}