<?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>'
  . ' &middot; 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>'
  . ' &middot; 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)) . ' &middot; Modified: ' . h(fmt_time((int)@filemtime($abs)))
  . ' &middot; Perm: ' . h(perm_str($abs)) . ' &middot; ' . (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)) . ' &middot; Modified: ' . h(fmt_time((int)@filemtime($abs)))
  . ' &middot; Perm: ' . h(perm_str($abs)) . ' &middot; ' . (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))) . ' &middot; Perm: ' . h(perm_str($abs))
  . ' &middot; ' . (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;
}
