diff --git a/.github/module-map.md b/.github/module-map.md index 990461ce..fae19d9f 100644 --- a/.github/module-map.md +++ b/.github/module-map.md @@ -33,7 +33,7 @@ This file captures how the control panel, storefront, agents, and helper scripts | `config_games` | `modules/config_games/add_mod.php`, `server_config_parser.php`, XML files under `server_configs/` | Admin UI for XML definitions. Controls what appears in storefront/service catalog. | Feeds `gamemanager`, billing catalog, cron installers. | | `steam_workshop` | `modules/steam_workshop/admin.php`, `user.php`, `Panel/includes/functions.php`, `navigation.xml` | Admin profile defaults + per-home mod management. Profile defaults can now be refreshed from game XML and the user route is explicitly exposed via `p=user`. | Uses `config_games` XML metadata + `server_homes`/assignment tables; feeds workshop agent updater. | | `user_games` | `modules/user_games/add_home.php`, `assign_home.php`, `edit_home.php` | Admin workflow to add homes manually or edit assignments. Shares DB tables with billing provisioner. | Uses `game_homes`, `remote_servers`, `billing_orders`. | -| `administration` / `user_admin` | CRUD around users, groups, permissions, expire dates. | Sets roles consumed by storefront admin guard and provisioning ACLs. | +| `administration` / `user_admin` | CRUD around users, groups, permissions, expire dates. `modules/administration/panel_update.php` now also runs repository-layout-aware panel updates, preflight checks, updater self-refresh, backup/rollback for Panel+Website, patch execution, and Apache path scan/fix helpers. | Sets roles consumed by storefront admin guard and provisioning ACLs; writes update lifecycle traces to root `logs/update_trace.log` and patch state via `modules/update/patches` + `update_patches` tracking. | | `server` | `modules/server/*` | Remote server management (agents, IPs, ports, reinstall keys). Billing uses these tables for available nodes/locations. | | `modulemanager` | Manage module install/uninstall/menus. Billing module registers `navigation.xml` to surface `create_servers.php` & admin pages. | | `tickets`, `support` | Support ticketing/email utilities. | Pulls user info and logger records. | diff --git a/Panel/CHANGELOG.md b/Panel/CHANGELOG.md index bf5d8a71..8f573d50 100644 --- a/Panel/CHANGELOG.md +++ b/Panel/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog ## 2026-05-18 +- **Updater layout hardening + pre-update patch framework:** Reworked `modules/administration/panel_update.php` to resolve explicit GSP root/Panel/Website paths, run mandatory preflight checks, self-update updater files before main sync when drift is detected, and apply ordered required patches from `modules/update/patches/` with DB/local state tracking. Backup/rollback now includes both Panel + Website archives and root `version.json`, logs moved to root `logs/update_trace.log`, and the admin Update UI now exposes preflight, patch apply, Apache path scan/fix, backup, update, and rollback actions. - **Billing runtime relocation + portable path bootstrap:** Re-homed storefront runtime to `Panel/modules/billing`, added portable runtime helpers (`billing_bootstrap.php`, `site_config.php`, `site_config.example.php`) with env/local override support for base path and panel path, normalized critical storefront redirects/links to computed billing URLs, and added `Website` compatibility wrappers for key billing entrypoints. - **Panel updater panel-subtree safety:** Hardened updater logic to treat repository `/panel` as the update source when present (ZIP + git flows) so root-level docs/examples/scripts are no longer candidates for panel file overwrite during updates. - **Panel registration stability + captcha fallback hardening:** Fixed a fatal syntax error in `modules/register/register-exec.php`, removed hardcoded/legacy registration redirects, added structured registration logging to `modules/register/logs/register.log` (auto-creates missing log dir), added duplicate username checks, added optional `users_pass_hash` write for PHP 8.3-compatible auth upgrades, and implemented graceful reCAPTCHA fallback when keys are missing/legacy-invalid or the widget reports an error so the themed registration flow no longer crashes with raw PHP errors. diff --git a/Panel/docs/COPILOT_TODO.md b/Panel/docs/COPILOT_TODO.md index 47c0b90e..50e18511 100644 --- a/Panel/docs/COPILOT_TODO.md +++ b/Panel/docs/COPILOT_TODO.md @@ -16,3 +16,4 @@ - Add an admin/serverlist UI badge that shows detected service OS variant (Windows/Linux/Any) from XML metadata next to each purchasable service row. - Add a panel settings health check that validates reCAPTCHA site/secret keys against active panel/storefront domains and warns admins before registration users see widget errors. - Add an automated deployment check that fails when `Website/timestamp.txt` and `modules/billing/timestamp.txt` diverge after storefront/content changes. +- Add an admin preview/diff panel for Apache path repairs so staff can review exact vhost line changes before confirming `Fix Apache Paths`. diff --git a/Panel/modules/administration/panel_update.php b/Panel/modules/administration/panel_update.php index eeeeb755..808efe72 100644 --- a/Panel/modules/administration/panel_update.php +++ b/Panel/modules/administration/panel_update.php @@ -10,1398 +10,1505 @@ * of the License, or any later version. * * GSP Panel Update System - * Provides safe, admin-only panel updates from GitHub with pre-update backup - * and a revert capability. + * Repository-layout aware updater with backup, rollback, patching, and apache path helpers. * */ -// Panel root is two directories up from this file (modules/administration/panel_update.php) -defined('GSP_PANEL_DIR') || define('GSP_PANEL_DIR', realpath(dirname(__FILE__) . '/../../')); -defined('GSP_BACKUP_BASE') || define('GSP_BACKUP_BASE', GSP_PANEL_DIR . '/backups'); -defined('GSP_UPDATE_LOG') || define('GSP_UPDATE_LOG', GSP_PANEL_DIR . '/logs/panel_updates.log'); +defined('GSP_PANEL_DIR') || define('GSP_PANEL_DIR', realpath(dirname(__FILE__) . '/../../')); +defined('GSP_ROOT_DIR') || define('GSP_ROOT_DIR', realpath(dirname(GSP_PANEL_DIR)) ?: dirname(GSP_PANEL_DIR)); +defined('GSP_WEBSITE_DIR') || define('GSP_WEBSITE_DIR', GSP_ROOT_DIR . '/Website'); +defined('GSP_BACKUP_BASE') || define('GSP_BACKUP_BASE', GSP_ROOT_DIR . '/backups'); +defined('GSP_UPDATE_LOG') || define('GSP_UPDATE_LOG', GSP_ROOT_DIR . '/logs/update_trace.log'); defined('GSP_VERSION_FILE') || define('GSP_VERSION_FILE', GSP_PANEL_DIR . '/includes/panel_version.php'); -defined('GSP_VERSION_JSON') || define('GSP_VERSION_JSON', GSP_PANEL_DIR . '/version.json'); +defined('GSP_VERSION_JSON') || define('GSP_VERSION_JSON', GSP_ROOT_DIR . '/version.json'); +defined('GSP_PATCH_DIR') || define('GSP_PATCH_DIR', GSP_PANEL_DIR . '/modules/update/patches'); +defined('GSP_EXPECTED_ROOT') || define('GSP_EXPECTED_ROOT', '/var/www/html/GSP'); + +$gspPatchManager = GSP_PANEL_DIR . '/modules/update/patch_manager.php'; +if (file_exists($gspPatchManager)) { +require_once($gspPatchManager); +} + +function gsp_update_log($message) +{ +$log_dir = dirname(GSP_UPDATE_LOG); +if (!is_dir($log_dir)) { +@mkdir($log_dir, 0755, true); +} +$line = '[' . date('Y-m-d H:i:s') . '] ' . $message . PHP_EOL; +@file_put_contents(GSP_UPDATE_LOG, $line, FILE_APPEND | LOCK_EX); +} + +function gsp_log_update_to_db($channel, $branch, $status, $message, $backup_path = null, $db_backup_path = null, $file_backup_path = null, $started_at = null, $finished_at = null) +{ +global $db; +if (!isset($db) || !is_object($db)) { +return; +} +if ($started_at === null) { +$started_at = date('Y-m-d H:i:s'); +} +$channel = $db->real_escape_string((string)$channel); +$branch = $branch !== null ? "'" . $db->real_escape_string((string)$branch) . "'" : 'NULL'; +$status = $db->real_escape_string((string)$status); +$message_esc = $message !== null ? "'" . $db->real_escape_string((string)$message) . "'" : 'NULL'; +$backup_path_esc = $backup_path !== null ? "'" . $db->real_escape_string((string)$backup_path) . "'" : 'NULL'; +$db_backup_esc = $db_backup_path !== null ? "'" . $db->real_escape_string((string)$db_backup_path) . "'" : 'NULL'; +$file_backup_esc = $file_backup_path !== null ? "'" . $db->real_escape_string((string)$file_backup_path) . "'" : 'NULL'; +$started_esc = "'" . $db->real_escape_string((string)$started_at) . "'"; +$finished_esc = $finished_at !== null ? "'" . $db->real_escape_string((string)$finished_at) . "'" : 'NULL'; +$db->query( +"INSERT INTO OGP_DB_PREFIXpanel_update_log" +. " (channel, branch, status, message, backup_path, db_backup_path, file_backup_path, started_at, finished_at)" +. " VALUES ('{$channel}', {$branch}, '{$status}', {$message_esc}, {$backup_path_esc}, {$db_backup_esc}, {$file_backup_esc}, {$started_esc}, {$finished_esc})" +); +} + +function gsp_random_token($bytes = 16) +{ +if (function_exists('random_bytes')) { +try { +return bin2hex(random_bytes($bytes)); +} catch (Exception $e) { +} +} +if (function_exists('openssl_random_pseudo_bytes')) { +return bin2hex(openssl_random_pseudo_bytes($bytes)); +} +return sha1(uniqid('gsp', true) . mt_rand()); +} function gsp_detect_repo_root() { - $panelGit = GSP_PANEL_DIR . '/.git'; - if (is_dir($panelGit) || is_file($panelGit)) { - return GSP_PANEL_DIR; - } - $parent = dirname(GSP_PANEL_DIR); - $parentGit = $parent . '/.git'; - if (is_dir($parentGit) || is_file($parentGit)) { - return $parent; - } - return null; +$candidates = [ +GSP_ROOT_DIR, +GSP_PANEL_DIR, +dirname(GSP_ROOT_DIR), +]; +foreach ($candidates as $candidate) { +$gitPath = $candidate . '/.git'; +if (is_dir($gitPath) || is_file($gitPath)) { +return realpath($candidate) ?: $candidate; +} +} +return null; } -function gsp_locate_panel_source_dir($root_dir) -{ - $root = realpath($root_dir); - if ($root === false) { - return null; - } - // New layout: repo root contains /panel subtree. - if (is_dir($root . '/panel/includes') && is_dir($root . '/panel/modules')) { - return realpath($root . '/panel'); - } - // Legacy layout: panel is at repository root. - if (is_dir($root . '/includes') && is_dir($root . '/modules')) { - return $root; - } - return null; -} - -// --------------------------------------------------------------------------- -// Helper: write a line to the panel update log -// --------------------------------------------------------------------------- -function gsp_update_log($message) -{ - $log_dir = dirname(GSP_UPDATE_LOG); - if (!is_dir($log_dir)) { - @mkdir($log_dir, 0750, true); - } - $line = '[' . date('Y-m-d H:i:s') . '] ' . $message . PHP_EOL; - @file_put_contents(GSP_UPDATE_LOG, $line, FILE_APPEND | LOCK_EX); -} - -// --------------------------------------------------------------------------- -// Helper: insert a row into gsp_panel_update_log (silently skips on failure) -// --------------------------------------------------------------------------- -function gsp_log_update_to_db($channel, $branch, $status, $message, $backup_path = null, $db_backup_path = null, $file_backup_path = null, $started_at = null, $finished_at = null) -{ - global $db; - if (!isset($db) || !is_object($db)) { - return; - } - if ($started_at === null) { - $started_at = date('Y-m-d H:i:s'); - } - $channel = $db->real_escape_string((string) $channel); - $branch = $branch !== null ? "'" . $db->real_escape_string((string) $branch) . "'" : 'NULL'; - $status = $db->real_escape_string((string) $status); - $message_esc = $message !== null ? "'" . $db->real_escape_string((string) $message) . "'" : 'NULL'; - $backup_path_esc = $backup_path !== null ? "'" . $db->real_escape_string((string) $backup_path) . "'" : 'NULL'; - $db_backup_esc = $db_backup_path !== null ? "'" . $db->real_escape_string((string) $db_backup_path) . "'" : 'NULL'; - $file_backup_esc = $file_backup_path !== null ? "'" . $db->real_escape_string((string) $file_backup_path) . "'" : 'NULL'; - $started_esc = "'" . $db->real_escape_string($started_at) . "'"; - $finished_esc = $finished_at !== null ? "'" . $db->real_escape_string((string) $finished_at) . "'" : 'NULL'; - $db->query( - "INSERT INTO OGP_DB_PREFIXpanel_update_log" - . " (channel, branch, status, message, backup_path, db_backup_path, file_backup_path, started_at, finished_at)" - . " VALUES ('{$channel}', {$branch}, '{$status}', {$message_esc}," - . " {$backup_path_esc}, {$db_backup_esc}, {$file_backup_esc}, {$started_esc}, {$finished_esc})" - ); -} - -// --------------------------------------------------------------------------- -// Helper: read the installed version / branch from panel_version.php -// --------------------------------------------------------------------------- function gsp_get_current_version() { - if (file_exists(GSP_VERSION_FILE)) { - $code = file_get_contents(GSP_VERSION_FILE); - if (preg_match("/define\('GSP_VERSION',\s*'([^']+)'\)/", $code, $m)) { - return $m[1]; - } - } - return 'unknown'; +if (file_exists(GSP_VERSION_FILE)) { +$code = @file_get_contents(GSP_VERSION_FILE); +if (preg_match("/define\('GSP_VERSION',\s*'([^']+)'\)/", (string)$code, $m)) { +return $m[1]; +} +} +return 'unknown'; } function gsp_get_current_branch() { - $repo_root = gsp_detect_repo_root(); - if ($repo_root && function_exists('exec')) { - $out = []; - $ret = 0; - exec('git -C ' . escapeshellarg($repo_root) . ' rev-parse --abbrev-ref HEAD 2>/dev/null', $out, $ret); - if ($ret === 0 && !empty($out[0])) { - return trim($out[0]); - } - } - if (file_exists(GSP_VERSION_FILE)) { - $code = file_get_contents(GSP_VERSION_FILE); - if (preg_match("/define\('GSP_BRANCH',\s*'([^']+)'\)/", $code, $m)) { - return $m[1]; - } - } - // Fall back to reading .git/HEAD - $repo_root = gsp_detect_repo_root() ?: GSP_PANEL_DIR; - $git_head = $repo_root . '/.git/HEAD'; - if (file_exists($git_head)) { - $content = trim(file_get_contents($git_head)); - if (preg_match('/^ref: refs\/heads\/(.+)$/', $content, $m)) { - return $m[1]; - } - } - return 'unknown'; +$repo_root = gsp_detect_repo_root(); +if ($repo_root && function_exists('exec')) { +$out = []; +$ret = 0; +exec('git -C ' . escapeshellarg($repo_root) . ' rev-parse --abbrev-ref HEAD 2>/dev/null', $out, $ret); +if ($ret === 0 && !empty($out[0])) { +return trim($out[0]); +} +} +return 'unknown'; } function gsp_get_git_commit() { - $repo_root = gsp_detect_repo_root() ?: GSP_PANEL_DIR; - $git_head = $repo_root . '/.git/HEAD'; - if (!file_exists($git_head)) { - return null; - } - $content = trim(file_get_contents($git_head)); - if (preg_match('/^ref: refs\/heads\/(.+)$/', $content, $m)) { - $branch_file = $repo_root . '/.git/refs/heads/' . $m[1]; - if (file_exists($branch_file)) { - return trim(file_get_contents($branch_file)); - } - } elseif (preg_match('/^[0-9a-f]{40}$/i', $content)) { - return $content; - } - return null; +$repo_root = gsp_detect_repo_root(); +if (!$repo_root || !function_exists('exec')) { +return null; +} +$out = []; +$ret = 0; +exec('git -C ' . escapeshellarg($repo_root) . ' rev-parse HEAD 2>/dev/null', $out, $ret); +if ($ret === 0 && !empty($out[0])) { +$sha = trim($out[0]); +if (preg_match('/^[0-9a-f]{40,64}$/i', $sha)) { +return $sha; +} +} +return null; } -// --------------------------------------------------------------------------- -// Helper: write/update includes/panel_version.php -// --------------------------------------------------------------------------- -function gsp_write_version_file($version, $branch_or_type) -{ - $content = " $installed_type, - 'installed_source' => $installed_source, - 'installed_version' => $installed_version, - 'installed_commit' => $installed_commit, - 'installed_at' => date('Y-m-d H:i:s'), - ]; - @file_put_contents(GSP_VERSION_JSON, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); +$data = [ +'installed_type' => (string)$installed_type, +'installed_source' => (string)$installed_source, +'installed_version' => (string)$installed_version, +'installed_commit' => $installed_commit, +'installed_at' => date('Y-m-d H:i:s'), +]; +@file_put_contents(GSP_VERSION_JSON, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); } -// --------------------------------------------------------------------------- -// Helper: generate a cryptographically strong random hex token -// --------------------------------------------------------------------------- -function gsp_random_token($bytes = 16) -{ - try { - return bin2hex(random_bytes($bytes)); - } catch (\Throwable $e) { - // Fallback for environments where random_bytes() is unavailable - return bin2hex(openssl_random_pseudo_bytes($bytes)); - } -} - -// --------------------------------------------------------------------------- -// GitHub API: fetch list of releases (newest first) -// --------------------------------------------------------------------------- function gsp_fetch_github_releases($repo_owner, $repo_name) { - $url = "https://api.github.com/repos/{$repo_owner}/{$repo_name}/releases?per_page=30"; - - 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, 10); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); - curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); - $data = curl_exec($ch); - $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); - curl_close($ch); - if ($code === 200 && $data) { - return json_decode($data, true); - } - } else { - $ctx = stream_context_create([ - 'http' => [ - 'method' => 'GET', - 'header' => "User-Agent: GSP-Panel-Updater\r\n", - 'timeout' => 10, - ], - 'ssl' => [ - 'verify_peer' => true, - 'verify_peer_name' => true, - ], - ]); - $data = @file_get_contents($url, false, $ctx); - if ($data) { - return json_decode($data, true); - } - } - return false; +$url = "https://api.github.com/repos/{$repo_owner}/{$repo_name}/releases?per_page=30"; +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, 15); +curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); +curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); +$data = curl_exec($ch); +$code = curl_getinfo($ch, CURLINFO_HTTP_CODE); +curl_close($ch); +if ($code === 200 && $data) { +return json_decode($data, true); +} +} +$ctx = stream_context_create([ +'http' => [ +'method' => 'GET', +'header' => "User-Agent: GSP-Panel-Updater\r\n", +'timeout' => 15, +], +'ssl' => [ +'verify_peer' => true, +'verify_peer_name' => true, +], +]); +$data = @file_get_contents($url, false, $ctx); +if ($data) { +return json_decode($data, true); +} +return false; } -// --------------------------------------------------------------------------- -// Helper: read DB credentials from includes/config.inc.php using an -// isolated scope so the caller's variables are not polluted and side effects -// from the config file (e.g. debug.php inclusion) stay contained. -// Returns an array with keys host/port/user/pass/name, or null on failure. -// --------------------------------------------------------------------------- -function load_panel_db_config() +function gsp_preflight_check() { - $config_file = GSP_PANEL_DIR . '/includes/config.inc.php'; - if (!is_readable($config_file)) { - return null; - } +$errors = []; +$warnings = []; +$layout = [ +'cwd' => getcwd(), +'expected_root' => GSP_EXPECTED_ROOT, +'gsp_root' => GSP_ROOT_DIR, +'panel_dir' => GSP_PANEL_DIR, +'website_dir' => GSP_WEBSITE_DIR, +'backup_dir' => GSP_BACKUP_BASE, +'config_file' => GSP_PANEL_DIR . '/includes/config.inc.php', +]; - // Static closure gives an isolated variable scope; the @ suppresses any - // non-fatal errors from transitive includes (e.g. debug.php). - $capture = static function ($__file) { - $db_host = 'localhost'; - $db_port = '3306'; - $db_user = ''; - $db_pass = ''; - $db_name = ''; - @include $__file; - return [ - 'host' => (string) $db_host, - 'port' => (string) $db_port, - 'user' => (string) $db_user, - 'pass' => (string) $db_pass, - 'name' => (string) $db_name, - ]; - }; - - $cfg = $capture($config_file); - - if (empty($cfg['user']) || empty($cfg['name'])) { - return null; - } - - return $cfg; +if (!$layout['cwd']) { +$errors[] = 'Unable to read current working directory.'; +} elseif (strpos(realpath($layout['cwd']) ?: $layout['cwd'], GSP_ROOT_DIR) !== 0) { +$errors[] = 'Current working directory is outside detected GSP root.'; +} +if (!is_dir(GSP_ROOT_DIR)) { +$errors[] = 'Detected GSP root path is missing.'; +} +if (realpath(GSP_ROOT_DIR) !== false && realpath(GSP_ROOT_DIR) !== GSP_EXPECTED_ROOT) { +$warnings[] = 'Detected root differs from expected production path: ' . GSP_EXPECTED_ROOT; +} +if (!is_dir(GSP_PANEL_DIR)) { +$errors[] = 'Panel directory is missing.'; +} +if (!is_dir(GSP_WEBSITE_DIR)) { +$errors[] = 'Website directory is missing.'; +} +if (!file_exists($layout['config_file'])) { +$errors[] = 'Panel includes/config.inc.php was not found and cannot be preserved.'; +} +if (!is_dir(GSP_BACKUP_BASE)) { +if (!@mkdir(GSP_BACKUP_BASE, 0755, true) && !is_dir(GSP_BACKUP_BASE)) { +$errors[] = 'Backups directory is missing and cannot be created.'; +} } -// --------------------------------------------------------------------------- -// Backup: dump the MySQL database into $backup_dir/database.sql -// Returns ['success'=>bool, 'error'=>string, 'file'=>string] -// --------------------------------------------------------------------------- -function create_database_backup($backup_dir, $db_config) +foreach ([GSP_ROOT_DIR, GSP_PANEL_DIR, GSP_WEBSITE_DIR, GSP_BACKUP_BASE] as $path) { +if (!is_writable($path)) { +$errors[] = 'Path is not writable: ' . $path; +} +} + +gsp_update_log('Preflight layout: ' . json_encode($layout)); +foreach ($warnings as $warning) { +gsp_update_log('Preflight warning: ' . $warning); +} +foreach ($errors as $error) { +gsp_update_log('Preflight error: ' . $error); +} + +return [ +'success' => empty($errors), +'errors' => $errors, +'warnings' => $warnings, +'layout' => $layout, +]; +} + +function gsp_load_panel_db_config() { - // Verify mysqldump is available before attempting anything - $check_out = []; - $check_ret = 0; - exec('command -v mysqldump 2>/dev/null', $check_out, $check_ret); - if ($check_ret !== 0 || empty($check_out[0])) { - return [ - 'success' => false, - 'error' => 'mysqldump is not installed or not in PATH. ' - . 'Install the mysql-client package and ensure it is on the PATH.', - ]; - } - - $sql_file = $backup_dir . '/database.sql'; - $creds_file = tempnam(sys_get_temp_dir(), 'gsp_db_'); - if ($creds_file === false) { - return ['success' => false, 'error' => 'Cannot create temporary credentials file.']; - } - - // Build MySQL option-file content; addcslashes protects backslashes and - // newlines so values are safe inside the ini-style [client] section. - // The password never appears in the process list this way. - $creds = "[client]\n" - . "user=" . addcslashes($db_config['user'], "\\\n") . "\n" - . "password=" . addcslashes($db_config['pass'], "\\\n") . "\n"; - if (!empty($db_config['host'])) { - $creds .= "host=" . addcslashes($db_config['host'], "\\\n") . "\n"; - } - if (!empty($db_config['port']) && $db_config['port'] !== '3306') { - $creds .= "port=" . addcslashes($db_config['port'], "\\\n") . "\n"; - } - file_put_contents($creds_file, $creds); - chmod($creds_file, 0600); - - // Redirect stderr to a separate temp file so it never pollutes the SQL dump - $err_tmp = tempnam(sys_get_temp_dir(), 'gsp_db_err_'); - if ($err_tmp === false) { - @unlink($creds_file); - return ['success' => false, 'error' => 'Cannot create temporary file for mysqldump error capture.']; - } - $command = 'mysqldump --defaults-extra-file=' . escapeshellarg($creds_file) - . ' --skip-opt --single-transaction --add-drop-table' - . ' --create-options --extended-insert --quick --set-charset' - . ' ' . escapeshellarg($db_config['name']) - . ' > ' . escapeshellarg($sql_file) - . ' 2> ' . escapeshellarg($err_tmp); - - $unused = []; - $ret = 0; - exec($command, $unused, $ret); - @unlink($creds_file); - - // Collect stderr; strip lines mentioning "password" or "passwd" as a - // defensive measure (mysqldump error output does not normally include the - // password, but we filter anyway in case of unusual configurations). - $err_output = ''; - if (file_exists($err_tmp)) { - $raw = trim(file_get_contents($err_tmp)); - @unlink($err_tmp); - if ($raw !== '') { - $err_output = implode("\n", array_filter( - explode("\n", $raw), - static function ($line) { - return stripos($line, 'password') === false - && stripos($line, 'passwd') === false; - } - )); - } - } - - if ($ret !== 0) { - @unlink($sql_file); - $msg = 'mysqldump failed (exit code ' . $ret . ').'; - if ($err_output !== '') { - $msg .= ' Error: ' . $err_output; - } - return ['success' => false, 'error' => $msg]; - } - - if (!file_exists($sql_file) || filesize($sql_file) < 100) { - @unlink($sql_file); - return ['success' => false, 'error' => 'Database dump file is missing or empty after mysqldump.']; - } - - return ['success' => true, 'file' => $sql_file]; +$config_file = GSP_PANEL_DIR . '/includes/config.inc.php'; +if (!is_readable($config_file)) { +return null; +} +$capture = static function ($__file) { +$db_host = 'localhost'; +$db_port = '3306'; +$db_user = ''; +$db_pass = ''; +$db_name = ''; +@include $__file; +return [ +'host' => (string)$db_host, +'port' => (string)$db_port, +'user' => (string)$db_user, +'pass' => (string)$db_pass, +'name' => (string)$db_name, +]; +}; +$cfg = $capture($config_file); +if (empty($cfg['user']) || empty($cfg['name'])) { +return null; +} +return $cfg; } -// --------------------------------------------------------------------------- -// Backup: tar-gzip the panel root into $backup_dir/panel-files.tar.gz -// Returns ['success'=>bool, 'error'=>string, 'file'=>string] -// --------------------------------------------------------------------------- -function create_panel_files_archive($backup_dir, $panel_root) +function gsp_create_database_backup($backup_dir, $db_config) { - // Verify tar is available - $check_out = []; - $check_ret = 0; - exec('command -v tar 2>/dev/null', $check_out, $check_ret); - if ($check_ret !== 0 || empty($check_out[0])) { - return ['success' => false, 'error' => 'tar is not installed or not in PATH.']; - } - - $tar_file = $backup_dir . '/panel-files.tar.gz'; - - // Top-level directories are anchored with ./ so they only match at the - // archive root; wildcard patterns match at any depth. - $exclude_dirs = ['./backups', './.git', './logs', './cache', './tmp', './node_modules', './vendor']; - $exclude_globs = ['*.log', '*.sql']; - - $exclude_args = ''; - foreach ($exclude_dirs as $dir) { - $exclude_args .= ' --exclude=' . escapeshellarg($dir); - } - foreach ($exclude_globs as $glob) { - $exclude_args .= ' --exclude=' . escapeshellarg($glob); - } - - // -C panel_root . preserves relative paths (./home.php, ./modules/…) - $command = 'tar -czf ' . escapeshellarg($tar_file) - . $exclude_args - . ' -C ' . escapeshellarg($panel_root) - . ' . 2>&1'; - - $out = []; - $ret = 0; - exec($command, $out, $ret); - - if ($ret !== 0) { - @unlink($tar_file); - return [ - 'success' => false, - 'error' => 'tar failed (exit code ' . $ret . '). ' . implode(' | ', $out), - ]; - } - - if (!file_exists($tar_file) || filesize($tar_file) < 100) { - @unlink($tar_file); - return ['success' => false, 'error' => 'Panel archive file is missing or empty after tar.']; - } - - return ['success' => true, 'file' => $tar_file]; +$sql_file = $backup_dir . '/database.sql'; +$check = []; +$ret = 0; +exec('command -v mysqldump 2>/dev/null', $check, $ret); +if ($ret !== 0 || empty($check[0])) { +return ['success' => false, 'error' => 'mysqldump is not available.']; +} +$creds_file = tempnam(sys_get_temp_dir(), 'gsp_db_'); +if ($creds_file === false) { +return ['success' => false, 'error' => 'Cannot create temporary credential file.']; +} +$creds = "[client]\n" +. "user=" . addcslashes($db_config['user'], "\\\n") . "\n" +. "password=" . addcslashes($db_config['pass'], "\\\n") . "\n"; +if (!empty($db_config['host'])) { +$creds .= "host=" . addcslashes($db_config['host'], "\\\n") . "\n"; +} +if (!empty($db_config['port']) && $db_config['port'] !== '3306') { +$creds .= "port=" . addcslashes($db_config['port'], "\\\n") . "\n"; +} +@file_put_contents($creds_file, $creds); +@chmod($creds_file, 0600); +$err_tmp = tempnam(sys_get_temp_dir(), 'gsp_db_err_'); +if ($err_tmp === false) { +@unlink($creds_file); +return ['success' => false, 'error' => 'Cannot create temporary error capture file.']; +} +$cmd = 'mysqldump --defaults-extra-file=' . escapeshellarg($creds_file) +. ' --skip-opt --single-transaction --add-drop-table --create-options --extended-insert --quick --set-charset' +. ' ' . escapeshellarg($db_config['name']) +. ' > ' . escapeshellarg($sql_file) +. ' 2> ' . escapeshellarg($err_tmp); +$unused = []; +$ret = 0; +exec($cmd, $unused, $ret); +@unlink($creds_file); +$err = trim((string)@file_get_contents($err_tmp)); +@unlink($err_tmp); +if ($ret !== 0) { +@unlink($sql_file); +return ['success' => false, 'error' => 'mysqldump failed: ' . $err]; +} +if (!file_exists($sql_file) || filesize($sql_file) < 100) { +@unlink($sql_file); +return ['success' => false, 'error' => 'Database dump file missing or empty.']; +} +return ['success' => true, 'file' => $sql_file]; } -// --------------------------------------------------------------------------- -// Backup: write backup.json metadata into $backup_dir -// Returns true on success, false on failure. -// --------------------------------------------------------------------------- -function write_backup_metadata($backup_dir, $metadata) +function gsp_create_archive($source_dir, $archive_file, array $excludes = []) { - return file_put_contents( - $backup_dir . '/backup.json', - json_encode($metadata, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) - ) !== false; +$check = []; +$ret = 0; +exec('command -v tar 2>/dev/null', $check, $ret); +if ($ret !== 0 || empty($check[0])) { +return ['success' => false, 'error' => 'tar is not available.']; +} +if (!is_dir($source_dir)) { +return ['success' => false, 'error' => 'Source directory missing: ' . $source_dir]; +} +$exclude_args = ''; +foreach ($excludes as $exclude) { +$exclude_args .= ' --exclude=' . escapeshellarg($exclude); +} +$cmd = 'tar -czf ' . escapeshellarg($archive_file) +. $exclude_args +. ' -C ' . escapeshellarg($source_dir) +. ' . 2>&1'; +$out = []; +$ret = 0; +exec($cmd, $out, $ret); +if ($ret !== 0) { +@unlink($archive_file); +return ['success' => false, 'error' => 'tar failed: ' . implode(' | ', $out)]; +} +if (!file_exists($archive_file) || filesize($archive_file) < 100) { +@unlink($archive_file); +return ['success' => false, 'error' => 'Archive missing or empty: ' . $archive_file]; +} +return ['success' => true, 'file' => $archive_file]; } -// --------------------------------------------------------------------------- -// Backup: create a full timestamped backup (DB + files + metadata + log) -// --------------------------------------------------------------------------- -function gsp_create_full_backup($update_target_type, $update_target_version) +function gsp_backup_apache_configs($backup_dir) { - $ts = date('Y-m-d_H-i-s'); - $backup_dir = GSP_BACKUP_BASE . '/' . $ts; - - // Helper that writes a timestamped line to backup.log inside $backup_dir - $append_log = static function ($msg) use ($backup_dir) { - $line = '[' . date('Y-m-d H:i:s') . '] ' . $msg . PHP_EOL; - @file_put_contents($backup_dir . '/backup.log', $line, FILE_APPEND | LOCK_EX); - }; - - // 1. Ensure backup base directory exists (0755 so the web server can write) - if (!is_dir(GSP_BACKUP_BASE)) { - $mkdir_ok = @mkdir(GSP_BACKUP_BASE, 0755, true); - if (!$mkdir_ok) { - $last_err = error_get_last(); - $detail = ($last_err && !empty($last_err['message'])) ? ' (' . $last_err['message'] . ')' : ''; - return [ - 'success' => false, - 'error' => 'Cannot create backup base directory: ' . GSP_BACKUP_BASE . $detail - . '. Ensure the web server user has write access to: ' - . dirname(GSP_BACKUP_BASE), - ]; - } - } - - // 2. Create timestamped backup directory - if (!@mkdir($backup_dir, 0755, true)) { - $last_err = error_get_last(); - $detail = ($last_err && !empty($last_err['message'])) ? ' (' . $last_err['message'] . ')' : ''; - return [ - 'success' => false, - 'error' => 'Cannot create backup directory: ' . $backup_dir . $detail - . '. Ensure the web server user has write access to: ' . GSP_BACKUP_BASE, - ]; - } - - $append_log("Backup started. Target: {$update_target_type} / {$update_target_version}"); - $append_log("Panel root: " . GSP_PANEL_DIR); - $append_log("Backup directory: {$backup_dir}"); - - // 3. Load DB configuration from includes/config.inc.php - $db_config = load_panel_db_config(); - if ($db_config === null) { - $append_log("ERROR: Cannot load database configuration from includes/config.inc.php"); - return [ - 'success' => false, - 'error' => 'Cannot load database configuration. ' - . 'Ensure includes/config.inc.php exists and contains valid DB credentials.', - ]; - } - $append_log("DB config loaded. Host: {$db_config['host']}, Database: {$db_config['name']}"); - - // 4. Database backup — stops the update if it fails - $append_log("Starting database backup (mysqldump)..."); - $db_result = create_database_backup($backup_dir, $db_config); - if (!$db_result['success']) { - $append_log("ERROR: Database backup failed: " . $db_result['error']); - return ['success' => false, 'error' => $db_result['error']]; - } - $append_log("Database backup complete: database.sql (" . filesize($db_result['file']) . " bytes)"); - - // 5. Panel files archive — stops the update if it fails - $append_log("Starting panel files archive (tar gzip)..."); - $tar_result = create_panel_files_archive($backup_dir, GSP_PANEL_DIR); - if (!$tar_result['success']) { - $append_log("ERROR: Panel files archive failed: " . $tar_result['error']); - return ['success' => false, 'error' => $tar_result['error']]; - } - $append_log("Panel files archive complete: panel-files.tar.gz (" . filesize($tar_result['file']) . " bytes)"); - - // 6. Write backup.json metadata - $vinfo = gsp_read_version_json(); - $metadata = [ - 'backup_timestamp' => $ts, - 'panel_root' => GSP_PANEL_DIR, - 'database_host' => $db_config['host'], - 'database_name' => $db_config['name'], - 'installed_version' => $vinfo - ? ($vinfo['installed_version'] ?? gsp_get_current_version()) - : gsp_get_current_version(), - 'git_branch' => gsp_get_current_branch(), - 'git_commit' => gsp_get_git_commit(), - 'update_target_type' => $update_target_type, - 'update_target_version' => $update_target_version, - 'backup_status' => 'complete', - ]; - write_backup_metadata($backup_dir, $metadata); - $append_log("Backup metadata written (backup.json)."); - - // 7. Final validation — both required files must be present and non-empty - $sql_file = $backup_dir . '/database.sql'; - $tar_file = $backup_dir . '/panel-files.tar.gz'; - - if (!file_exists($sql_file) || filesize($sql_file) < 100) { - $append_log("ERROR: Validation failed — database.sql is missing or empty."); - return ['success' => false, 'error' => 'Backup validation failed: database.sql is missing or empty.']; - } - if (!file_exists($tar_file) || filesize($tar_file) < 100) { - $append_log("ERROR: Validation failed — panel-files.tar.gz is missing or empty."); - return ['success' => false, 'error' => 'Backup validation failed: panel-files.tar.gz is missing or empty.']; - } - - $append_log("Backup validated and complete."); - - return [ - 'success' => true, - 'backup_dir' => $backup_dir, - 'backup_ts' => $ts, - ]; +$apache_source = '/etc/apache2/sites-available'; +if (!is_dir($apache_source)) { +return ['success' => false, 'error' => 'Apache sites-available path not found: ' . $apache_source]; +} +$dest = $backup_dir . '/apache-sites-available'; +if (!@mkdir($dest, 0755, true) && !is_dir($dest)) { +return ['success' => false, 'error' => 'Cannot create apache backup directory.']; +} +$files = glob($apache_source . '/*.conf') ?: []; +foreach ($files as $file) { +@copy($file, $dest . '/' . basename($file)); +} +return ['success' => true, 'path' => $dest, 'count' => count($files)]; +} + +function gsp_prune_old_backups($max_backups = 5) +{ +$entries = gsp_get_available_backups(); +if (count($entries) <= $max_backups) { +return; +} +$to_delete = array_slice($entries, $max_backups); +foreach ($to_delete as $entry) { +gsp_rmdir_recursive(GSP_BACKUP_BASE . '/' . $entry['ts']); +gsp_update_log('Pruned old backup: ' . $entry['ts']); +} +} + +function gsp_create_full_backup($update_target_type, $update_target_version, $include_apache = false) +{ +$ts = date('Y-m-d_H-i-s'); +$backup_dir = GSP_BACKUP_BASE . '/' . $ts; +if (!is_dir(GSP_BACKUP_BASE) && !@mkdir(GSP_BACKUP_BASE, 0755, true)) { +return ['success' => false, 'error' => 'Cannot create backup base directory.']; +} +if (!@mkdir($backup_dir, 0755, true)) { +return ['success' => false, 'error' => 'Cannot create backup directory: ' . $backup_dir]; +} + +$meta = [ +'backup_timestamp' => $ts, +'gsp_root' => GSP_ROOT_DIR, +'panel_root' => GSP_PANEL_DIR, +'website_root' => GSP_WEBSITE_DIR, +'update_target_type' => $update_target_type, +'update_target_version' => $update_target_version, +'created_at' => date('Y-m-d H:i:s'), +]; + +$db_config = gsp_load_panel_db_config(); +if ($db_config !== null) { +$db_backup = gsp_create_database_backup($backup_dir, $db_config); +if (!$db_backup['success']) { +return ['success' => false, 'error' => $db_backup['error']]; +} +$meta['database_backup'] = basename($db_backup['file']); +} + +$panel_backup = gsp_create_archive( +GSP_PANEL_DIR, +$backup_dir . '/panel-files.tar.gz', +['./backups', './logs', './cache', './tmp', './node_modules', './vendor'] +); +if (!$panel_backup['success']) { +return ['success' => false, 'error' => $panel_backup['error']]; +} +$meta['panel_archive'] = basename($panel_backup['file']); + +$website_backup = gsp_create_archive( +GSP_WEBSITE_DIR, +$backup_dir . '/website-files.tar.gz', +['./logs', './cache', './tmp'] +); +if (!$website_backup['success']) { +return ['success' => false, 'error' => $website_backup['error']]; +} +$meta['website_archive'] = basename($website_backup['file']); + +if (file_exists(GSP_VERSION_JSON)) { +@copy(GSP_VERSION_JSON, $backup_dir . '/version.json.bak'); +$meta['version_json_backup'] = 'version.json.bak'; +} + +if ($include_apache) { +$apache_backup = gsp_backup_apache_configs($backup_dir); +if ($apache_backup['success']) { +$meta['apache_backup'] = $apache_backup['path']; +} else { +$meta['apache_backup_error'] = $apache_backup['error']; +} +} + +@file_put_contents($backup_dir . '/backup.json', json_encode($meta, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); +gsp_prune_old_backups(5); +gsp_update_log('Backup created: ' . $backup_dir); + +return [ +'success' => true, +'backup_dir' => $backup_dir, +'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, true); - curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); - 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' => true, - 'verify_peer_name' => true, - ], - ]); - $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; +$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, true); +curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); +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, +], +'ssl' => [ +'verify_peer' => true, +'verify_peer_name' => true, +], +]); +$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) +function gsp_extract_update_source($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]; - } - - $panel_src = gsp_locate_panel_source_dir($src_dir); - if ($panel_src === null) { - gsp_rmdir_recursive($temp_dir); - return ['success' => false, 'error' => 'Update archive does not contain a valid panel source directory.']; - } - $src_dir = $panel_src; - - // Copy files from the extracted panel 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]; +$temp_dir = sys_get_temp_dir() . '/gsp_upd_' . time() . '_' . mt_rand(1000, 9999); +if (!@mkdir($temp_dir, 0750, true)) { +return ['success' => false, 'error' => 'Cannot create temp extraction directory.']; +} +require_once(GSP_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]; +} +$source_root = $temp_dir; +$subdirs = glob($temp_dir . '/*', GLOB_ONLYDIR); +if ($subdirs && count($subdirs) === 1) { +$source_root = $subdirs[0]; +} +return ['success' => true, 'temp_dir' => $temp_dir, 'source_root' => $source_root]; } -// --------------------------------------------------------------------------- -// Helper: recursively remove a directory -// --------------------------------------------------------------------------- -function gsp_rmdir_recursive($dir) +function gsp_normalize_rel($path) { - 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); +$path = str_replace('\\', '/', $path); +$path = ltrim($path, '/'); +return $path; } -// --------------------------------------------------------------------------- -// Post-update helpers -// --------------------------------------------------------------------------- -function gsp_fix_permissions($panel_dir) +function gsp_is_preserved_path($relative_path) { - // Restore sane defaults in a single find traversal: files 644, directories 755 - @system( - 'find ' . escapeshellarg($panel_dir) - . ' -maxdepth 10 \( -name "*.php" -exec chmod 644 {} + \)' - . ' -o \( -type d -exec chmod 755 {} + \)' - . ' 2>/dev/null' - ); +$relative_path = gsp_normalize_rel($relative_path); +$exact = [ +'Panel/includes/config.inc.php', +]; +$prefixes = [ +'logs/', +'backups/', +'Panel/logs/', +'Panel/backups/', +'Website/logs/', +'Website/uploads/', +'Website/upload/', +'Panel/uploads/', +'Panel/upload/', +]; +if (in_array($relative_path, $exact, true)) { +return true; +} +foreach ($prefixes as $prefix) { +if (strpos($relative_path, $prefix) === 0) { +return true; +} +} +return false; +} + +function gsp_copy_file($src, $dst) +{ +$parent = dirname($dst); +if (!is_dir($parent)) { +@mkdir($parent, 0755, true); +} +return @copy($src, $dst); +} + +function gsp_copy_tree($src_root, $dst_root, $base_rel = '') +{ +$copied = 0; +$skipped = []; +$source = rtrim($src_root, '/'); +$iter = new RecursiveIteratorIterator( +new RecursiveDirectoryIterator($source, RecursiveDirectoryIterator::SKIP_DOTS), +RecursiveIteratorIterator::SELF_FIRST +); +foreach ($iter as $item) { +$rel = gsp_normalize_rel(($base_rel !== '' ? $base_rel . '/' : '') . substr($item->getPathname(), strlen($source) + 1)); +if (gsp_is_preserved_path($rel)) { +$skipped[] = $rel; +continue; +} +$dst = rtrim($dst_root, '/') . '/' . $rel; +if ($item->isDir()) { +if (!is_dir($dst)) { +@mkdir($dst, 0755, true); +} +continue; +} +if (gsp_copy_file($item->getPathname(), $dst)) { +$copied++; +} +} +return ['copied' => $copied, 'skipped' => $skipped]; +} + +function gsp_updater_watch_list() +{ +return [ +'Panel/modules/administration/panel_update.php', +'Panel/modules/update/update.php', +'Panel/modules/update/updating.php', +'Panel/modules/update/post_update.php', +'Panel/modules/update/module.php', +'Panel/modules/update/unzip.php', +'Panel/modules/update/blacklist.php', +'Panel/modules/update/patch_manager.php', +'Panel/modules/update/patches', +]; +} + +function gsp_collect_files_under($path, $base_rel) +{ +$list = []; +if (is_file($path)) { +$list[] = gsp_normalize_rel($base_rel); +return $list; +} +if (!is_dir($path)) { +return $list; +} +$iter = new RecursiveIteratorIterator( +new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS), +RecursiveIteratorIterator::LEAVES_ONLY +); +foreach ($iter as $item) { +if ($item->isFile()) { +$list[] = gsp_normalize_rel($base_rel . '/' . substr($item->getPathname(), strlen($path) + 1)); +} +} +return $list; +} + +function gsp_detect_updater_drift_files($source_root, $target_root) +{ +$drift = []; +foreach (gsp_updater_watch_list() as $rel) { +$src = rtrim($source_root, '/') . '/' . $rel; +$files = gsp_collect_files_under($src, $rel); +foreach ($files as $fileRel) { +$s = rtrim($source_root, '/') . '/' . $fileRel; +$d = rtrim($target_root, '/') . '/' . $fileRel; +if (!file_exists($d)) { +$drift[] = $fileRel; +continue; +} +if (@hash_file('sha256', $s) !== @hash_file('sha256', $d)) { +$drift[] = $fileRel; +} +} +} +return array_values(array_unique($drift)); +} + +function gsp_apply_updater_files_only($source_root, $target_root, array $drift_files) +{ +$copied = 0; +foreach ($drift_files as $rel) { +$src = rtrim($source_root, '/') . '/' . $rel; +$dst = rtrim($target_root, '/') . '/' . $rel; +if (is_file($src) && gsp_copy_file($src, $dst)) { +$copied++; +} +} +return $copied; +} + +function gsp_apply_layout_sync($source_root) +{ +$top_level = scandir($source_root); +$skip = ['.', '..', '.git', '.github', '.gitignore', '.vscode']; +$copied = 0; +$skipped = []; +foreach ((array)$top_level as $entry) { +if (in_array($entry, $skip, true)) { +continue; +} +$src = rtrim($source_root, '/') . '/' . $entry; +$dst = GSP_ROOT_DIR . '/' . $entry; +if ($entry === 'backups') { +$skipped[] = 'backups/'; +continue; +} +if (is_file($src)) { +$rel = gsp_normalize_rel($entry); +if (gsp_is_preserved_path($rel)) { +$skipped[] = $rel; +continue; +} +if (gsp_copy_file($src, $dst)) { +$copied++; +} +continue; +} +if (is_dir($src)) { +$part = gsp_copy_tree($src, GSP_ROOT_DIR, $entry); +$copied += $part['copied']; +$skipped = array_merge($skipped, $part['skipped']); +} +} +return [ +'success' => true, +'files_copied' => $copied, +'skipped' => array_values(array_unique($skipped)), +]; +} + +function gsp_run_required_patches($updater_version) +{ +global $db; +if (!function_exists('gsp_patch_run_all')) { +return ['success' => false, 'error' => 'Patch manager helper is unavailable.']; +} +$run = gsp_patch_run_all($db, GSP_PATCH_DIR, 'gsp_update_log', $updater_version); +if (!$run['success']) { +return [ +'success' => false, +'error' => 'Patch failure at ' . $run['failed_patch'] . ': ' . $run['error'], +'run' => $run, +]; +} +return ['success' => true, 'run' => $run]; +} + +function gsp_apply_update_from_zip($zip_file, $restart_nonce = '') +{ +$extract = gsp_extract_update_source($zip_file); +if (!$extract['success']) { +return $extract; +} +$temp_dir = $extract['temp_dir']; +$source_root = $extract['source_root']; +$updater_version = substr((string)@hash_file('sha256', $source_root . '/Panel/modules/administration/panel_update.php'), 0, 12); + +$drift_files = gsp_detect_updater_drift_files($source_root, GSP_ROOT_DIR); +if (!empty($drift_files) && empty($restart_nonce)) { +$copied = gsp_apply_updater_files_only($source_root, GSP_ROOT_DIR, $drift_files); +$nonce = gsp_random_token(12); +$_SESSION['gsp_update_restart_nonce'] = $nonce; +gsp_update_log('Updater self-update applied (' . $copied . ' files); restart nonce=' . $nonce); +gsp_rmdir_recursive($temp_dir); +return [ +'success' => false, +'restart_required' => true, +'restart_nonce' => $nonce, +'updater_files_updated' => $copied, +'drift_files' => $drift_files, +]; +} +if (!empty($restart_nonce)) { +$expected = isset($_SESSION['gsp_update_restart_nonce']) ? $_SESSION['gsp_update_restart_nonce'] : null; +if ($expected === null || !hash_equals($expected, $restart_nonce)) { +gsp_rmdir_recursive($temp_dir); +return ['success' => false, 'error' => 'Invalid updater restart marker.']; +} +unset($_SESSION['gsp_update_restart_nonce']); +} + +$patches = gsp_run_required_patches($updater_version); +if (!$patches['success']) { +gsp_rmdir_recursive($temp_dir); +return ['success' => false, 'error' => $patches['error']]; +} + +$sync = gsp_apply_layout_sync($source_root); +gsp_rmdir_recursive($temp_dir); +if (!$sync['success']) { +return $sync; +} +gsp_update_log('Layout sync complete: copied=' . $sync['files_copied'] . ', skipped=' . count($sync['skipped'])); +if (!empty($sync['skipped'])) { +gsp_update_log('Preserved paths: ' . implode(', ', array_slice($sync['skipped'], 0, 50))); +} +return [ +'success' => true, +'files_copied' => $sync['files_copied'], +'preserved' => $sync['skipped'], +'patches' => $patches['run'], +]; +} + +function gsp_fix_permissions($root_dir) +{ +@system( +'find ' . escapeshellarg($root_dir) +. ' -maxdepth 12 \( -name "*.php" -exec chmod 644 {} + \)' +. ' -o \( -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); - } - } +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); +} +} +} + +function gsp_do_update($repo_owner, $repo_name, $ref, $update_type, $restart_nonce = '') +{ +global $db; +$preflight = gsp_preflight_check(); +if (!$preflight['success']) { +return ['success' => false, 'error' => 'Preflight failed: ' . implode(' | ', $preflight['errors'])]; +} + +$backup = gsp_create_full_backup($update_type, $ref, false); +if (!$backup['success']) { +return $backup; +} +gsp_update_log("Backup created at {$backup['backup_ts']} before {$update_type} update to {$ref}"); + +$temp_dir = sys_get_temp_dir() . '/gsp_dl_' . time() . '_' . mt_rand(1000, 9999); +@mkdir($temp_dir, 0750, true); +$zip_file = gsp_download_zip($repo_owner, $repo_name, $ref, $temp_dir); +if (!$zip_file) { +gsp_rmdir_recursive($temp_dir); +return ['success' => false, 'error' => 'Failed to download update ZIP from GitHub.']; +} + +$apply = gsp_apply_update_from_zip($zip_file, $restart_nonce); +@unlink($zip_file); +gsp_rmdir_recursive($temp_dir); +if (!empty($apply['restart_required'])) { +$apply['backup_dir'] = $backup['backup_dir']; +$apply['success'] = false; +return $apply; +} +if (!$apply['success']) { +return $apply; +} + +$commit_after = gsp_get_git_commit(); +gsp_fix_permissions(GSP_ROOT_DIR); +gsp_clear_panel_cache(GSP_PANEL_DIR); +gsp_write_version_file($ref, $update_type); +$vsource = ($update_type === 'release') ? 'GitHub Releases' : $ref; +$vversion = ($update_type === 'release') ? $ref : ($commit_after ?: $ref); +gsp_write_version_json($update_type, $vsource, $vversion, $commit_after); +$db->setSettings(['ogp_version' => $ref, 'version_type' => $update_type]); + +if (file_exists(GSP_PANEL_DIR . '/modules/modulemanager/module_handling.php')) { +require_once(GSP_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'], +'backup_dir' => $backup['backup_dir'], +'preserved' => $apply['preserved'], +'patches' => $apply['patches'], +]; } -// --------------------------------------------------------------------------- -// 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; +$backups = []; +if (!is_dir(GSP_BACKUP_BASE)) { +return $backups; +} +foreach ((array)scandir(GSP_BACKUP_BASE) as $entry) { +if ($entry === '.' || $entry === '..') { +continue; +} +$dir = GSP_BACKUP_BASE . '/' . $entry; +if (!is_dir($dir)) { +continue; +} +if (!preg_match('/^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}$/', $entry)) { +continue; +} +$meta_file = $dir . '/backup.json'; +$meta = []; +if (file_exists($meta_file)) { +$meta = json_decode(@file_get_contents($meta_file), true) ?: []; +} +$backups[] = ['ts' => $entry, 'meta' => $meta]; +} +usort($backups, function ($a, $b) { +return strcmp($b['ts'], $a['ts']); +}); +return $backups; } -// --------------------------------------------------------------------------- -// Update: attempt a git-based update (fetch + checkout + reset --hard) -// Returns an array on success or null if git is unavailable / fails. -// --------------------------------------------------------------------------- -function gsp_try_git_update($branch) +function gsp_restore_archive($archive, $target) { - $repo_root = gsp_detect_repo_root(); - if (!function_exists('exec') || !$repo_root) { - return null; - } - - $panel_source = gsp_locate_panel_source_dir($repo_root); - if ($panel_source === null) { - gsp_update_log("Git update aborted: panel source directory not found under repo root {$repo_root}"); - return null; - } - - $panel_arg = escapeshellarg($repo_root); - $branch_arg = escapeshellarg($branch); - $origin_ref = escapeshellarg('origin/' . $branch); - - $out = []; - $ret = 0; - - exec("git -C {$panel_arg} fetch origin {$branch_arg} --tags 2>&1", $out, $ret); - if ($ret !== 0) { - gsp_update_log("Git fetch for branch {$branch} failed (exit {$ret}): " . implode(' | ', $out)); - return null; - } - - exec("git -C {$panel_arg} checkout {$branch_arg} 2>&1", $out, $ret); - exec("git -C {$panel_arg} reset --hard {$origin_ref} 2>&1", $out, $ret); - if ($ret !== 0) { - gsp_update_log("Git checkout/reset for branch {$branch} failed (exit {$ret}): " . implode(' | ', $out)); - return null; - } - - $commit_out = []; - $ret2 = 0; - exec("git -C {$panel_arg} rev-parse HEAD 2>&1", $commit_out, $ret2); - $commit = ($ret2 === 0 && !empty($commit_out[0]) && preg_match('/^[0-9a-f]{40,64}$/i', trim($commit_out[0]))) - ? trim($commit_out[0]) - : null; - - return [ - 'success' => true, - 'method' => 'git', - 'commit' => $commit, - 'output' => implode("\n", $out), - ]; +if (!file_exists($archive)) { +return ['success' => false, 'error' => 'Archive not found: ' . $archive]; +} +$out = []; +$ret = 0; +exec('tar -xzf ' . escapeshellarg($archive) . ' -C ' . escapeshellarg($target) . ' 2>&1', $out, $ret); +if ($ret !== 0) { +return ['success' => false, 'error' => 'tar extract failed: ' . implode(' | ', $out)]; +} +return ['success' => true]; } -// --------------------------------------------------------------------------- -// Orchestrate a full update -// --------------------------------------------------------------------------- -function gsp_do_update($repo_owner, $repo_name, $ref, $update_type) +function gsp_restore_database_from_backup($backup_dir) { - 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 — try git for branch updates; fall back to ZIP download - $commit_after = null; - $files_copied = 0; - $used_git = false; - - if ($update_type !== 'release') { - $git_result = gsp_try_git_update($ref); - if ($git_result && $git_result['success']) { - $commit_after = $git_result['commit']; - $used_git = true; - gsp_update_log("Updated via git to {$ref}: " . $git_result['output']); - } else { - gsp_update_log("Git update not available or failed for {$ref}; falling back to ZIP download"); - } - } - - if (!$used_git) { - $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}"); - - $apply = gsp_apply_update($zip_file); - @unlink($zip_file); - @rmdir($temp_dir); - if (!$apply['success']) { - return $apply; - } - $files_copied = $apply['files_copied']; - $commit_after = gsp_get_git_commit(); - gsp_update_log("Applied update via ZIP: {$apply['files_copied']} files written"); - } - - // Step 3 — housekeeping - gsp_fix_permissions($panel_dir); - gsp_clear_panel_cache($panel_dir); - gsp_write_version_file($ref, $update_type); - - // Write version.json with canonical type/source/version data - $vsource = ($update_type === 'release') ? 'GitHub Releases' : $ref; - $vversion = ($update_type === 'release') ? $ref : ($commit_after ?? $ref); - gsp_write_version_json($update_type, $vsource, $vversion, $commit_after); - - $db->setSettings(['ogp_version' => $ref, 'version_type' => $update_type]); - - // Step 4 — 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' => $files_copied, 'backup_dir' => $backup['backup_dir']]; +$sql_file = $backup_dir . '/database.sql'; +if (!file_exists($sql_file)) { +return ['success' => true, 'skipped' => true]; +} +$db_config = gsp_load_panel_db_config(); +if ($db_config === null) { +return ['success' => false, 'error' => 'Cannot load DB config for restore.']; +} +$creds_file = tempnam(sys_get_temp_dir(), 'gsp_db_'); +if ($creds_file === false) { +return ['success' => false, 'error' => 'Cannot create temporary DB credential file.']; +} +$creds = "[client]\n" +. "user=" . addcslashes($db_config['user'], "\\\n") . "\n" +. "password=" . addcslashes($db_config['pass'], "\\\n") . "\n"; +if (!empty($db_config['host'])) { +$creds .= "host=" . addcslashes($db_config['host'], "\\\n") . "\n"; +} +if (!empty($db_config['port']) && $db_config['port'] !== '3306') { +$creds .= "port=" . addcslashes($db_config['port'], "\\\n") . "\n"; +} +@file_put_contents($creds_file, $creds); +@chmod($creds_file, 0600); +$cmd = 'mysql --defaults-extra-file=' . escapeshellarg($creds_file) +. ' ' . escapeshellarg($db_config['name']) +. ' < ' . escapeshellarg($sql_file) +. ' 2>&1'; +$out = []; +$ret = 0; +exec($cmd, $out, $ret); +@unlink($creds_file); +if ($ret !== 0) { +return ['success' => false, 'error' => 'MySQL restore failed: ' . implode(' | ', $out)]; +} +return ['success' => true]; } -// --------------------------------------------------------------------------- -// Orchestrate a revert to a previous backup -// --------------------------------------------------------------------------- -function gsp_do_revert($backup_ts) +function gsp_do_revert($backup_ts, $restore_apache = false) { - 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)]; - } - - // Detect backup format: new (panel-files.tar.gz) or legacy (files/ directory) - $tar_archive = $backup_dir . '/panel-files.tar.gz'; - $legacy_files_dir = $backup_dir . '/files'; - $use_tar = file_exists($tar_archive); - - if (!$use_tar && !is_dir($legacy_files_dir)) { - return ['success' => false, 'error' => 'Backup files not found (expected panel-files.tar.gz or files/ directory).']; - } - - // Detect SQL file: new (database.sql) or legacy glob *.sql - $sql_file = $backup_dir . '/database.sql'; - if (!file_exists($sql_file)) { - $sql_candidates = glob($backup_dir . '/*.sql') ?: []; - $sql_file = !empty($sql_candidates) ? $sql_candidates[0] : null; - } - if (!$sql_file || !file_exists($sql_file)) { - return ['success' => false, 'error' => 'No SQL dump found in backup.']; - } - - // 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 - $files_restored = 0; - if ($use_tar) { - // New format: extract tar archive back to the panel root - $out = []; - $ret = 0; - exec('tar -xzf ' . escapeshellarg($tar_archive) . ' -C ' . escapeshellarg($panel_dir) . ' 2>&1', $out, $ret); - if ($ret === 0) { - // tar restores files in place; it does not provide a count, - // so use 0 here — callers should not rely on an exact file count for the tar path. - $files_restored = 0; - gsp_update_log("Revert: extracted panel-files.tar.gz to {$panel_dir}"); - } else { - gsp_update_log("Revert warning: tar extraction exited with code {$ret}: " . implode(' | ', $out)); - } - } else { - // Legacy format: recursive copy from files/ directory - $iter = new RecursiveIteratorIterator( - new RecursiveDirectoryIterator($legacy_files_dir, RecursiveDirectoryIterator::SKIP_DOTS), - RecursiveIteratorIterator::SELF_FIRST - ); - foreach ($iter as $item) { - $rel = substr($item->getPathname(), strlen($legacy_files_dir)); - $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)) { - $files_restored++; - } - } - } - gsp_update_log("Revert: restored {$files_restored} files from legacy backup {$backup_ts}"); - } - - // Restore database using credentials from config - $db_config = load_panel_db_config(); - if ($db_config !== null) { - $creds_file = tempnam(sys_get_temp_dir(), 'gsp_db_'); - if ($creds_file !== false) { - $creds = "[client]\n" - . "user=" . addcslashes($db_config['user'], "\\\n") . "\n" - . "password=" . addcslashes($db_config['pass'], "\\\n") . "\n"; - if (!empty($db_config['host'])) { - $creds .= "host=" . addcslashes($db_config['host'], "\\\n") . "\n"; - } - if (!empty($db_config['port']) && $db_config['port'] !== '3306') { - $creds .= "port=" . addcslashes($db_config['port'], "\\\n") . "\n"; - } - file_put_contents($creds_file, $creds); - chmod($creds_file, 0600); - $cmd = 'mysql --defaults-extra-file=' . escapeshellarg($creds_file) - . ' ' . escapeshellarg($db_config['name']) - . ' < ' . escapeshellarg($sql_file) - . ' 2>&1'; - $unused_out = []; - $ret = 0; - exec($cmd, $unused_out, $ret); - @unlink($creds_file); - if ($ret !== 0) { - gsp_update_log("Revert warning: database restore exited with code {$ret}"); - } - } - } else { - gsp_update_log("Revert warning: could not load DB config; database was not restored."); - } - - // 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' => $files_restored]; +global $db; +$backup_dir = GSP_BACKUP_BASE . '/' . $backup_ts; +if (!is_dir($backup_dir)) { +return ['success' => false, 'error' => 'Backup not found: ' . $backup_ts]; +} + +$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 backup. Please wait.', +]); +} + +$panel_restore = gsp_restore_archive($backup_dir . '/panel-files.tar.gz', GSP_PANEL_DIR); +if (!$panel_restore['success']) { +if (!$had_maintenance) { +$db->setSettings(['maintenance_mode' => '0']); +} +return $panel_restore; +} +$website_archive = $backup_dir . '/website-files.tar.gz'; +if (file_exists($website_archive)) { +$website_restore = gsp_restore_archive($website_archive, GSP_WEBSITE_DIR); +if (!$website_restore['success']) { +if (!$had_maintenance) { +$db->setSettings(['maintenance_mode' => '0']); +} +return $website_restore; +} +} +if (file_exists($backup_dir . '/version.json.bak')) { +@copy($backup_dir . '/version.json.bak', GSP_VERSION_JSON); +} +$db_restore = gsp_restore_database_from_backup($backup_dir); +if (!$db_restore['success']) { +gsp_update_log('Revert warning: ' . $db_restore['error']); +} + +if ($restore_apache && is_dir($backup_dir . '/apache-sites-available')) { +$apache_restore = gsp_restore_apache_backup($backup_dir . '/apache-sites-available', true); +if (!$apache_restore['success']) { +gsp_update_log('Revert apache restore warning: ' . $apache_restore['error']); +} +} + +gsp_fix_permissions(GSP_ROOT_DIR); +gsp_clear_panel_cache(GSP_PANEL_DIR); +if (!$had_maintenance) { +$db->setSettings(['maintenance_mode' => '0']); +} + +gsp_update_log('Revert complete: ' . $backup_ts); +return ['success' => true, 'files_restored' => 0]; +} + +function gsp_scan_apache_configs() +{ +$base = '/etc/apache2/sites-available'; +$result = [ +'success' => true, +'available' => is_dir($base), +'base' => $base, +'files' => [], +'issues' => [], +'recommendations' => [], +]; +if (!$result['available']) { +$result['success'] = false; +$result['issues'][] = 'Apache sites-available directory not found.'; +return $result; +} +$stale = [ +'/var/www/html/panel', +'/var/www/html/GSP/Panel/GSP/Panel', +'/var/www/html/GSP/Panel/modules/billing', +]; +$files = glob($base . '/*.conf') ?: []; +foreach ($files as $file) { +$lines = @file($file, FILE_IGNORE_NEW_LINES); +if (!is_array($lines)) { +continue; +} +$file_info = ['file' => $file, 'document_roots' => [], 'directories' => [], 'stale_hits' => []]; +foreach ($lines as $line) { +if (preg_match('/^\s*DocumentRoot\s+(.+)$/i', $line, $m)) { +$path = trim($m[1], "\"' "); +$file_info['document_roots'][] = $path; +} +if (preg_match('/^\s*/i', $line, $m)) { +$path = trim($m[1], "\"' "); +$file_info['directories'][] = $path; +} +foreach ($stale as $stalePath) { +if (strpos($line, $stalePath) !== false) { +$file_info['stale_hits'][] = $stalePath; +$result['issues'][] = basename($file) . ' contains stale path: ' . $stalePath; +} +} +} +if (!empty($file_info['stale_hits'])) { +if (stripos($file, 'gameservers.world') !== false) { +$result['recommendations'][] = basename($file) . ' should target ' . GSP_WEBSITE_DIR; +} else { +$result['recommendations'][] = basename($file) . ' should target ' . GSP_PANEL_DIR; +} +} +$result['files'][] = $file_info; +} +return $result; +} + +function gsp_restore_apache_backup($backup_dir, $reload_apache) +{ +$target = '/etc/apache2/sites-available'; +if (!is_dir($backup_dir)) { +return ['success' => false, 'error' => 'Apache backup folder not found.']; +} +$files = glob($backup_dir . '/*.conf') ?: []; +foreach ($files as $file) { +@copy($file, $target . '/' . basename($file)); +} +$test = gsp_apache_configtest(); +if (!$test['success']) { +return ['success' => false, 'error' => 'apache2ctl configtest failed after restore: ' . $test['output']]; +} +if ($reload_apache) { +gsp_apache_reload(); +} +return ['success' => true, 'restored' => count($files)]; +} + +function gsp_apache_configtest() +{ +$out = []; +$ret = 0; +exec('apache2ctl configtest 2>&1', $out, $ret); +return [ +'success' => ($ret === 0), +'output' => trim(implode("\n", $out)), +]; +} + +function gsp_apache_reload() +{ +$out = []; +$ret = 0; +exec('apache2ctl graceful 2>&1', $out, $ret); +return [ +'success' => ($ret === 0), +'output' => trim(implode("\n", $out)), +]; +} + +function gsp_fix_apache_paths($confirmed, $reload_apache) +{ +if (!$confirmed) { +return ['success' => false, 'error' => 'Apache path fix requires explicit confirmation.']; +} +$scan = gsp_scan_apache_configs(); +if (!$scan['available']) { +return ['success' => false, 'error' => 'Apache config folder not available.']; +} + +$backup = gsp_create_full_backup('apache-fix', 'apache-path-repair', true); +if (!$backup['success']) { +return ['success' => false, 'error' => 'Could not create backup before apache fix: ' . $backup['error']]; +} + +$base = '/etc/apache2/sites-available'; +$files = glob($base . '/*.conf') ?: []; +$replace = [ +'/var/www/html/panel' => GSP_PANEL_DIR, +'/var/www/html/GSP/Panel/GSP/Panel' => GSP_PANEL_DIR, +'/var/www/html/GSP/Panel/modules/billing' => GSP_WEBSITE_DIR, +]; +$changed = []; +foreach ($files as $file) { +$orig = @file_get_contents($file); +if ($orig === false) { +continue; +} +$new = strtr($orig, $replace); +if ($new !== $orig) { +@file_put_contents($file, $new); +$changed[] = $file; +} +} + +$test = gsp_apache_configtest(); +if (!$test['success']) { +$restore = gsp_restore_apache_backup($backup['backup_dir'] . '/apache-sites-available', false); +return [ +'success' => false, +'error' => 'apache2ctl configtest failed; restored backup. Output: ' . $test['output'] +. ($restore['success'] ? '' : (' | restore failed: ' . $restore['error'])), +]; +} + +$reload = ['success' => true, 'output' => 'Apache reload skipped']; +if ($reload_apache) { +$reload = gsp_apache_reload(); +} + +gsp_update_log('Apache path fix changed ' . count($changed) . ' file(s).'); +return [ +'success' => true, +'changed_files' => $changed, +'backup_dir' => $backup['backup_dir'], +'configtest' => $test, +'reload' => $reload, +]; +} + +function gsp_get_patch_overview() +{ +global $db; +if (!function_exists('gsp_patch_load_definitions') || !function_exists('gsp_patch_get_applied_map') || !function_exists('gsp_patch_state_fallback_file')) { +return ['available' => [], 'applied' => [], 'pending' => []]; +} +$defs = gsp_patch_load_definitions(GSP_PATCH_DIR); +$applied = gsp_patch_get_applied_map($db, gsp_patch_state_fallback_file()); +$pending = []; +foreach ($defs as $def) { +$id = (string)$def['id']; +if (!isset($applied[$id]) || $applied[$id]['status'] !== 'applied') { +$pending[] = $id; +} +} +return [ +'available' => $defs, +'applied' => $applied, +'pending' => $pending, +]; +} + +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); +} + +function gsp_render_restart_form($action, $csrf_token, $nonce, $release_version = '') +{ +echo "
"; +echo ""; +echo ""; +echo ""; +if ($release_version !== '') { +echo ""; +} +echo "
"; +echo ""; } -// --------------------------------------------------------------------------- -// Main entry point — render the "Panel Updates" section -// --------------------------------------------------------------------------- function gsp_panel_update_section() { - global $db, $settings; +global $db, $settings; +if ($_SESSION['users_group'] !== 'admin') { +return; +} - // Guard: admins only - if ($_SESSION['users_group'] !== 'admin') { - return; - } +$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'; - // 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'; +if (empty($_SESSION['gsp_update_csrf'])) { +$_SESSION['gsp_update_csrf'] = gsp_random_token(); +} +$csrf_token = $_SESSION['gsp_update_csrf']; - // Per-session CSRF token - if (empty($_SESSION['gsp_update_csrf'])) { - $_SESSION['gsp_update_csrf'] = gsp_random_token(); - } - $csrf_token = $_SESSION['gsp_update_csrf']; +$apache_scan_result = null; +$preflight_result = null; +$auto_restart_payload = null; - // ---- 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); +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 and try again.'); +} else { +$action = $_POST['gsp_update_action']; +$started_at = date('Y-m-d H:i:s'); +$restart_nonce = isset($_POST['gsp_restart_nonce']) ? trim($_POST['gsp_restart_nonce']) : ''; +set_time_limit(0); - $user_label = htmlspecialchars($_SESSION['users_login']) - . ' (IP: ' . htmlspecialchars($_SERVER['REMOTE_ADDR']) . ')'; +if ($action === 'preflight') { +$preflight_result = gsp_preflight_check(); +if ($preflight_result['success']) { +print_success('Preflight check passed.'); +} else { +print_failure('Preflight failed: ' . htmlspecialchars(implode(' | ', $preflight_result['errors']))); +} +} elseif ($action === 'apply_patches') { +$run = gsp_run_required_patches((string)gsp_get_current_version()); +if ($run['success']) { +print_success('Required patches applied successfully.'); +} else { +print_failure('Patch application failed: ' . htmlspecialchars($run['error'])); +} +} elseif ($action === 'fix_apache') { +$apache_fix = gsp_fix_apache_paths(true, true); +if ($apache_fix['success']) { +print_success('Apache paths fixed successfully. Updated files: ' . intval(count($apache_fix['changed_files'])) . '.'); +} else { +print_failure('Apache path fix failed: ' . htmlspecialchars($apache_fix['error'])); +} +} elseif ($action === 'backup_only') { +$result = gsp_create_full_backup('backup-only', 'manual', false); +if ($result['success']) { +print_success('Backup created: ' . htmlspecialchars($result['backup_dir']) . ''); +} else { +print_failure('Backup failed: ' . htmlspecialchars($result['error'])); +} +} elseif ($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', $restart_nonce); +if (!empty($result['restart_required'])) { +print_success('Updater files changed and were updated first. Restarting update with refreshed updater...'); +$auto_restart_payload = ['action' => 'update_release', 'nonce' => $result['restart_nonce'], 'version' => $version]; +} elseif ($result['success']) { +print_success('Panel updated to release ' . htmlspecialchars($version) . '. ' +. intval($result['files_copied']) . ' file(s) copied.'); +} else { +print_failure('Update failed: ' . htmlspecialchars($result['error'])); +} +} +} elseif ($action === 'update_stable') { +$result = gsp_do_update($repo_owner, $repo_name, $stable_branch, 'development', $restart_nonce); +if (!empty($result['restart_required'])) { +print_success('Updater files changed and were updated first. Restarting stable update...'); +$auto_restart_payload = ['action' => 'update_stable', 'nonce' => $result['restart_nonce']]; +} elseif ($result['success']) { +print_success('Panel updated to GitHub Stable (' . htmlspecialchars($stable_branch) . '). ' +. intval($result['files_copied']) . ' file(s) copied.'); +} else { +print_failure('Update failed: ' . htmlspecialchars($result['error'])); +} +} elseif ($action === 'update_unstable') { +$result = gsp_do_update($repo_owner, $repo_name, $unstable_branch, 'cutting-edge', $restart_nonce); +if (!empty($result['restart_required'])) { +print_success('Updater files changed and were updated first. Restarting unstable update...'); +$auto_restart_payload = ['action' => 'update_unstable', 'nonce' => $result['restart_nonce']]; +} elseif ($result['success']) { +print_success('Panel updated to GitHub Unstable (' . htmlspecialchars($unstable_branch) . '). ' +. intval($result['files_copied']) . ' file(s) copied.'); +} else { +print_failure('Update failed: ' . htmlspecialchars($result['error'])); +} +} elseif ($action === 'revert') { +$backup_ts = isset($_POST['gsp_revert_backup']) ? trim($_POST['gsp_revert_backup']) : ''; +$restore_apache = !empty($_POST['gsp_restore_apache']); +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, $restore_apache); +if ($result['success']) { +print_success('Reverted to backup ' . htmlspecialchars($backup_ts) . '.'); +} else { +print_failure('Revert failed: ' . htmlspecialchars($result['error'])); +} +} +} - if ($action === 'backup_only') { - $started_at = date('Y-m-d H:i:s'); - $result = gsp_create_full_backup('backup-only', 'manual'); - $finished_at = date('Y-m-d H:i:s'); - if ($result['success']) { - $bk_dir = htmlspecialchars($result['backup_dir']); - print_success('Backup created successfully at ' . $bk_dir . '.'); - gsp_update_log("Admin {$user_label} created manual backup at {$result['backup_dir']}"); - gsp_log_update_to_db( - 'backup-only', null, 'success', - 'Manual backup by ' . $_SESSION['users_login'], - $result['backup_dir'], - $result['backup_dir'] . '/database.sql', - $result['backup_dir'] . '/panel-files.tar.gz', - $started_at, $finished_at - ); - } else { - print_failure('Backup failed: ' . htmlspecialchars($result['error'])); - gsp_update_log("Admin {$user_label} manual backup FAILED: {$result['error']}"); - gsp_log_update_to_db( - 'backup-only', null, 'failed', - 'Manual backup failed: ' . $result['error'], - null, null, null, $started_at, $finished_at - ); - } +$finished_at = date('Y-m-d H:i:s'); +gsp_log_update_to_db('panel-update', null, 'info', 'Admin action: ' . $action, null, null, null, $started_at, $finished_at); +} +$_SESSION['gsp_update_csrf'] = gsp_random_token(); +$csrf_token = $_SESSION['gsp_update_csrf']; +} - } elseif ($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 { - $started_at = date('Y-m-d H:i:s'); - $result = gsp_do_update($repo_owner, $repo_name, $version, 'release'); - $finished_at = date('Y-m-d H:i:s'); - if ($result['success']) { - print_success( - 'Panel updated to release ' . htmlspecialchars($version) . '. ' - . intval($result['files_copied']) . ' file(s) updated. Source: GitHub Releases' - ); - gsp_update_log("Admin {$user_label} updated panel to release {$version}"); - gsp_log_update_to_db( - 'release', $version, 'success', - 'Updated to release ' . $version . ' by ' . $_SESSION['users_login'], - $result['backup_dir'] ?? null, - isset($result['backup_dir']) ? $result['backup_dir'] . '/database.sql' : null, - isset($result['backup_dir']) ? $result['backup_dir'] . '/panel-files.tar.gz': null, - $started_at, $finished_at - ); - } else { - print_failure('Update failed: ' . htmlspecialchars($result['error'])); - gsp_update_log("Admin {$user_label} update to release {$version} FAILED: {$result['error']}"); - gsp_log_update_to_db( - 'release', $version, 'failed', - 'Update to release ' . $version . ' failed: ' . $result['error'], - null, null, null, $started_at, $finished_at - ); - } - } +$current_version = gsp_get_current_version(); +$current_branch = gsp_get_current_branch(); +$git_commit = gsp_get_git_commit(); +$vinfo = gsp_read_version_json(); +$releases = gsp_fetch_github_releases($repo_owner, $repo_name); +$latest_release = (is_array($releases) && !empty($releases)) ? htmlspecialchars($releases[0]['tag_name']) : 'N/A'; +$backups = gsp_get_available_backups(); +$patch_overview = gsp_get_patch_overview(); +if ($apache_scan_result === null) { +$apache_scan_result = gsp_scan_apache_configs(); +} +if ($preflight_result === null) { +$preflight_result = gsp_preflight_check(); +} - } elseif ($action === 'update_stable') { - $started_at = date('Y-m-d H:i:s'); - $result = gsp_do_update($repo_owner, $repo_name, $stable_branch, 'development'); - $finished_at = date('Y-m-d H:i:s'); - if ($result['success']) { - print_success( - 'Panel updated to GitHub Stable (' . htmlspecialchars($stable_branch) . '). ' - . intval($result['files_copied']) . ' file(s) updated. Source: ' - . htmlspecialchars($stable_branch) . '' - ); - gsp_update_log("Admin {$user_label} updated panel to GitHub Stable branch {$stable_branch}"); - gsp_log_update_to_db( - 'development', $stable_branch, 'success', - 'Updated to GitHub Stable branch ' . $stable_branch . ' by ' . $_SESSION['users_login'], - $result['backup_dir'] ?? null, - isset($result['backup_dir']) ? $result['backup_dir'] . '/database.sql' : null, - isset($result['backup_dir']) ? $result['backup_dir'] . '/panel-files.tar.gz': null, - $started_at, $finished_at - ); - } else { - print_failure('Update failed: ' . htmlspecialchars($result['error'])); - gsp_update_log("Admin {$user_label} update to GitHub Stable branch {$stable_branch} FAILED: {$result['error']}"); - gsp_log_update_to_db( - 'development', $stable_branch, 'failed', - 'Update to GitHub Stable branch ' . $stable_branch . ' failed: ' . $result['error'], - null, null, null, $started_at, $finished_at - ); - } +echo "

Panel Updates

\n"; +echo "
\n"; +echo "

Detected Layout

\n"; +echo "\n"; +echo "\n"; +echo "\n"; +echo "\n"; +echo "\n"; +echo "\n"; +echo "
Expected Root:" . htmlspecialchars(GSP_EXPECTED_ROOT) . "
Detected GSP Root:" . htmlspecialchars(GSP_ROOT_DIR) . "
Panel Path:" . htmlspecialchars(GSP_PANEL_DIR) . "
Website Path:" . htmlspecialchars(GSP_WEBSITE_DIR) . "
Update Trace Log:" . htmlspecialchars(GSP_UPDATE_LOG) . "

\n"; - } elseif ($action === 'update_unstable') { - $started_at = date('Y-m-d H:i:s'); - $result = gsp_do_update($repo_owner, $repo_name, $unstable_branch, 'cutting-edge'); - $finished_at = date('Y-m-d H:i:s'); - if ($result['success']) { - print_success( - 'Panel updated to GitHub Unstable (' . htmlspecialchars($unstable_branch) . '). ' - . intval($result['files_copied']) . ' file(s) updated. Source: ' - . htmlspecialchars($unstable_branch) . '' - ); - gsp_update_log("Admin {$user_label} updated panel to GitHub Unstable branch {$unstable_branch}"); - gsp_log_update_to_db( - 'cutting-edge', $unstable_branch, 'success', - 'Updated to GitHub Unstable branch ' . $unstable_branch . ' by ' . $_SESSION['users_login'], - $result['backup_dir'] ?? null, - isset($result['backup_dir']) ? $result['backup_dir'] . '/database.sql' : null, - isset($result['backup_dir']) ? $result['backup_dir'] . '/panel-files.tar.gz': null, - $started_at, $finished_at - ); - } else { - print_failure('Update failed: ' . htmlspecialchars($result['error'])); - gsp_update_log("Admin {$user_label} update to GitHub Unstable branch {$unstable_branch} FAILED: {$result['error']}"); - gsp_log_update_to_db( - 'cutting-edge', $unstable_branch, 'failed', - 'Update to GitHub Unstable branch ' . $unstable_branch . ' failed: ' . $result['error'], - null, null, null, $started_at, $finished_at - ); - } +echo "

Current Installation

\n"; +echo "\n"; +if ($vinfo) { +echo "\n"; +echo "\n"; +echo "\n"; +echo "\n"; +} else { +echo "\n"; +echo "\n"; +} +if ($git_commit) { +echo "\n"; +} +echo "\n"; +echo "\n"; +echo "\n"; +echo "\n"; +echo "
Installed Type:" . htmlspecialchars($vinfo['installed_type']) . "
Installed Source:" . htmlspecialchars($vinfo['installed_source']) . "
Installed Version:" . htmlspecialchars($vinfo['installed_version']) . "
Installed At:" . htmlspecialchars($vinfo['installed_at']) . "
Installed Version:" . htmlspecialchars($current_version) . "
Current Branch:" . htmlspecialchars($current_branch) . "
Git Commit:" . htmlspecialchars(substr($git_commit, 0, 12)) . "
Latest Release:" . $latest_release . "
Repository:" . htmlspecialchars($repo_owner . '/' . $repo_name) . "
Backup Directory:" . htmlspecialchars(GSP_BACKUP_BASE) . "
Backups Stored:" . intval(count($backups)) . " (retention: 5)

\n"; - } 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 { - $started_at = date('Y-m-d H:i:s'); - $result = gsp_do_revert($backup_ts); - $finished_at = date('Y-m-d H:i:s'); - 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}"); - gsp_log_update_to_db( - 'revert', $backup_ts, 'success', - 'Reverted to backup ' . $backup_ts . ' by ' . $_SESSION['users_login'], - GSP_BACKUP_BASE . '/' . $backup_ts, - GSP_BACKUP_BASE . '/' . $backup_ts . '/database.sql', - GSP_BACKUP_BASE . '/' . $backup_ts . '/panel-files.tar.gz', - $started_at, $finished_at - ); - } else { - print_failure('Revert failed: ' . htmlspecialchars($result['error'])); - gsp_update_log("Admin {$user_label} revert to backup {$backup_ts} FAILED: {$result['error']}"); - gsp_log_update_to_db( - 'revert', $backup_ts, 'failed', - 'Revert to backup ' . $backup_ts . ' failed: ' . $result['error'], - null, null, null, $started_at, $finished_at - ); - } - } - } - } +echo "

Updater Preflight & Patches

\n"; +echo "\n"; +echo "\n"; +echo "\n"; +echo "\n"; +echo "
Preflight Status:" . ($preflight_result['success'] ? 'PASS' : 'FAIL') . "
Pending Patches:" . intval(count($patch_overview['pending'])) . "
Patch Directory:" . htmlspecialchars(GSP_PATCH_DIR) . "
\n"; +if (!empty($preflight_result['warnings'])) { +echo "

Preflight warnings:
" . implode('
', array_map('htmlspecialchars', $preflight_result['warnings'])) . "

\n"; +} +if (!empty($preflight_result['errors'])) { +echo "

Preflight errors:
" . implode('
', array_map('htmlspecialchars', $preflight_result['errors'])) . "

\n"; +} - // Rotate CSRF token after every submission - $_SESSION['gsp_update_csrf'] = gsp_random_token(); - $csrf_token = $_SESSION['gsp_update_csrf']; - } - // ---- End POST handling -------------------------------------------------- +echo "
\n"; +echo "\n"; +echo "\n"; +echo "\n"; +echo "
\n"; +echo "
\n"; +echo "\n"; +echo "\n"; +echo "\n"; +echo "
\n"; - // Gather display data - $current_version = gsp_get_current_version(); - $current_branch = gsp_get_current_branch(); - $git_commit = gsp_get_git_commit(); - $vinfo = gsp_read_version_json(); - $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(); +echo "

Apache Configuration Status

\n"; +echo "\n"; +echo "\n"; +echo "\n"; +echo "\n"; +echo "\n"; +echo "\n"; +echo "
Config Directory:" . htmlspecialchars($apache_scan_result['base']) . "
Configs Found:" . intval(count($apache_scan_result['files'])) . "
Stale Path Hits:" . intval(count($apache_scan_result['issues'])) . "
Recommended Panel Path:" . htmlspecialchars(GSP_PANEL_DIR) . "
Recommended Website Path:" . htmlspecialchars(GSP_WEBSITE_DIR) . "
\n"; +if (!empty($apache_scan_result['issues'])) { +echo "

Apache path issues:
" . implode('
', array_map('htmlspecialchars', array_unique($apache_scan_result['issues']))) . "

\n"; +} +echo "
\n"; +echo "\n"; +echo "\n"; +echo "\n"; +echo "
\n"; - // ---- Render UI ---------------------------------------------------------- - echo "

Panel Updates

\n"; - echo "
\n"; +echo "

Backup

\n"; +echo "
\n"; +echo "\n"; +echo "\n"; +echo "\n"; +echo "
\n"; - // Current status table - echo "

Current Installation

\n"; - echo "\n"; - if ($vinfo) { - echo "\n"; - echo "\n"; - echo "\n"; - if (!empty($vinfo['installed_commit'])) { - echo "\n"; - } - echo "\n"; - } else { - echo "\n"; - echo "\n"; - if ($git_commit) { - echo "\n"; - } - } - echo "\n"; - echo "\n"; - // Backup status rows - echo "\n"; - if (!empty($backups)) { - $last_bk = $backups[0]; - $last_ts = htmlspecialchars($last_bk['ts']); - $last_status = !empty($last_bk['meta']['backup_status']) - ? htmlspecialchars($last_bk['meta']['backup_status']) - : 'unknown'; - echo "\n"; - } else { - echo "\n"; - } - echo "
Installed Type:" . htmlspecialchars($vinfo['installed_type'] ?? 'N/A') . "
Installed Source:" . htmlspecialchars($vinfo['installed_source'] ?? 'N/A') . "
Installed Version:" . htmlspecialchars($vinfo['installed_version'] ?? 'N/A') . "
Installed Commit:" - . htmlspecialchars(substr($vinfo['installed_commit'], 0, 12)) . "
Installed / Updated At:" . htmlspecialchars($vinfo['installed_at'] ?? 'N/A') . "
Installed Version:" . htmlspecialchars($current_version) . "
Current Branch / Type:" . htmlspecialchars($current_branch) . "
Git Commit:" - . htmlspecialchars(substr($git_commit, 0, 12)) . "
Latest Release on GitHub:" . $latest_release . "
Repository:" - . htmlspecialchars("{$repo_owner}/{$repo_name}") . "
Backup Directory:" - . htmlspecialchars(GSP_BACKUP_BASE) . "
Last Backup:" - . $last_ts . " — status: " . $last_status . "
Last Backup:None yet
\n
\n"; +echo "

Update Panel

\n"; +if (is_array($releases) && !empty($releases)) { +echo "
\n"; +echo "\n"; +echo "\n"; +echo "\n"; +echo " \n"; +echo "
\n"; +} +echo "
\n"; +echo "\n"; +echo "\n"; +echo "\n"; +echo "
\n"; +echo "
\n"; +echo "\n"; +echo "\n"; +echo "\n"; +echo "
\n"; - // ---- Backup Only -------------------------------------------------------- - echo "

Create Backup

\n"; - echo "
\n"; - echo "\n"; - echo "\n"; - echo "\n"; - echo "Saves to: " - . htmlspecialchars(GSP_BACKUP_BASE) . "\n"; - echo "
\n"; +if (!empty($backups)) { +echo "

Rollback

\n"; +echo "
\n"; +echo "\n"; +echo "\n"; +echo "\n"; +echo " \n"; +echo " \n"; +echo "
\n"; +} - echo "
\n"; +echo "
\n"; - // ---- Numbered Releases -------------------------------------------------- - echo "

Numbered Releases

\n"; - if (is_array($releases) && !empty($releases)) { - echo "
\n"; - echo "\n"; - echo "\n"; - echo "\n"; - echo " \n"; - echo "
\n"; - } else { - echo "

No releases available (GitHub API unreachable or no releases published).

\n"; - } - - echo "
\n"; - - // ---- GitHub Stable ------------------------------------------------------- - echo "

GitHub Stable

\n"; - echo "

GitHub Stable should always match the latest official numbered release.

\n"; - echo "
\n"; - echo "\n"; - echo "\n"; - echo ""; - echo " Branch: " - . htmlspecialchars($stable_branch) . "\n"; - echo "
\n"; - - echo "
\n"; - - // ---- GitHub Unstable ----------------------------------------------------- - echo "

GitHub Unstable

\n"; - echo "

GitHub Unstable represents the latest development branch and may be unstable.

\n"; - echo "

" - . "⚠ Cutting-edge updates may include unfinished changes. Use stable releases for production.


\n"; - echo "
\n"; - echo "\n"; - echo "\n"; - echo ""; - echo " Branch: " - . htmlspecialchars($unstable_branch) . "\n"; - echo "
\n"; - - // ---- Revert Section ----------------------------------------------------- - if (!empty($backups)) { - echo "
\n

Revert Panel Update

\n"; - echo "

Available backups in " . htmlspecialchars(GSP_BACKUP_BASE) . ":

\n"; - echo "
\n"; - echo "\n"; - echo "\n"; - echo "\n"; - echo " \n"; - echo "
\n"; - } - - echo "
\n"; +if ($auto_restart_payload) { +$version = isset($auto_restart_payload['version']) ? $auto_restart_payload['version'] : ''; +gsp_render_restart_form($auto_restart_payload['action'], $csrf_token, $auto_restart_payload['nonce'], $version); +} } ?> diff --git a/Panel/modules/update/module.php b/Panel/modules/update/module.php index c303fa51..1e889729 100644 --- a/Panel/modules/update/module.php +++ b/Panel/modules/update/module.php @@ -25,7 +25,7 @@ // Module general information $module_title = "Update"; $module_version = "1.0"; -$db_version = 3; // avoid 'duplicate table' error message. +$db_version = 4; $module_required = TRUE; $module_menus = array( array( 'subpage' => '', 'name'=>'Update', 'group'=>'admin' ) @@ -62,4 +62,14 @@ $install_queries[3] = array( PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;" ); +$install_queries[4] = array( +"CREATE TABLE IF NOT EXISTS `".OGP_DB_PREFIX."update_patches` ( + `patch_id` varchar(191) NOT NULL, + `status` varchar(32) NOT NULL, + `details` text DEFAULT NULL, + `updater_version` varchar(80) DEFAULT NULL, + `applied_at` datetime NOT NULL, + PRIMARY KEY (`patch_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;" +); ?> diff --git a/Panel/modules/update/patch_manager.php b/Panel/modules/update/patch_manager.php new file mode 100644 index 00000000..94368333 --- /dev/null +++ b/Panel/modules/update/patch_manager.php @@ -0,0 +1,183 @@ + date('Y-m-d H:i:s'), +'patches' => $patches, +]; +@file_put_contents($state_file, json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); +} + +function gsp_patch_load_definitions($patch_dir) +{ +$patches = []; +if (!is_dir($patch_dir)) { +return $patches; +} +$files = glob(rtrim($patch_dir, '/') . '/*.php') ?: []; +sort($files, SORT_NATURAL); +foreach ($files as $file) { +$def = include $file; +if (!is_array($def) || empty($def['id']) || empty($def['runner'])) { +continue; +} +$def['file'] = $file; +$patches[] = $def; +} +return $patches; +} + +function gsp_patch_get_applied_map($db, $state_file) +{ +$map = []; +if (isset($db) && is_object($db)) { +$rows = $db->resultQuery("SELECT patch_id, status, details, applied_at FROM `OGP_DB_PREFIXupdate_patches`"); +if (is_array($rows)) { +foreach ($rows as $row) { +$map[$row['patch_id']] = [ +'status' => $row['status'], +'details' => $row['details'], +'applied_at' => $row['applied_at'], +]; +} +} +} +foreach (gsp_patch_state_load_local($state_file) as $id => $row) { +if (!isset($map[$id])) { +$map[$id] = $row; +} +} +return $map; +} + +function gsp_patch_record($db, $state_file, $patch_id, $status, $details, $updater_version) +{ +$patch_id = (string)$patch_id; +$status = (string)$status; +$details = (string)$details; +$updater_version = (string)$updater_version; +$applied_at = date('Y-m-d H:i:s'); + +if (isset($db) && is_object($db)) { +$pid = $db->real_escape_string($patch_id); +$st = $db->real_escape_string($status); +$dt = $db->real_escape_string($details); +$uv = $db->real_escape_string($updater_version); +$at = $db->real_escape_string($applied_at); +$db->query( +"INSERT INTO `OGP_DB_PREFIXupdate_patches` (patch_id, status, details, updater_version, applied_at) " +. "VALUES ('{$pid}','{$st}','{$dt}','{$uv}','{$at}') " +. "ON DUPLICATE KEY UPDATE status=VALUES(status), details=VALUES(details), updater_version=VALUES(updater_version), applied_at=VALUES(applied_at)" +); +} + +$state = gsp_patch_state_load_local($state_file); +$state[$patch_id] = [ +'status' => $status, +'details' => $details, +'applied_at' => $applied_at, +'updater_version' => $updater_version, +]; +gsp_patch_state_save_local($state_file, $state); +} + +function gsp_patch_run_all($db, $patch_dir, callable $logger, $updater_version) +{ +$state_file = gsp_patch_state_fallback_file(); +$definitions = gsp_patch_load_definitions($patch_dir); +$applied = gsp_patch_get_applied_map($db, $state_file); +$result = [ +'success' => true, +'patches_available' => count($definitions), +'applied' => [], +'skipped' => [], +'failed_patch' => null, +'error' => null, +]; + +foreach ($definitions as $patch) { +$id = (string)$patch['id']; +$title = !empty($patch['title']) ? (string)$patch['title'] : $id; +if (isset($applied[$id]) && $applied[$id]['status'] === 'applied') { +$result['skipped'][] = $id; +$logger("Patch {$id} ({$title}) already applied; skipping."); +continue; +} + +$runner = $patch['runner']; +if (!is_callable($runner)) { +$msg = "Patch {$id} runner is not callable."; +gsp_patch_record($db, $state_file, $id, 'failed', $msg, $updater_version); +$result['success'] = false; +$result['failed_patch'] = $id; +$result['error'] = $msg; +$logger($msg); +break; +} + +$logger("Running patch {$id} ({$title})."); +$run = call_user_func($runner, [ +'root_dir' => defined('GSP_ROOT_DIR') ? GSP_ROOT_DIR : null, +'panel_dir' => defined('GSP_PANEL_DIR') ? GSP_PANEL_DIR : null, +'website_dir' => defined('GSP_WEBSITE_DIR') ? GSP_WEBSITE_DIR : null, +]); +if (!is_array($run)) { +$run = ['success' => false, 'details' => 'Patch runner returned invalid result.']; +} +$ok = !empty($run['success']); +$details = !empty($run['details']) ? (string)$run['details'] : ($ok ? 'Applied.' : 'Failed.'); +gsp_patch_record($db, $state_file, $id, $ok ? 'applied' : 'failed', $details, $updater_version); + +if ($ok) { +$result['applied'][] = $id; +$logger("Patch {$id} applied: {$details}"); +} else { +$result['success'] = false; +$result['failed_patch'] = $id; +$result['error'] = $details; +$logger("Patch {$id} failed: {$details}"); +break; +} +} + +return $result; +} diff --git a/Panel/modules/update/patches/001_layout_bootstrap.php b/Panel/modules/update/patches/001_layout_bootstrap.php new file mode 100644 index 00000000..9aa7a56c --- /dev/null +++ b/Panel/modules/update/patches/001_layout_bootstrap.php @@ -0,0 +1,36 @@ + false, 'details' => 'Failed to create directory: ' . $dir]; +} +$created[] = $dir; +} +} +$details = empty($created) +? 'Layout already in expected state.' +: 'Created directories: ' . implode(', ', $created); +return ['success' => true, 'details' => $details]; +} +} + +return [ +'id' => '001_layout_bootstrap', +'title' => 'Ensure baseline GSP root directories exist', +'runner' => 'gsp_patch_001_layout_bootstrap', +];