From feebfea70228ebcf263f6987f252061a6af2986f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 1 May 2026 14:19:40 +0000 Subject: [PATCH 1/3] feat: add safe panel update system to administration page Agent-Logs-Url: https://github.com/GameServerPanel/GSP/sessions/63f4e381-38d6-4fcf-b084-409cb4d2138c Co-authored-by: iaretechnician <2749183+iaretechnician@users.noreply.github.com> --- CHANGELOG.md | 13 + lang/English/modules/administration.php | 8 + modules/administration/administration.php | 5 + modules/administration/panel_update.php | 844 ++++++++++++++++++++++ 4 files changed, 870 insertions(+) create mode 100644 modules/administration/panel_update.php diff --git a/CHANGELOG.md b/CHANGELOG.md index e4013049..cab75a81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## 2026-05-01 +- Added safe panel update system to `home.php?m=administration&p=main`: + - Numbered Releases: fetches GitHub releases via API, shows newest-first dropdown, updates to selected tag. + - Development Version: pulls from the stable branch (default `Panel-stable`). + - Cutting Edge Version: pulls from the unstable branch (default `Panel-unstable`) with an instability warning. + - Full pre-update backup (DB via mysqldump + panel files via PHP copy, excluding `.git/`, `logs/`, `cache/`, `tmp/`) saved to `/var/backups/gsp-panel/YYYY-mm-dd_HH-MM-SS/`. + - `backup.json` metadata (timestamp, git commit, current version, update target) written with each backup. + - Revert section lists available backups; restores files and database from the selected snapshot. + - Config files (`includes/config.inc.php`, DB update-blacklist) preserved across all updates. + - CSRF protection on every update/revert form; admin-only access enforced. + - All update actions logged to `logs/panel_updates.log`. + - Installed version/branch written to `includes/panel_version.php` after each update. + ## 2026-04-23 - Applied a repository-wide PHP 8 compatibility sweep across PHP sources to harden array iteration/count/key checks, normalize `date()` timestamp casting, and quote bare `delete/edit/remove` string concatenations. diff --git a/lang/English/modules/administration.php b/lang/English/modules/administration.php index f1d62755..5dd3d5d1 100644 --- a/lang/English/modules/administration.php +++ b/lang/English/modules/administration.php @@ -46,4 +46,12 @@ define('OGP_LANG_banned_until', "Banned until"); define('OGP_LANG_unban_selected_ips', "Unban selected IPs"); define('OGP_LANG_view', "View"); define('OGP_LANG_per_page', "log entries per page"); +define('OGP_LANG_panel_updates', "Panel Updates"); +define('OGP_LANG_update_to_selected_release', "Update to Selected Release"); +define('OGP_LANG_update_to_dev_version', "Update to Development Version"); +define('OGP_LANG_update_to_cutting_edge', "Update to Cutting Edge Version"); +define('OGP_LANG_cutting_edge_warning', "Warning: The cutting edge version may be unstable or contain bugs. Use with caution in production."); +define('OGP_LANG_revert_panel_update', "Revert Panel Update"); +define('OGP_LANG_installed_version', "Installed Version"); +define('OGP_LANG_latest_release', "Latest Release on GitHub"); ?> \ No newline at end of file diff --git a/modules/administration/administration.php b/modules/administration/administration.php index 01fcb711..84a33480 100644 --- a/modules/administration/administration.php +++ b/modules/administration/administration.php @@ -181,6 +181,11 @@ function exec_ogp_module() "\n"; ### END ICONS TO FRAMES + ### PANEL UPDATES + require_once(dirname(__FILE__) . '/panel_update.php'); + gsp_panel_update_section(); + ### END PANEL UPDATES + ### CHANGE MENU ORDER if ( isset( $_POST['changeOrder'] ) ) diff --git a/modules/administration/panel_update.php b/modules/administration/panel_update.php new file mode 100644 index 00000000..efa632dd --- /dev/null +++ b/modules/administration/panel_update.php @@ -0,0 +1,844 @@ + [ + 'method' => 'GET', + 'header' => "User-Agent: GSP-Panel-Updater\r\n", + 'timeout' => 10, + ], + 'ssl' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, + ], + ]); + $data = @file_get_contents($url, false, $ctx); + if ($data) { + return json_decode($data, true); + } + } + return false; +} + +// --------------------------------------------------------------------------- +// Backup: dump the MySQL database into $backup_dir +// --------------------------------------------------------------------------- +function gsp_backup_database($backup_dir) +{ + // Load DB credentials from config + @include(GSP_PANEL_DIR . '/includes/config.inc.php'); + if (empty($db_user) || empty($db_name)) { + return false; + } + + $sql_file = $backup_dir . '/' . $db_name . '_backup.sql'; + $command = 'mysqldump --skip-opt --single-transaction --add-drop-table' + . ' --create-options --extended-insert --quick --set-charset' + . ' -u ' . escapeshellarg($db_user) + . ' -p' . escapeshellarg($db_pass) + . ' ' . escapeshellarg($db_name) + . ' > ' . escapeshellarg($sql_file) + . ' 2>&1'; + @system($command); + + if (!file_exists($sql_file) || filesize($sql_file) < 100) { + return false; + } + return $sql_file; +} + +// --------------------------------------------------------------------------- +// Backup: recursively copy panel files (excluding noise dirs) into $dst_dir +// --------------------------------------------------------------------------- +function gsp_backup_files($src_dir, $dst_dir) +{ + $exclude_top = ['.git', 'logs', 'backups', 'cache', 'tmp']; + + if (!is_dir($dst_dir) && !@mkdir($dst_dir, 0750, true)) { + return false; + } + + $iter = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($src_dir, RecursiveDirectoryIterator::SKIP_DOTS), + RecursiveIteratorIterator::SELF_FIRST + ); + + foreach ($iter as $item) { + $rel = substr($item->getPathname(), strlen($src_dir) + 1); + $parts = preg_split('#[/\\\\]#', $rel); + + // Skip excluded top-level directories + if (in_array($parts[0], $exclude_top)) { + continue; + } + + // Skip *.log files + if (!$item->isDir() && substr($item->getFilename(), -4) === '.log') { + continue; + } + + $dst_path = $dst_dir . DIRECTORY_SEPARATOR . $rel; + + if ($item->isDir()) { + if (!is_dir($dst_path)) { + @mkdir($dst_path, 0755, true); + } + } else { + $dst_parent = dirname($dst_path); + if (!is_dir($dst_parent)) { + @mkdir($dst_parent, 0755, true); + } + if (!@copy($item->getPathname(), $dst_path)) { + return false; + } + } + } + return true; +} + +// --------------------------------------------------------------------------- +// Backup: create a full timestamped backup (DB + files + metadata) +// --------------------------------------------------------------------------- +function gsp_create_full_backup($update_type, $update_target) +{ + $ts = date('Y-m-d_H-i-s'); + $backup_dir = GSP_BACKUP_BASE . '/' . $ts; + + // Ensure backup base exists + if (!is_dir(GSP_BACKUP_BASE) && !@mkdir(GSP_BACKUP_BASE, 0750, true)) { + return [ + 'success' => false, + 'error' => 'Cannot create backup directory ' . GSP_BACKUP_BASE + . '. Run: sudo mkdir -p ' . GSP_BACKUP_BASE + . ' && sudo chown www-data:www-data ' . GSP_BACKUP_BASE, + ]; + } + + if (!@mkdir($backup_dir, 0750, true)) { + return ['success' => false, 'error' => 'Cannot create backup directory: ' . $backup_dir]; + } + + // 1. Database backup + $sql_file = gsp_backup_database($backup_dir); + if ($sql_file === false) { + return [ + 'success' => false, + 'error' => 'Database backup failed. Check that mysqldump is installed and credentials are correct.', + ]; + } + + // 2. File backup + $backup_files_dir = $backup_dir . '/files'; + if (!gsp_backup_files(GSP_PANEL_DIR, $backup_files_dir)) { + return ['success' => false, 'error' => 'Panel file backup failed.']; + } + + // 3. Metadata + $meta = [ + 'backup_timestamp' => $ts, + 'git_commit' => gsp_get_git_commit(), + 'installed_version' => gsp_get_current_version(), + 'update_type' => $update_type, + 'update_target' => $update_target, + ]; + file_put_contents($backup_dir . '/backup.json', json_encode($meta, JSON_PRETTY_PRINT)); + + return [ + 'success' => true, + 'backup_dir' => $backup_dir, + 'sql_file' => $sql_file, + 'backup_ts' => $ts, + ]; +} + +// --------------------------------------------------------------------------- +// Update: download the GitHub archive ZIP for a given ref (tag or branch) +// --------------------------------------------------------------------------- +function gsp_download_zip($repo_owner, $repo_name, $ref, $temp_dir) +{ + // GitHub returns a redirect from /zipball/ to the actual download URL + $url = "https://api.github.com/repos/{$repo_owner}/{$repo_name}/zipball/{$ref}"; + $zip_file = $temp_dir . '/gsp_update.zip'; + + if (function_exists('curl_init')) { + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_USERAGENT, 'GSP-Panel-Updater'); + curl_setopt($ch, CURLOPT_TIMEOUT, 180); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + $data = curl_exec($ch); + $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + if ($code !== 200 || !$data) { + return false; + } + file_put_contents($zip_file, $data); + } else { + $ctx = stream_context_create([ + 'http' => [ + 'method' => 'GET', + 'header' => "User-Agent: GSP-Panel-Updater\r\n", + 'timeout' => 180, + 'follow_location' => 1, + ], + 'ssl' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, + ], + ]); + $data = @file_get_contents($url, false, $ctx); + if (!$data) { + return false; + } + file_put_contents($zip_file, $data); + } + + if (!file_exists($zip_file) || filesize($zip_file) < 1000) { + return false; + } + return $zip_file; +} + +// --------------------------------------------------------------------------- +// Update: apply the downloaded zip to the panel directory +// --------------------------------------------------------------------------- +function gsp_apply_update($zip_file) +{ + $panel_dir = GSP_PANEL_DIR; + + // Files to never overwrite when applying an update + $preserve = [ + 'includes/config.inc.php', + 'modules/gamemanager/rsync_sites_local.list', + 'install.php', + ]; + + // Merge with the DB update-blacklist (strip leading slash from stored paths) + global $db; + $blacklisted = $db->resultQuery('SELECT file_path FROM `OGP_DB_PREFIXupdate_blacklist`;'); + if ($blacklisted !== false) { + foreach ((array)$blacklisted as $bf) { + $preserve[] = ltrim($bf['file_path'], '/'); + } + } + + // Extract ZIP to a temporary directory + $temp_dir = sys_get_temp_dir() . '/gsp_upd_' . time(); + if (!@mkdir($temp_dir, 0750)) { + return ['success' => false, 'error' => 'Cannot create temporary extraction directory.']; + } + + require_once($panel_dir . '/modules/update/unzip.php'); + $result = extractZip($zip_file, $temp_dir); + if (!is_array($result)) { + gsp_rmdir_recursive($temp_dir); + return ['success' => false, 'error' => 'ZIP extraction failed: ' . $result]; + } + + // GitHub archives place all files under a single subdirectory (e.g. "Owner-Repo-sha/") + // Detect that prefix directory + $src_dir = $temp_dir; + $subdirs = glob($temp_dir . '/*', GLOB_ONLYDIR); + if ($subdirs && count($subdirs) === 1) { + $src_dir = $subdirs[0]; + } + + // Copy files from the extracted source into the panel directory + $copied = 0; + $iter = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($src_dir, RecursiveDirectoryIterator::SKIP_DOTS), + RecursiveIteratorIterator::SELF_FIRST + ); + + foreach ($iter as $item) { + $rel = str_replace('\\', '/', substr($item->getPathname(), strlen($src_dir) + 1)); + + // Skip preserved/blacklisted files + if (in_array($rel, $preserve)) { + continue; + } + + $dst = $panel_dir . '/' . $rel; + + if ($item->isDir()) { + if (!is_dir($dst)) { + @mkdir($dst, 0755, true); + } + } else { + $dst_parent = dirname($dst); + if (!is_dir($dst_parent)) { + @mkdir($dst_parent, 0755, true); + } + if (@copy($item->getPathname(), $dst)) { + $copied++; + } + } + } + + gsp_rmdir_recursive($temp_dir); + return ['success' => true, 'files_copied' => $copied]; +} + +// --------------------------------------------------------------------------- +// Helper: recursively remove a directory +// --------------------------------------------------------------------------- +function gsp_rmdir_recursive($dir) +{ + if (!is_dir($dir)) { + return; + } + $iter = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS), + RecursiveIteratorIterator::CHILD_FIRST + ); + foreach ($iter as $item) { + if ($item->isDir()) { + @rmdir($item->getPathname()); + } else { + @unlink($item->getPathname()); + } + } + @rmdir($dir); +} + +// --------------------------------------------------------------------------- +// Post-update helpers +// --------------------------------------------------------------------------- +function gsp_fix_permissions($panel_dir) +{ + // Restore sane defaults: files 644, directories 755 + @system('find ' . escapeshellarg($panel_dir) + . ' -maxdepth 10 -name "*.php" -exec chmod 644 {} \; 2>/dev/null'); + @system('find ' . escapeshellarg($panel_dir) + . ' -maxdepth 10 -type d -exec chmod 755 {} \; 2>/dev/null'); +} + +function gsp_clear_panel_cache($panel_dir) +{ + foreach (['cache', 'temp'] as $dir) { + $cache = $panel_dir . '/' . $dir; + if (!is_dir($cache)) { + continue; + } + foreach (glob($cache . '/*.php') ?: [] as $f) { + @unlink($f); + } + foreach (glob($cache . '/*.cache') ?: [] as $f) { + @unlink($f); + } + } +} + +// --------------------------------------------------------------------------- +// List available backup timestamps under GSP_BACKUP_BASE +// --------------------------------------------------------------------------- +function gsp_get_available_backups() +{ + $backups = []; + if (!is_dir(GSP_BACKUP_BASE)) { + return $backups; + } + foreach ((array)scandir(GSP_BACKUP_BASE) as $entry) { + if ($entry === '.' || $entry === '..') { + continue; + } + if (!is_dir(GSP_BACKUP_BASE . '/' . $entry)) { + continue; + } + if (!preg_match('/^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}$/', $entry)) { + continue; + } + $meta_file = GSP_BACKUP_BASE . '/' . $entry . '/backup.json'; + $meta = []; + if (file_exists($meta_file)) { + $meta = json_decode(file_get_contents($meta_file), true) ?: []; + } + $backups[] = ['ts' => $entry, 'meta' => $meta]; + } + // Newest first + usort($backups, function ($a, $b) { + return strcmp($b['ts'], $a['ts']); + }); + return $backups; +} + +// --------------------------------------------------------------------------- +// Orchestrate a full update +// --------------------------------------------------------------------------- +function gsp_do_update($repo_owner, $repo_name, $ref, $update_type) +{ + global $db; + $panel_dir = GSP_PANEL_DIR; + + // Step 1 — backup + $backup = gsp_create_full_backup($update_type, $ref); + if (!$backup['success']) { + return $backup; // contains 'error' + } + gsp_update_log("Backup created at {$backup['backup_ts']} before {$update_type} update to {$ref}"); + + // Step 2 — download + $temp_dir = sys_get_temp_dir() . '/gsp_dl_' . time(); + @mkdir($temp_dir, 0750); + $zip_file = gsp_download_zip($repo_owner, $repo_name, $ref, $temp_dir); + if (!$zip_file) { + @rmdir($temp_dir); + return [ + 'success' => false, + 'error' => 'Failed to download update ZIP from GitHub. Check network connectivity.', + ]; + } + gsp_update_log("Downloaded update ZIP for ref={$ref}"); + + // Step 3 — apply + $apply = gsp_apply_update($zip_file); + @unlink($zip_file); + @rmdir($temp_dir); + if (!$apply['success']) { + return $apply; + } + gsp_update_log("Applied update: {$apply['files_copied']} files written"); + + // Step 4 — housekeeping + gsp_fix_permissions($panel_dir); + gsp_clear_panel_cache($panel_dir); + gsp_write_version_file($ref, $update_type); + $db->setSettings(['ogp_version' => $ref, 'version_type' => $update_type]); + + // Step 5 — post-update module handling (mirrors updating.php behaviour) + if (file_exists($panel_dir . '/modules/modulemanager/module_handling.php')) { + require_once($panel_dir . '/modules/modulemanager/module_handling.php'); + } + if (function_exists('updateAllPanelModules')) { + updateAllPanelModules(); + } + if (function_exists('runPostUpdateOperations')) { + runPostUpdateOperations(); + } + + gsp_update_log("Update to {$ref} (type={$update_type}) complete"); + return ['success' => true, 'files_copied' => $apply['files_copied']]; +} + +// --------------------------------------------------------------------------- +// Orchestrate a revert to a previous backup +// --------------------------------------------------------------------------- +function gsp_do_revert($backup_ts) +{ + global $db; + $panel_dir = GSP_PANEL_DIR; + $backup_dir = GSP_BACKUP_BASE . '/' . $backup_ts; + + if (!is_dir($backup_dir)) { + return ['success' => false, 'error' => 'Backup directory not found: ' . htmlspecialchars($backup_ts)]; + } + + $backup_files_dir = $backup_dir . '/files'; + if (!is_dir($backup_files_dir)) { + return ['success' => false, 'error' => 'Backup files directory not found.']; + } + + $sql_files = glob($backup_dir . '/*.sql'); + if (!$sql_files) { + return ['success' => false, 'error' => 'No SQL dump found in backup.']; + } + $sql_file = $sql_files[0]; + + // Enable maintenance mode for the duration of the revert + $had_maintenance = isset($db->getSettings()['maintenance_mode']) + && $db->getSettings()['maintenance_mode'] == '1'; + if (!$had_maintenance) { + $db->setSettings([ + 'maintenance_mode' => '1', + 'maintenance_title' => 'Reverting...', + 'maintenance_message' => 'The panel is being reverted to a previous version. Please wait.', + ]); + } + + // Restore files + $copied = 0; + $iter = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($backup_files_dir, RecursiveDirectoryIterator::SKIP_DOTS), + RecursiveIteratorIterator::SELF_FIRST + ); + foreach ($iter as $item) { + $rel = substr($item->getPathname(), strlen($backup_files_dir)); + $dst = $panel_dir . $rel; + if ($item->isDir()) { + if (!is_dir($dst)) { + @mkdir($dst, 0755, true); + } + } else { + $dst_dir = dirname($dst); + if (!is_dir($dst_dir)) { + @mkdir($dst_dir, 0755, true); + } + if (@copy($item->getPathname(), $dst)) { + $copied++; + } + } + } + gsp_update_log("Revert: restored {$copied} files from backup {$backup_ts}"); + + // Restore database + @include(GSP_PANEL_DIR . '/includes/config.inc.php'); + if (!empty($db_user) && !empty($db_name)) { + $cmd = 'mysql' + . ' --user=' . escapeshellarg($db_user) + . ' --password=' . escapeshellarg($db_pass) + . ' ' . escapeshellarg($db_name) + . ' < ' . escapeshellarg($sql_file) + . ' 2>&1'; + @system($cmd, $ret); + if ($ret !== 0) { + gsp_update_log("Revert warning: database restore exited with code {$ret}"); + } + } + + // Housekeeping + gsp_fix_permissions($panel_dir); + gsp_clear_panel_cache($panel_dir); + + // Turn off maintenance mode (unless it was already on before we started) + if (!$had_maintenance) { + $db->setSettings(['maintenance_mode' => '0']); + } + + gsp_update_log("Revert to backup {$backup_ts} complete"); + return ['success' => true, 'files_restored' => $copied]; +} + +// --------------------------------------------------------------------------- +// Main entry point — render the "Panel Updates" section +// --------------------------------------------------------------------------- +function gsp_panel_update_section() +{ + global $db, $settings; + + // Guard: admins only + if ($_SESSION['users_group'] !== 'admin') { + return; + } + + // GitHub repository settings (with GSP defaults) + $repo_owner = !empty($settings['gsp_repo_owner']) ? $settings['gsp_repo_owner'] : 'GameServerPanel'; + $repo_name = !empty($settings['gsp_repo_name']) ? $settings['gsp_repo_name'] : 'GSP'; + $stable_branch = !empty($settings['gsp_stable_branch']) ? $settings['gsp_stable_branch'] : 'Panel-stable'; + $unstable_branch= !empty($settings['gsp_unstable_branch'])? $settings['gsp_unstable_branch']: 'Panel-unstable'; + + // Per-session CSRF token + if (empty($_SESSION['gsp_update_csrf'])) { + $_SESSION['gsp_update_csrf'] = bin2hex(random_bytes(16)); + } + $csrf_token = $_SESSION['gsp_update_csrf']; + + // ---- Handle POST actions ------------------------------------------------ + if (isset($_POST['gsp_update_action'])) { + $submitted_csrf = isset($_POST['gsp_update_csrf']) ? $_POST['gsp_update_csrf'] : ''; + if (!hash_equals($csrf_token, $submitted_csrf)) { + print_failure('Invalid security token. Please reload the page and try again.'); + } else { + $action = $_POST['gsp_update_action']; + set_time_limit(0); + + $user_label = htmlspecialchars($_SESSION['users_login']) + . ' (IP: ' . htmlspecialchars($_SERVER['REMOTE_ADDR']) . ')'; + + if ($action === 'update_release') { + $version = isset($_POST['gsp_release_version']) ? trim($_POST['gsp_release_version']) : ''; + if (!preg_match('/^[a-zA-Z0-9._\-]+$/', $version) || strlen($version) > 80) { + print_failure('Invalid release tag selected.'); + } else { + $result = gsp_do_update($repo_owner, $repo_name, $version, 'release'); + if ($result['success']) { + print_success( + 'Panel updated to release ' . htmlspecialchars($version) . '. ' + . intval($result['files_copied']) . ' file(s) updated.' + ); + gsp_update_log("Admin {$user_label} updated panel to release {$version}"); + } else { + print_failure('Update failed: ' . htmlspecialchars($result['error'])); + gsp_update_log("Admin {$user_label} update to release {$version} FAILED: {$result['error']}"); + } + } + + } elseif ($action === 'update_stable') { + $result = gsp_do_update($repo_owner, $repo_name, $stable_branch, 'stable'); + if ($result['success']) { + print_success( + 'Panel updated to development version (' . htmlspecialchars($stable_branch) . '). ' + . intval($result['files_copied']) . ' file(s) updated.' + ); + gsp_update_log("Admin {$user_label} updated panel to stable branch {$stable_branch}"); + } else { + print_failure('Update failed: ' . htmlspecialchars($result['error'])); + gsp_update_log("Admin {$user_label} update to stable branch {$stable_branch} FAILED: {$result['error']}"); + } + + } elseif ($action === 'update_unstable') { + $result = gsp_do_update($repo_owner, $repo_name, $unstable_branch, 'unstable'); + if ($result['success']) { + print_success( + 'Panel updated to cutting edge version (' . htmlspecialchars($unstable_branch) . '). ' + . intval($result['files_copied']) . ' file(s) updated.' + ); + gsp_update_log("Admin {$user_label} updated panel to unstable branch {$unstable_branch}"); + } else { + print_failure('Update failed: ' . htmlspecialchars($result['error'])); + gsp_update_log("Admin {$user_label} update to unstable branch {$unstable_branch} FAILED: {$result['error']}"); + } + + } elseif ($action === 'revert') { + $backup_ts = isset($_POST['gsp_revert_backup']) ? trim($_POST['gsp_revert_backup']) : ''; + if (!preg_match('/^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}$/', $backup_ts)) { + print_failure('Invalid backup timestamp selected.'); + } else { + $result = gsp_do_revert($backup_ts); + if ($result['success']) { + print_success( + 'Panel reverted to backup from ' . htmlspecialchars($backup_ts) . '. ' + . intval($result['files_restored']) . ' file(s) restored.' + ); + gsp_update_log("Admin {$user_label} reverted panel to backup {$backup_ts}"); + } else { + print_failure('Revert failed: ' . htmlspecialchars($result['error'])); + gsp_update_log("Admin {$user_label} revert to backup {$backup_ts} FAILED: {$result['error']}"); + } + } + } + } + + // Rotate CSRF token after every submission + $_SESSION['gsp_update_csrf'] = bin2hex(random_bytes(16)); + $csrf_token = $_SESSION['gsp_update_csrf']; + } + // ---- End POST handling -------------------------------------------------- + + // Gather display data + $current_version = gsp_get_current_version(); + $current_branch = gsp_get_current_branch(); + $git_commit = gsp_get_git_commit(); + $releases = gsp_fetch_github_releases($repo_owner, $repo_name); + $latest_release = (is_array($releases) && !empty($releases)) + ? htmlspecialchars($releases[0]['tag_name'] ?? 'N/A') + : 'N/A (could not reach GitHub)'; + $backups = gsp_get_available_backups(); + + // ---- Render UI ---------------------------------------------------------- + echo "
\n";
+
+ // Current status table
+ echo "
\n"; + + // ---- Numbered Releases -------------------------------------------------- + echo " Numbered Releases\n"; + if (is_array($releases) && !empty($releases)) { + echo "\n"; + } else { + echo "No releases available (GitHub API unreachable or no releases published). \n"; + } + + echo "\n"; + + // ---- Development Version ------------------------------------------------ + echo " Development Version\n"; + echo "\n"; + + echo "\n"; + + // ---- Cutting Edge Version ----------------------------------------------- + echo " Cutting Edge Version\n"; + echo "" + . "⚠ Warning: The cutting edge version may be unstable or contain bugs. Use with caution in production. \n"; + echo "\n"; + + // ---- Revert Section ----------------------------------------------------- + if (!empty($backups)) { + echo " \n Revert Panel Update\n"; + echo "Available backups in |