🐘
index_copy4.php
Back
📝 Php ⚡ Executable Ctrl+S: Save • Ctrl+R: Run • Ctrl+F: Find
<?php // Force login + disable caching session_start(); require_once __DIR__ . '/../core/db_config.php'; // Anti-cache headers header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0'); header('Cache-Control: post-check=0, pre-check=0', false); header('Pragma: no-cache'); header('Expires: 0'); // Redirect if not logged in if (empty($_SESSION['username'])) { $redirect = urlencode($_SERVER['REQUEST_URI'] ?? '/'); header("Location: /core/auth/login.php?redirect={$redirect}"); exit; } // Handle SFTP API requests if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['sftp_action'])) { header('Content-Type: application/json'); error_reporting(E_ALL); ini_set('display_errors', 0); ini_set('log_errors', 1); ob_start(); // Clear any stale connections on new request if (!isset($_SESSION['sftp_last_activity'])) { unset($_SESSION['sftp_connected']); unset($_SESSION['sftp_config']); } elseif (time() - $_SESSION['sftp_last_activity'] > 3600) { unset($_SESSION['sftp_connected']); unset($_SESSION['sftp_config']); } class SFTPConnector { private $connection; private $sftp; public function __construct() { if (!extension_loaded('ssh2')) { throw new Exception('SSH2 extension is not installed'); } } public function connect($host, $port, $username, $password) { try { $this->connection = ssh2_connect($host, $port); if (!$this->connection) { throw new Exception("Could not connect to $host on port $port"); } if (!ssh2_auth_password($this->connection, $username, $password)) { throw new Exception("Authentication failed for user $username"); } $this->sftp = ssh2_sftp($this->connection); if (!$this->sftp) { throw new Exception("Could not initialize SFTP subsystem"); } return true; } catch (Exception $e) { return $e->getMessage(); } } public function listDirectory($path = '.') { try { $handle = opendir("ssh2.sftp://{$this->sftp}$path"); if (!$handle) { throw new Exception("Could not open directory: $path"); } $files = []; while (($file = readdir($handle)) !== false) { if ($file !== '.' && $file !== '..') { $fullPath = rtrim($path, '/') . '/' . $file; $stat = ssh2_sftp_stat($this->sftp, $fullPath); if ($stat !== false) { $files[] = [ 'name' => $file, 'path' => $fullPath, 'size' => $stat['size'], 'modified' => date('Y-m-d H:i:s', $stat['mtime']), 'type' => is_dir("ssh2.sftp://{$this->sftp}$fullPath") ? 'directory' : 'file' ]; } } } closedir($handle); usort($files, function($a, $b) { if ($a['type'] === 'directory' && $b['type'] === 'file') return -1; if ($a['type'] === 'file' && $b['type'] === 'directory') return 1; return strcasecmp($a['name'], $b['name']); }); return $files; } catch (Exception $e) { return false; } } public function deleteFile($remotePath) { try { if (ssh2_sftp_unlink($this->sftp, $remotePath)) { return true; } return "Failed to delete file"; } catch (Exception $e) { return $e->getMessage(); } } public function createDirectory($path) { try { if (ssh2_sftp_mkdir($this->sftp, $path, 0755, true)) { return true; } return "Failed to create directory"; } catch (Exception $e) { return $e->getMessage(); } } public function createFile($path, $content) { try { $stream = fopen("ssh2.sftp://{$this->sftp}$path", 'w'); if (!$stream) { throw new Exception("Could not create file"); } if (fwrite($stream, $content) === false) { throw new Exception("Could not write to file"); } fclose($stream); return true; } catch (Exception $e) { return $e->getMessage(); } } public function disconnect() { $this->connection = null; $this->sftp = null; } } class SystemSFTPConnector { private $host; private $port; private $username; private $password; private $connected = false; public function connect($host, $port, $username, $password) { $this->host = $host; $this->port = $port; $this->username = $username; $this->password = $password; if (!function_exists('exec')) { return 'System commands are disabled on this server'; } $testCommand = "echo 'quit' | sftp -o ConnectTimeout=10 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -P {$port} {$username}@{$host} 2>&1"; $output = []; $returnCode = null; exec($testCommand, $output, $returnCode); if ($returnCode === 0 || strpos(implode(' ', $output), 'Connected to') !== false) { $this->connected = true; return true; } return 'Cannot connect to SFTP server: ' . implode(' ', $output); } public function listDirectory($path = '/') { if (!$this->connected) { return false; } $scriptFile = tempnam(sys_get_temp_dir(), 'sftp_script'); $script = "cd {$path}\nls -la\nquit\n"; file_put_contents($scriptFile, $script); $command = "sftp -o ConnectTimeout=10 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -P {$this->port} -b {$scriptFile} {$this->username}@{$this->host} 2>&1"; $output = []; exec($command, $output); unlink($scriptFile); $files = []; $inListing = false; foreach ($output as $line) { if (strpos($line, 'sftp>') !== false && strpos($line, 'ls') !== false) { $inListing = true; continue; } if ($inListing && strpos($line, 'sftp>') !== false) { break; } if ($inListing && preg_match('/^([drwx-]+)\s+\d+\s+\w+\s+\w+\s+(\d+)\s+(\w+\s+\d+\s+[\d:]+)\s+(.+)$/', $line, $matches)) { $name = $matches[4]; if ($name !== '.' && $name !== '..') { $files[] = [ 'name' => $name, 'path' => rtrim($path, '/') . '/' . $name, 'size' => (int)$matches[2], 'modified' => $matches[3], 'type' => substr($matches[1], 0, 1) === 'd' ? 'directory' : 'file' ]; } } } return $files; } public function deleteFile($remotePath) { if (!$this->connected) { return 'Not connected'; } $scriptFile = tempnam(sys_get_temp_dir(), 'sftp_script'); $script = "rm {$remotePath}\nquit\n"; file_put_contents($scriptFile, $script); $command = "sftp -o ConnectTimeout=10 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -P {$this->port} -b {$scriptFile} {$this->username}@{$this->host} 2>&1"; $output = []; exec($command, $output); unlink($scriptFile); if (strpos(implode(' ', $output), 'Removing') !== false || empty(array_filter($output))) { return true; } return 'Delete failed: ' . implode(' ', $output); } public function createDirectory($path) { if (!$this->connected) { return 'Not connected'; } $scriptFile = tempnam(sys_get_temp_dir(), 'sftp_script'); $script = "mkdir {$path}\nquit\n"; file_put_contents($scriptFile, $script); $command = "sftp -o ConnectTimeout=10 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -P {$this->port} -b {$scriptFile} {$this->username}@{$this->host} 2>&1"; $output = []; exec($command, $output); unlink($scriptFile); return true; } public function createFile($path, $content) { if (!$this->connected) { return 'Not connected'; } $tempFile = tempnam(sys_get_temp_dir(), 'sftp_upload'); file_put_contents($tempFile, $content); $scriptFile = tempnam(sys_get_temp_dir(), 'sftp_script'); $script = "put {$tempFile} {$path}\nquit\n"; file_put_contents($scriptFile, $script); $command = "sftp -o ConnectTimeout=10 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -P {$this->port} -b {$scriptFile} {$this->username}@{$this->host} 2>&1"; $output = []; exec($command, $output); unlink($scriptFile); unlink($tempFile); return true; } public function disconnect() { $this->connected = false; } } $action = $_POST['sftp_action'] ?? ''; $response = ['success' => false, 'message' => '', 'data' => null]; try { if (extension_loaded('ssh2')) { $connector = new SFTPConnector(); $connectorType = 'SSH2 Extension'; } else if (function_exists('exec')) { $connector = new SystemSFTPConnector(); $connectorType = 'System Commands'; } else { throw new Exception('Neither SSH2 extension nor system commands are available'); } switch ($action) { case 'connect': $result = $connector->connect( $_POST['host'] ?? '', $_POST['port'] ?? 22, $_POST['username'] ?? '', $_POST['password'] ?? '' ); if ($result === true) { $_SESSION['sftp_connected'] = true; $_SESSION['sftp_config'] = [ 'host' => $_POST['host'], 'port' => $_POST['port'] ?? 22, 'username' => $_POST['username'], 'password' => $_POST['password'] ]; $_SESSION['sftp_last_activity'] = time(); $response['success'] = true; $response['message'] = "Connected successfully via {$connectorType}"; } else { $response['message'] = $result; } break; case 'list': if (!isset($_SESSION['sftp_connected']) || !$_SESSION['sftp_connected']) { $response['success'] = false; $response['message'] = 'Not connected to SFTP server. Please connect first.'; break; } $config = $_SESSION['sftp_config']; if (extension_loaded('ssh2')) { $connector = new SFTPConnector(); } else { $connector = new SystemSFTPConnector(); } $connectResult = $connector->connect( $config['host'], $config['port'], $config['username'], $config['password'] ); if ($connectResult !== true) { unset($_SESSION['sftp_connected']); unset($_SESSION['sftp_config']); $response['message'] = 'Session expired. Please reconnect.'; break; } $_SESSION['sftp_last_activity'] = time(); $files = $connector->listDirectory($_POST['path'] ?? '/'); if ($files !== false) { $response['success'] = true; $response['data'] = $files; } else { $response['message'] = 'Failed to list directory'; } break; case 'delete': if (!isset($_SESSION['sftp_connected']) || !$_SESSION['sftp_connected']) { $response['message'] = 'Not connected to SFTP server'; break; } $config = $_SESSION['sftp_config']; if (extension_loaded('ssh2')) { $connector = new SFTPConnector(); } else { $connector = new SystemSFTPConnector(); } $connectResult = $connector->connect( $config['host'], $config['port'], $config['username'], $config['password'] ); if ($connectResult !== true) { unset($_SESSION['sftp_connected']); unset($_SESSION['sftp_config']); $response['message'] = 'Session expired. Please reconnect.'; break; } $_SESSION['sftp_last_activity'] = time(); $result = $connector->deleteFile($_POST['path'] ?? ''); if ($result === true) { $response['success'] = true; $response['message'] = 'File deleted successfully'; } else { $response['message'] = $result; } break; case 'create_folder': if (!isset($_SESSION['sftp_connected']) || !$_SESSION['sftp_connected']) { $response['message'] = 'Not connected to SFTP server'; break; } $config = $_SESSION['sftp_config']; if (extension_loaded('ssh2')) { $connector = new SFTPConnector(); } else { $connector = new SystemSFTPConnector(); } $connectResult = $connector->connect( $config['host'], $config['port'], $config['username'], $config['password'] ); if ($connectResult !== true) { unset($_SESSION['sftp_connected']); unset($_SESSION['sftp_config']); $response['message'] = 'Session expired. Please reconnect.'; break; } $_SESSION['sftp_last_activity'] = time(); $result = $connector->createDirectory($_POST['path'] ?? ''); if ($result === true) { $response['success'] = true; $response['message'] = 'Directory created successfully'; } else { $response['message'] = $result; } break; case 'create_file': if (!isset($_SESSION['sftp_connected']) || !$_SESSION['sftp_connected']) { $response['message'] = 'Not connected to SFTP server'; break; } $config = $_SESSION['sftp_config']; if (extension_loaded('ssh2')) { $connector = new SFTPConnector(); } else { $connector = new SystemSFTPConnector(); } $connectResult = $connector->connect( $config['host'], $config['port'], $config['username'], $config['password'] ); if ($connectResult !== true) { unset($_SESSION['sftp_connected']); unset($_SESSION['sftp_config']); $response['message'] = 'Session expired. Please reconnect.'; break; } $_SESSION['sftp_last_activity'] = time(); $result = $connector->createFile($_POST['path'] ?? '', $_POST['content'] ?? ''); if ($result === true) { $response['success'] = true; $response['message'] = 'File created successfully'; } else { $response['message'] = $result; } break; case 'disconnect': unset($_SESSION['sftp_connected']); unset($_SESSION['sftp_config']); unset($_SESSION['sftp_last_activity']); if (isset($connector)) { $connector->disconnect(); } $response['success'] = true; $response['message'] = 'Disconnected successfully'; break; case 'status': $response['success'] = true; $response['data'] = [ 'connected' => isset($_SESSION['sftp_connected']) && $_SESSION['sftp_connected'], 'config' => isset($_SESSION['sftp_config']) ? [ 'host' => $_SESSION['sftp_config']['host'], 'username' => $_SESSION['sftp_config']['username'], 'port' => $_SESSION['sftp_config']['port'] ] : null, 'last_activity' => $_SESSION['sftp_last_activity'] ?? null ]; break; default: $response['message'] = 'Invalid action: ' . $action; } } catch (Exception $e) { $response['message'] = 'Server error: ' . $e->getMessage(); } ob_clean(); echo json_encode($response); exit; } // Cache-busting helper function asset($path) { $isAbsolute = strlen($path) && $path[0] === '/'; $abs = $isAbsolute ? rtrim($_SERVER['DOCUMENT_ROOT'], '/') . $path : __DIR__ . '/' . $path; $v = is_file($abs) ? filemtime($abs) : time(); return $path . '?v=' . $v; } ?> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>SFTP Manager</title> <link rel="stylesheet" href="<?= asset('/core/css/overlay.css') ?>"> <style> /* Main Styles Start */ html, body { height: 100%; margin: 0; overscroll-behavior-y: contain; } body { background: #0b0f14; color: #e6edf3; font-family: system-ui, -apple-system, Segoe UI, Roboto, Inter, "Helvetica Neue", Arial; } .topbar { position: sticky; top: 0; z-index: 5; background: linear-gradient(180deg, rgba(11,15,20,.95), rgba(11,15,20,.85)); border-bottom: 1px solid #1e2633; backdrop-filter: blur(6px); } .topbar-inner { max-width: 1400px; margin: 0 auto; padding: .75rem; display: flex; align-items: center; gap: .75rem; } #buttonRow { flex: 1 1 auto; min-width: 0; display: flex; gap: .75rem; align-items: center; overflow-x: auto; overflow-y: hidden; scrollbar-width: thin; -webkit-overflow-scrolling: touch; } #menuContainer { flex: 0 0 auto; margin-left: .25rem; position: relative; } .chip { flex: 0 0 auto; border: 1px solid #2a3648; background: #1a2332; color: #e6edf3; padding: .55rem .9rem; border-radius: 999px; font-weight: 600; cursor: pointer; transition: background .15s ease; } .chip:hover { background: #263244; } .container { max-width: 1400px; margin: 0 auto; padding: 1.25rem .75rem; } .menu-trigger { width: 38px; text-align: center; } .menu-list { display: none; position: absolute; right: 0; top: calc(100% + 6px); background: #1a2332; border: 1px solid #2a3648; border-radius: 10px; min-width: 180px; padding: .25rem 0; z-index: 9999; box-shadow: 0 10px 30px rgba(0,0,0,.3); } .menu-list.open { display: block; } .menu-item { display: block; width: 100%; text-align: left; background: none; border: none; color: #e6edf3; padding: .6rem 1rem; cursor: pointer; font: inherit; } .menu-item:hover { background: #263244; } /* Quick Connect Card */ .quick-connect-card { position: relative; background: rgba(30, 41, 59, 0.7); border: 2px solid #2a3648; border-radius: 12px; padding: 1rem; cursor: pointer; transition: all 0.3s ease; max-width: 600px; margin: 0 auto; } .quick-connect-card.minimized { padding: 0.75rem 1rem; border-radius: 8px; max-width: 100%; } .quick-connect-card.minimized .quick-connect-info, .quick-connect-card.minimized #quickLoginForm { display: none; } .quick-connect-card.minimized .quick-connect-header { margin: 0; } .quick-connect-card.minimized .quick-connect-title { font-size: 14px; } .quick-connect-card.minimized .quick-status-badge { margin-top: 0; margin-left: 0.5rem; } .quick-connect-card:hover { border-color: #3b82f6; transform: translateY(-2px); box-shadow: 0 8px 16px rgba(59, 130, 246, 0.2); } .quick-connect-card.active { border-color: #10b981; background: linear-gradient(135deg, rgba(16, 185, 129, 0.15), rgba(16, 185, 129, 0.05)); } .quick-status-bar { position: absolute; top: 0; left: 0; right: 0; height: 4px; border-radius: 12px 12px 0 0; background: #2a3648; } .quick-connect-card.active .quick-status-bar { background: linear-gradient(90deg, #10b981, #34d399); } .quick-connect-header { display: flex; justify-content: space-between; align-items: center; margin-top: 8px; margin-bottom: 12px; } .quick-connect-title { font-size: 18px; font-weight: 700; color: #e6edf3; } .quick-connect-info { font-size: 14px; color: #9aa4b2; line-height: 1.6; margin-bottom: 12px; } .quick-info-row { display: flex; gap: 8px; margin-bottom: 4px; } .quick-info-label { font-weight: 600; min-width: 60px; } .quick-password-input { width: 100%; padding: 0.625rem 0.75rem; background: #0f1725; border: 1px solid #2a3648; border-radius: 8px; color: #e6edf3; font-size: 0.875rem; margin-bottom: 8px; transition: border-color 0.15s; } .quick-password-input:focus { outline: none; border-color: #3b82f6; } .quick-connect-btn { width: 100%; padding: 0.625rem; border: none; border-radius: 8px; font-weight: 600; font-size: 0.875rem; cursor: pointer; transition: all 0.15s; background: linear-gradient(135deg, #10b981, #059669); color: white; } .quick-connect-btn:hover { background: linear-gradient(135deg, #059669, #047857); transform: translateY(-1px); box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3); } .quick-connect-btn:active { transform: translateY(0); } .quick-status-badge { display: inline-block; padding: 4px 10px; border-radius: 12px; font-size: 12px; font-weight: 600; } .quick-status-badge.active { background: rgba(16, 185, 129, 0.2); color: #10b981; } .quick-status-badge.inactive { background: rgba(148, 163, 184, 0.2); color: #94a3b8; } /* Active Files Bar */ .active-files-bar { background: rgba(15, 23, 37, 0.8); border: 1px solid #2a3648; border-radius: 8px; padding: 0.5rem; margin-top: 1rem; display: none; overflow-x: auto; scrollbar-width: thin; } .active-files-inner { display: flex; gap: 0.25rem; min-height: 2rem; } .active-file-tab { display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem 0.75rem; background: rgba(30, 41, 59, 0.5); border: 1px solid #2a3648; border-radius: 6px; cursor: pointer; transition: all 0.15s; white-space: nowrap; font-size: 0.875rem; color: #94a3b8; } .active-file-tab:hover { background: rgba(30, 41, 59, 0.8); border-color: #3b82f6; } .active-file-tab.active { background: rgba(59, 130, 246, 0.2); border-color: #3b82f6; color: #e6edf3; } .active-file-close { background: transparent; border: none; color: #9aa4b2; cursor: pointer; font-size: 1.25rem; line-height: 1; padding: 0; width: 1.25rem; height: 1.25rem; display: flex; align-items: center; justify-content: center; border-radius: 4px; transition: all 0.15s; } .active-file-close:hover { background: rgba(239, 68, 68, 0.2); color: #ef4444; } /* Projects Section */ .projects-section { background: rgba(15, 23, 37, 0.5); border: 1px solid #2a3648; border-radius: 12px; padding: 1.5rem; margin-top: 1.5rem; display: none; } .projects-section.visible { display: block; } .projects-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem; } .projects-header h2 { margin: 0; font-size: 1.5rem; } .projects-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 1rem; } .project-card { position: relative; background: rgba(30, 41, 59, 0.7); border: 2px solid #2a3648; border-radius: 12px; padding: 1.25rem; cursor: pointer; transition: all 0.2s ease; } .project-card:hover { border-color: #3b82f6; transform: translateY(-2px); box-shadow: 0 8px 16px rgba(59, 130, 246, 0.2); } .project-card-icon { font-size: 2.5rem; margin-bottom: 0.75rem; } .project-card-name { font-size: 1.125rem; font-weight: 700; color: #e6edf3; margin-bottom: 0.5rem; } .project-card-path { font-size: 0.8125rem; color: #64748b; } .file-card { position: relative; background: rgba(30, 41, 59, 0.7); border: 2px solid #2a3648; border-radius: 12px; padding: 1rem; cursor: pointer; transition: all 0.2s ease; display: flex; align-items: center; gap: 1rem; } .file-card:hover { border-color: #3b82f6; transform: translateY(-2px); box-shadow: 0 8px 16px rgba(59, 130, 246, 0.2); } .file-card-icon { font-size: 2rem; flex-shrink: 0; } .file-card-info { flex: 1; min-width: 0; } .file-card-name { font-size: 0.9375rem; font-weight: 600; color: #e6edf3; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .file-card-meta { font-size: 0.8125rem; color: #64748b; margin-top: 0.25rem; } .file-card-actions { display: flex; gap: 0.25rem; } .file-icon-btn { background: transparent; border: none; color: #9aa4b2; cursor: pointer; padding: 0.5rem; border-radius: 6px; font-size: 1.125rem; transition: all 0.15s; } .file-icon-btn:hover { background: #263244; color: #e6edf3; } .new-project-card { background: transparent; border: 2px dashed #2a3648; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 0.5rem; min-height: 150px; color: #9aa4b2; font-weight: 600; } .new-project-card:hover { border-color: #3b82f6; color: #3b82f6; } .new-project-icon { font-size: 2rem; } .projects-loading { text-align: center; padding: 3rem; color: #64748b; } .conn-btn { padding: 10px 20px; border: none; border-radius: 8px; font-weight: 600; cursor: pointer; transition: all 0.15s; font-size: 14px; } .conn-btn-primary { background: linear-gradient(135deg, #3b82f6, #9333ea); color: white; } .conn-btn-primary:hover { opacity: 0.9; } .conn-toast-container { position: fixed; right: 16px; bottom: 16px; z-index: 999999; display: flex; flex-direction: column; gap: 8px; } .conn-toast { background: rgba(15, 23, 42, 0.98); border: 1px solid rgba(71, 85, 105, 0.5); color: #e2e8f0; padding: 10px 14px; border-radius: 10px; box-shadow: 0 8px 20px rgba(0, 0, 0, 0.35); font-size: 0.9rem; opacity: 0; transform: translateY(8px); animation: connToastIn 200ms ease forwards; } .conn-toast.success { border-color: rgba(16, 185, 129, 0.6); } .conn-toast.error { border-color: rgba(239, 68, 68, 0.6); } @keyframes connToastIn { to { opacity: 1; transform: translateY(0); } } @keyframes connToastOut { to { opacity: 0; transform: translateY(8px); } } </style> </head> <body> <header class="topbar" aria-label="Top navigation"> <div class="topbar-inner"> <div id="buttonRow" role="tablist" aria-label="App sections"></div> <div id="menuContainer" aria-label="More actions"></div> </div> </header> <main class="container"> <!-- Quick Connect Card --> <div class="quick-connect-card" id="quickConnectCard"> <div class="quick-status-bar"></div> <div class="quick-connect-header"> <div class="quick-connect-title">⚡ DevBrewing SFTP</div> <span class="quick-status-badge inactive" id="quickStatusBadge">⚫ Disconnected</span> </div> <div class="quick-connect-info"> <div class="quick-info-row"> <span class="quick-info-label">User:</span> <span><?= htmlspecialchars($_SESSION['username']) ?></span> </div> </div> <form id="quickLoginForm"> <input type="password" class="quick-password-input" id="quickPassword" placeholder="Enter SFTP password" required > <button type="submit" class="quick-connect-btn">🚀 Connect</button> </form> </div> <!-- Active Files Bar --> <div class="active-files-bar" id="activeFilesBar"> <div class="active-files-inner" id="activeFilesInner"> <!-- Active files tabs will appear here --> </div> </div> <!-- Projects Section --> <div class="projects-section" id="projectsSection"> <div class="projects-header"> <h2>📁 Browse Files</h2> <div style="display: flex; gap: 0.5rem; align-items: center;"> <select id="categorySelect" onchange="switchTab(this.value)" style="padding: 0.5rem 1rem; background: #1a2332; border: 1px solid #2a3648; border-radius: 8px; color: #e6edf3; font-weight: 600; cursor: pointer;"> <option value="projects">📁 Projects</option> <option value="core">⚙️ Core</option> <option value="libraries">📚 Libraries</option> <option value="images">🖼️ Images</option> <option value="jsons">📋 JSONs</option> <option value="audios">🎵 Audios</option> </select> <button class="conn-btn conn-btn-primary" onclick="handleCreateNewFolder()">+ New Folder</button> </div> </div> <div id="projectsGrid" class="projects-grid"> <div class="projects-loading">Loading...</div> </div> </div> <!-- Folder Contents Section --> <div class="projects-section" id="projectContentsSection" style="display: none;"> <div class="projects-header"> <button class="conn-btn conn-btn-primary" onclick="handleBackToProjects()">⬅️ Back</button> <h2 id="currentProjectName">Folder Files</h2> <div></div> </div> <div id="projectContentsGrid" class="projects-grid"> <div class="projects-loading">Loading files...</div> </div> </div> </main> <div class="conn-toast-container" id="connToastContainer"></div> <script> window.AppItems = []; window.AppMenu = []; </script> <script src="/core/js/core.js" defer></script> <script src="<?= asset('/core/js/overlay.js') ?>" defer></script> <script src="<?= asset('filemanager.js') ?>" defer></script> <script src="<?= asset('menu.js') ?>" defer></script> <script> // Global state const STORAGE_KEY = 'sftp_quick_connection'; let currentTab = 'projects'; let activeFiles = []; const TAB_FOLDERS = { projects: '/files/projects', core: '/files/core', libraries: '/files/libraries', images: '/files/images', jsons: '/files/jsons', audios: '/files/audios' }; const TAB_LABELS = { projects: 'Projects', core: 'Core', libraries: 'Libraries', images: 'Images', jsons: 'JSONs', audios: 'Audios' }; function getConnection() { const data = localStorage.getItem(STORAGE_KEY); return data ? JSON.parse(data) : null; } function saveConnection(connection) { localStorage.setItem(STORAGE_KEY, JSON.stringify(connection)); } function showToast(message, type = 'success', timeout = 2500) { const container = document.getElementById('connToastContainer'); if (!container) return; const toast = document.createElement('div'); toast.className = `conn-toast ${type}`; toast.textContent = message; container.appendChild(toast); setTimeout(() => { toast.style.animation = 'connToastOut 160ms ease forwards'; setTimeout(() => container.removeChild(toast), 170); }, timeout); } function updateQuickConnectStatus(isActive) { const card = document.getElementById('quickConnectCard'); const badge = document.getElementById('quickStatusBadge'); if (isActive) { card.classList.add('active'); badge.className = 'quick-status-badge active'; badge.textContent = '🟢 Connected'; } else { card.classList.remove('active'); badge.className = 'quick-status-badge inactive'; badge.textContent = '⚫ Disconnected'; } } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // ========== ACTIVE FILES SYSTEM ========== function renderActiveFiles() { const bar = document.getElementById('activeFilesBar'); const inner = document.getElementById('activeFilesInner'); inner.innerHTML = ''; if (activeFiles.length === 0) { bar.style.display = 'none'; return; } bar.style.display = 'block'; activeFiles.forEach((file, index) => { const tab = document.createElement('div'); tab.className = 'active-file-tab' + (file.active ? ' active' : ''); tab.innerHTML = ` <span>${file.icon} ${escapeHtml(file.name)}</span> <button class="active-file-close" title="Close">&times;</button> `; tab.querySelector('.active-file-close').addEventListener('click', (e) => { e.stopPropagation(); closeActiveFile(index); }); tab.addEventListener('click', () => { setActiveFile(index); }); inner.appendChild(tab); }); } function addActiveFile(path, name, icon = '📄') { // Skip if already open const existing = activeFiles.find(f => f.path === path); if (existing) { setActiveFile(activeFiles.indexOf(existing)); return; } activeFiles.forEach(f => f.active = false); activeFiles.push({ path, name, icon, active: true }); renderActiveFiles(); } function setActiveFile(index) { activeFiles.forEach(f => f.active = false); activeFiles[index].active = true; renderActiveFiles(); showToast(`Opened ${activeFiles[index].name}`, 'success'); } function closeActiveFile(index) { activeFiles.splice(index, 1); if (activeFiles.length > 0) { activeFiles[activeFiles.length - 1].active = true; } renderActiveFiles(); } // Load folders from current tab async function handleLoadProjects() { const projectsSection = document.getElementById('projectsSection'); const projectsGrid = document.getElementById('projectsGrid'); if (!projectsSection || !projectsGrid) { console.error('Projects section not found in DOM'); return; } projectsSection.classList.add('visible'); projectsGrid.innerHTML = '<div class="projects-loading">Loading...</div>'; try { const folderPath = TAB_FOLDERS[currentTab]; console.log('Loading folders from:', folderPath); // First, ensure the folder exists await ensureFolderExists(folderPath); const formData = new FormData(); formData.append('sftp_action', 'list'); formData.append('path', folderPath); const res = await fetch(window.location.href, { method: 'POST', body: formData }); const data = await res.json(); console.log('Load response:', data); if (data.success && data.data) { const folders = data.data.filter(f => f.type === 'directory'); console.log('Found folders:', folders); renderProjects(folders); } else { projectsGrid.innerHTML = '<div class="projects-loading">No folders found</div>'; } } catch (err) { console.error('Load error:', err); projectsGrid.innerHTML = `<div class="projects-loading" style="color: #ef4444;">Error: ${err.message}</div>`; } } async function ensureFolderExists(path) { try { const formData = new FormData(); formData.append('sftp_action', 'create_folder'); formData.append('path', path); await fetch(window.location.href, { method: 'POST', body: formData }); } catch (err) { // Folder might already exist, that's fine } } function switchTab(tab) { currentTab = tab; document.getElementById('categorySelect').value = tab; handleLoadProjects(); } function renderProjects(folders) { const projectsGrid = document.getElementById('projectsGrid'); projectsGrid.innerHTML = ''; // Existing folders folders.forEach(folder => { const card = document.createElement('div'); card.className = 'project-card'; card.innerHTML = ` <div class="project-card-icon">📁</div> <div class="project-card-name">${escapeHtml(folder.name)}</div> <div class="project-card-path">${TAB_FOLDERS[currentTab]}/${folder.name}</div> `; card.onclick = () => handleOpenProject(folder.name); projectsGrid.appendChild(card); }); if (folders.length === 0) { projectsGrid.innerHTML = `<div class="projects-loading">No folders yet. Click "+ New Folder" to create one.</div>`; } } async function handleCreateNewFolder() { const folderName = prompt(`Enter folder name for ${TAB_LABELS[currentTab]}:`); if (!folderName) return; if (!/^[a-zA-Z0-9_-]+$/.test(folderName)) { showToast('❌ Invalid name. Use letters, numbers, hyphens, underscores only', 'error'); return; } try { const folderPath = `${TAB_FOLDERS[currentTab]}/${folderName}`; // Create folder let formData = new FormData(); formData.append('sftp_action', 'create_folder'); formData.append('path', folderPath); let res = await fetch(window.location.href, { method: 'POST', body: formData }); let data = await res.json(); if (!data.success) throw new Error(data.message); // Only create starter files for projects if (currentTab === 'projects') { // Create index.html formData = new FormData(); formData.append('sftp_action', 'create_file'); formData.append('path', `${folderPath}/index.html`); formData.append('content', `<!DOCTYPE html>\n<html>\n<head>\n <title>${folderName}</title>\n</head>\n<body>\n <h1>Welcome to ${folderName}</h1>\n</body>\n</html>`); res = await fetch(window.location.href, { method: 'POST', body: formData }); data = await res.json(); if (!data.success) throw new Error(data.message); // Create manifest.json formData = new FormData(); formData.append('sftp_action', 'create_file'); formData.append('path', `${folderPath}/manifest.json`); formData.append('content', JSON.stringify({ name: folderName, version: '1.0.0', created: new Date().toISOString() }, null, 2)); res = await fetch(window.location.href, { method: 'POST', body: formData }); data = await res.json(); if (!data.success) throw new Error(data.message); // Create manifest.db formData = new FormData(); formData.append('sftp_action', 'create_file'); formData.append('path', `${folderPath}/manifest.db`); formData.append('content', ''); res = await fetch(window.location.href, { method: 'POST', body: formData }); data = await res.json(); if (!data.success) throw new Error(data.message); } showToast(`✅ Folder "${folderName}" created`, 'success'); handleLoadProjects(); } catch (err) { showToast(`❌ Failed: ${err.message}`, 'error'); } } function handleOpenProject(folderName) { document.getElementById('projectsSection').style.display = 'none'; document.getElementById('projectContentsSection').style.display = 'block'; document.getElementById('currentProjectName').textContent = `📁 ${folderName}`; loadProjectContents(folderName); } function handleBackToProjects() { document.getElementById('projectContentsSection').style.display = 'none'; document.getElementById('projectsSection').style.display = 'block'; } async function loadProjectContents(projectName) { const grid = document.getElementById('projectContentsGrid'); grid.innerHTML = '<div class="projects-loading">Loading files...</div>'; try { const formData = new FormData(); formData.append('sftp_action', 'list'); formData.append('path', `/files/projects/${projectName}`); const res = await fetch(window.location.href, { method: 'POST', body: formData }); const data = await res.json(); if (data.success && data.data) { renderProjectContents(data.data, projectName); } else { grid.innerHTML = '<div class="projects-loading">No files found</div>'; } } catch (err) { grid.innerHTML = `<div class="projects-loading" style="color: #ef4444;">Error: ${err.message}</div>`; } } function renderProjectContents(files, projectName) { const grid = document.getElementById('projectContentsGrid'); grid.innerHTML = ''; files.forEach(file => { const card = document.createElement('div'); card.className = 'file-card'; const isDir = file.type === 'directory'; const icon = isDir ? '📁' : getFileIcon(file.name); const size = isDir ? 'Folder' : formatBytes(file.size); const fullPath = `/files/projects/${projectName}/${file.name}`; card.innerHTML = ` <div class="file-card-icon">${icon}</div> <div class="file-card-info"> <div class="file-card-name">${escapeHtml(file.name)}</div> <div class="file-card-meta">${size} • ${file.modified || ''}</div> </div> ${!isDir ? ` <div class="file-card-actions"> <button class="file-icon-btn" onclick="event.stopPropagation(); handleDownloadFile('${fullPath}', '${escapeHtml(file.name)}')" title="Download">⬇️</button> <button class="file-icon-btn" onclick="event.stopPropagation(); handleDeleteFile('${fullPath}', '${projectName}')" title="Delete">🗑️</button> </div> ` : ''} `; if (!isDir) { card.onclick = () => handleEditFile(fullPath, file.name); } grid.appendChild(card); }); if (files.length === 0) { grid.innerHTML = '<div class="projects-loading">This folder is empty</div>'; } } function getFileIcon(filename) { const ext = filename.split('.').pop().toLowerCase(); const icons = { 'html': '🌐', 'htm': '🌐', 'css': '🎨', 'js': '⚙️', 'json': '📋', 'md': '📝', 'txt': '📝', 'pdf': '📄', 'jpg': '🖼️', 'jpeg': '🖼️', 'png': '🖼️', 'gif': '🖼️', 'svg': '🖼️', 'zip': '📦', 'rar': '📦', 'db': '🗄️' }; return icons[ext] || '📄'; } function formatBytes(bytes) { if (!bytes) return '0 B'; if (bytes < 1024) return bytes + ' B'; const units = ['KB', 'MB', 'GB']; let u = -1; do { bytes /= 1024; ++u; } while (bytes >= 1024 && u < units.length - 1); return bytes.toFixed(1) + ' ' + units[u]; } async function handleDownloadFile(path, filename) { showToast('⬇️ Download feature coming soon', 'error'); } async function handleDeleteFile(path, folderName) { if (!confirm(`Delete ${path.split('/').pop()}?`)) return; try { const formData = new FormData(); formData.append('sftp_action', 'delete'); formData.append('path', path); const res = await fetch(window.location.href, { method: 'POST', body: formData }); const data = await res.json(); if (data.success) { showToast('✅ File deleted', 'success'); loadProjectContents(folderName); } else { throw new Error(data.message); } } catch (err) { showToast(`❌ Delete failed: ${err.message}`, 'error'); } } function handleEditFile(path, filename) { const icon = getFileIcon(filename); addActiveFile(path, filename, icon); showToast('✏️ Edit feature coming soon', 'error'); } // Handle card click for disconnect document.getElementById('quickConnectCard').addEventListener('click', async function(e) { const card = document.getElementById('quickConnectCard'); // Only handle disconnect when minimized if (card.classList.contains('minimized')) { try { const formData = new FormData(); formData.append('sftp_action', 'disconnect'); await fetch(window.location.href, { method: 'POST', body: formData }); const conn = getConnection(); if (conn) { conn.active = false; saveConnection(conn); } updateQuickConnectStatus(false); card.classList.remove('minimized'); document.getElementById('projectsSection').classList.remove('visible'); showToast('Disconnected', 'success'); } catch (err) { showToast('Disconnect failed: ' + err.message, 'error'); } } }); // Handle form submission document.getElementById('quickLoginForm').addEventListener('submit', async function(e) { e.preventDefault(); e.stopPropagation(); const password = document.getElementById('quickPassword').value; const btn = this.querySelector('button[type="submit"]'); const originalText = btn.textContent; if (!password) { showToast('❌ Enter password', 'error'); return; } btn.disabled = true; btn.textContent = '🔄 Connecting...'; try { const formData = new FormData(); formData.append('sftp_action', 'connect'); formData.append('host', 'files.devbrewing.com'); formData.append('port', 22); formData.append('username', '<?= htmlspecialchars($_SESSION['username']) ?>'); formData.append('password', password); const res = await fetch(window.location.href, { method: 'POST', body: formData }); const data = await res.json(); if (data.success) { const conn = { host: 'files.devbrewing.com', port: 22, username: '<?= htmlspecialchars($_SESSION['username']) ?>', password: password, active: true }; saveConnection(conn); updateQuickConnectStatus(true); // Minimize and load projects document.getElementById('quickConnectCard').classList.add('minimized'); handleLoadProjects(); showToast('✅ Connected to DevBrewing', 'success'); } else { throw new Error(data.message || 'Connection failed'); } } catch (err) { showToast(`❌ ${err.message}`, 'error'); } finally { btn.disabled = false; btn.textContent = originalText; } }); // On page load document.addEventListener('DOMContentLoaded', function() { const conn = getConnection(); if (conn) { updateQuickConnectStatus(conn.active || false); if (conn.password) { document.getElementById('quickPassword').value = conn.password; } if (conn.active) { document.getElementById('quickConnectCard').classList.add('minimized'); // Wait a bit for DOM to be ready, then load projects setTimeout(() => { handleLoadProjects(); }, 300); } } }); // Render menu document.addEventListener('DOMContentLoaded', () => { const row = document.getElementById('buttonRow'); row.innerHTML = ''; (window.AppItems || []).forEach((item, i) => { const btn = document.createElement('button'); btn.className = 'chip'; btn.textContent = item.title || `Item ${i+1}`; btn.onclick = () => window.AppOverlay && AppOverlay.open(window.AppItems, i, btn); row.appendChild(btn); }); const menuContainer = document.getElementById('menuContainer'); menuContainer.innerHTML = ''; const menuItems = window.AppMenu || []; if (menuItems.length > 0) { const trigger = document.createElement('button'); trigger.className = 'chip menu-trigger'; trigger.textContent = '⋮'; menuContainer.appendChild(trigger); const dropdown = document.createElement('div'); dropdown.className = 'menu-list'; menuContainer.appendChild(dropdown); menuItems.forEach((m, idx) => { const item = document.createElement('button'); item.className = 'menu-item'; item.textContent = m.label || `Action ${idx+1}`; item.onclick = () => { dropdown.classList.remove('open'); m.action && m.action(); }; dropdown.appendChild(item); }); trigger.addEventListener('click', (e)=>{ e.stopPropagation(); dropdown.classList.toggle('open'); }); document.addEventListener('click', (e)=>{ if (!menuContainer.contains(e.target)) dropdown.classList.remove('open'); }); } }); </script> </body> </html>