From 5ead40a7615c1532774efd53e9a9a5b512e1b5d5 Mon Sep 17 00:00:00 2001 From: Frank Harris Date: Sun, 7 Jun 2026 15:37:43 -0500 Subject: [PATCH] fixed updated --- Panel/modules/administration/panel_update.php | 594 +++++++++++------- .../update/tests/update_config_smoke.php | 3 + Panel/modules/update/update.php | 13 +- docs/modules/PANEL_UPDATE.md | 200 ++++++ docs/modules/SCHEDULER.md | 3 + docs/modules/UPDATE.md | 4 + logs/update_trace.log | 2 + 7 files changed, 606 insertions(+), 213 deletions(-) create mode 100644 docs/modules/PANEL_UPDATE.md create mode 100644 logs/update_trace.log diff --git a/Panel/modules/administration/panel_update.php b/Panel/modules/administration/panel_update.php index 7187edca..199aeadf 100644 --- a/Panel/modules/administration/panel_update.php +++ b/Panel/modules/administration/panel_update.php @@ -37,6 +37,7 @@ defined('GSP_DEFAULT_PANEL_SOURCE') || define('GSP_DEFAULT_PANEL_SOURCE', 'Panel defined('GSP_DEFAULT_LINUX_AGENT_SOURCE') || define('GSP_DEFAULT_LINUX_AGENT_SOURCE', 'Agent_Linux'); defined('GSP_DEFAULT_WINDOWS_AGENT_SOURCE') || define('GSP_DEFAULT_WINDOWS_AGENT_SOURCE', 'Agent-Windows'); defined('GSP_DEFAULT_WEBSITE_SOURCE') || define('GSP_DEFAULT_WEBSITE_SOURCE', 'Website'); +defined('GSP_DEFAULT_BACKUP_RETENTION') || define('GSP_DEFAULT_BACKUP_RETENTION', 5); $gspPatchManager = GSP_PANEL_DIR . '/modules/update/patch_manager.php'; if (file_exists($gspPatchManager)) { @@ -53,6 +54,79 @@ $line = '[' . date('Y-m-d H:i:s') . '] ' . $message . PHP_EOL; @file_put_contents(GSP_UPDATE_LOG, $line, FILE_APPEND | LOCK_EX); } +function gsp_path_debug_summary($path) +{ +$path = rtrim((string)$path, '/'); +if ($path === '') { +return '[empty path]'; +} +$parent = dirname($path); +$parts = array( +'path=' . $path, +'exists=' . (file_exists($path) ? 'yes' : 'no'), +'is_dir=' . (is_dir($path) ? 'yes' : 'no'), +'writable=' . (is_writable($path) ? 'yes' : 'no'), +'parent=' . $parent, +'parent_exists=' . (file_exists($parent) ? 'yes' : 'no'), +'parent_writable=' . (is_writable($parent) ? 'yes' : 'no'), +); +return implode(', ', $parts); +} + +function gsp_ensure_directory($path, $label = 'directory') +{ +$path = rtrim((string)$path, '/'); +if ($path === '' || strpos($path, "\0") !== false) { +$message = 'Invalid ' . $label . ' path.'; +gsp_update_log($message . ' ' . gsp_path_debug_summary($path)); +return array('success' => false, 'error' => $message); +} +if (is_dir($path)) { +if (!is_writable($path)) { +$message = ucfirst($label) . ' exists but is not writable: ' . $path; +gsp_update_log($message . ' [' . gsp_path_debug_summary($path) . ']'); +return array('success' => false, 'error' => $message); +} +return array('success' => true, 'path' => $path, 'created' => false); +} +if (!@mkdir($path, 0755, true) && !is_dir($path)) { +$last = error_get_last(); +$detail = isset($last['message']) ? $last['message'] : 'mkdir returned false'; +$message = 'Cannot create ' . $label . ': ' . $path; +gsp_update_log($message . ' [' . $detail . '] [' . gsp_path_debug_summary($path) . ']'); +return array('success' => false, 'error' => $message . ' (' . $detail . ')'); +} +if (!is_writable($path)) { +$message = ucfirst($label) . ' was created but is not writable: ' . $path; +gsp_update_log($message . ' [' . gsp_path_debug_summary($path) . ']'); +return array('success' => false, 'error' => $message); +} +return array('success' => true, 'path' => $path, 'created' => true); +} + +function gsp_get_backup_base(array $update_cfg = null) +{ +$update_cfg = $update_cfg ?: gsp_update_settings(); +$base = !empty($update_cfg['backup_path']) ? rtrim((string)$update_cfg['backup_path'], '/') : GSP_BACKUP_BASE; +if ($base === '' || strpos($base, "\0") !== false || strpos($base, '..') !== false || strpos($base, '/') !== 0) { +$base = GSP_BACKUP_BASE; +} +return $base; +} + +function gsp_get_backup_retention(array $update_cfg = null) +{ +$update_cfg = $update_cfg ?: gsp_update_settings(); +$retention = isset($update_cfg['backup_retention']) ? (int)$update_cfg['backup_retention'] : GSP_DEFAULT_BACKUP_RETENTION; +if ($retention < 1) { +$retention = GSP_DEFAULT_BACKUP_RETENTION; +} +if ($retention > 200) { +$retention = 200; +} +return $retention; +} + 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; @@ -167,9 +241,10 @@ return [ 'linux_agent_source_path' => !empty($settings['gsp_update_linux_agent_source_path']) ? trim((string)$settings['gsp_update_linux_agent_source_path'], '/') : GSP_DEFAULT_LINUX_AGENT_SOURCE, 'windows_agent_source_path' => !empty($settings['gsp_update_windows_agent_source_path']) ? trim((string)$settings['gsp_update_windows_agent_source_path'], '/') : GSP_DEFAULT_WINDOWS_AGENT_SOURCE, 'website_source_path' => !empty($settings['gsp_update_website_source_path']) ? trim((string)$settings['gsp_update_website_source_path'], '/') : GSP_DEFAULT_WEBSITE_SOURCE, -'git_path' => !empty($settings['gsp_update_git_path']) ? trim((string)$settings['gsp_update_git_path']) : 'git', -'backup_path' => !empty($settings['gsp_update_backup_path']) ? rtrim((string)$settings['gsp_update_backup_path'], '/') : GSP_BACKUP_BASE, -'panel_post_update_command' => !empty($settings['gsp_update_panel_post_update_command']) ? (string)$settings['gsp_update_panel_post_update_command'] : '', + 'git_path' => !empty($settings['gsp_update_git_path']) ? trim((string)$settings['gsp_update_git_path']) : 'git', + 'backup_path' => !empty($settings['gsp_update_backup_path']) ? rtrim((string)$settings['gsp_update_backup_path'], '/') : GSP_BACKUP_BASE, + 'backup_retention' => !empty($settings['gsp_update_backup_retention']) ? (string)$settings['gsp_update_backup_retention'] : (string)GSP_DEFAULT_BACKUP_RETENTION, + 'panel_post_update_command' => !empty($settings['gsp_update_panel_post_update_command']) ? (string)$settings['gsp_update_panel_post_update_command'] : '', 'website_post_update_command' => !empty($settings['gsp_update_website_post_update_command']) ? (string)$settings['gsp_update_website_post_update_command'] : '', 'linux_agent_post_update_command' => !empty($settings['gsp_update_linux_agent_post_update_command']) ? (string)$settings['gsp_update_linux_agent_post_update_command'] : '', 'windows_agent_post_update_command' => !empty($settings['gsp_update_windows_agent_post_update_command']) ? (string)$settings['gsp_update_windows_agent_post_update_command'] : '', @@ -200,11 +275,14 @@ if (trim((string)$cfg[$key]) === '' || strpos((string)$cfg[$key], "\0") !== fals $errors[] = ucfirst(str_replace('_', ' ', $key)) . ' is invalid.'; } } -foreach (['website_path', 'backup_path'] as $key) { -if (isset($cfg[$key]) && (trim((string)$cfg[$key]) === '' || strpos((string)$cfg[$key], "\0") !== false || strpos((string)$cfg[$key], '..') !== false || strpos((string)$cfg[$key], '/') !== 0)) { -$errors[] = ucfirst(str_replace('_', ' ', $key)) . ' must be a safe absolute path.'; -} -} + foreach (['website_path', 'backup_path'] as $key) { + if (isset($cfg[$key]) && (trim((string)$cfg[$key]) === '' || strpos((string)$cfg[$key], "\0") !== false || strpos((string)$cfg[$key], '..') !== false || strpos((string)$cfg[$key], '/') !== 0)) { + $errors[] = ucfirst(str_replace('_', ' ', $key)) . ' must be a safe absolute path.'; + } + } + if (isset($cfg['backup_retention']) && (!preg_match('/^\d+$/', (string)$cfg['backup_retention']) || (int)$cfg['backup_retention'] < 1 || (int)$cfg['backup_retention'] > 200)) { + $errors[] = 'Backup retention must be a whole number between 1 and 200.'; + } foreach (['panel_source_path', 'linux_agent_source_path', 'windows_agent_source_path', 'website_source_path'] as $key) { $value = isset($cfg[$key]) ? trim((string)$cfg[$key], '/') : ''; if ($value === '' || strpos($value, "\0") !== false || strpos($value, '..') !== false || !preg_match('/^[A-Za-z0-9._\/-]+$/', $value)) { @@ -298,10 +376,11 @@ $warnings = []; $update_cfg = $update_cfg ?: gsp_update_settings(); $cwd = getcwd(); $cwd_real = $cwd ? (realpath($cwd) ?: $cwd) : ''; -$root_path = rtrim((string)$update_cfg['repo_root'], '/'); -$panel_path = rtrim((string)$update_cfg['panel_path'], '/'); -$website_path = !empty($update_cfg['website_path']) ? rtrim((string)$update_cfg['website_path'], '/') : ($root_path . '/Website'); -$root_real = realpath($root_path) ?: $root_path; + $root_path = rtrim((string)$update_cfg['repo_root'], '/'); + $panel_path = rtrim((string)$update_cfg['panel_path'], '/'); + $website_path = !empty($update_cfg['website_path']) ? rtrim((string)$update_cfg['website_path'], '/') : ($root_path . '/Website'); + $backup_base = gsp_get_backup_base($update_cfg); + $root_real = realpath($root_path) ?: $root_path; $panel_real = realpath($panel_path) ?: $panel_path; $website_real = realpath($website_path) ?: $website_path; $layout = [ @@ -316,7 +395,7 @@ $layout = [ 'panel_dir_real' => $panel_real, 'website_dir' => $website_path, 'website_dir_real' => $website_real, -'backup_dir' => GSP_BACKUP_BASE, + 'backup_dir' => $backup_base, 'config_file' => $panel_path . '/includes/config.inc.php', 'destination_panel' => $panel_path, 'destination_website' => $website_path, @@ -339,13 +418,12 @@ $warnings[] = 'Website directory is missing. Panel updates can still continue, b 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_ready = gsp_ensure_directory($backup_base, 'backup directory'); + if (!$backup_ready['success']) { + $errors[] = $backup_ready['error']; + } -foreach ([$root_path, $panel_path, GSP_BACKUP_BASE] as $path) { + foreach ([$root_path, $panel_path, $backup_base] as $path) { if (!is_writable($path)) { $errors[] = 'Path is not writable: ' . $path; } @@ -518,33 +596,94 @@ $count++; return ['success' => true, 'path' => $dest, 'count' => $count]; } -function gsp_prune_old_backups($max_backups = 5) +function gsp_get_managed_backup_entries($backup_base = null) { -$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']); -} + $backup_base = $backup_base ? rtrim((string)$backup_base, '/') : gsp_get_backup_base(); + $entries = array(); + if (!is_dir($backup_base)) { + return $entries; + } + foreach ((array)scandir($backup_base) as $entry) { + if ($entry === '.' || $entry === '..') { + continue; + } + $dir = $backup_base . '/' . $entry; + if (!is_dir($dir)) { + continue; + } + $type = null; + $stamp = null; + if (preg_match('/^(\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2})$/', $entry, $m)) { + $type = 'full'; + $stamp = $m[1]; + } elseif (preg_match('/^component_[A-Za-z0-9._-]+_(\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2})$/', $entry, $m)) { + $type = 'component'; + $stamp = $m[1]; + } else { + continue; + } + $meta_file = $dir . '/backup.json'; + $meta = array(); + if (is_file($meta_file)) { + $meta = json_decode((string)@file_get_contents($meta_file), true) ?: array(); + } + $sort_time = strtotime(str_replace('_', ' ', $stamp)); + if ($sort_time === false) { + $sort_time = @filemtime($dir) ?: 0; + } + $entries[] = array( + 'name' => $entry, + 'path' => $dir, + 'type' => $type, + 'ts' => $stamp, + 'sort_time' => $sort_time, + 'meta' => $meta, + ); + } + usort($entries, function ($a, $b) { + if ($a['sort_time'] === $b['sort_time']) { + return strcmp($b['name'], $a['name']); + } + return ($a['sort_time'] < $b['sort_time']) ? 1 : -1; + }); + return $entries; } -function gsp_create_full_backup($update_target_type, $update_target_version, $include_apache = false) +function gsp_prune_old_backups($max_backups = 5, $backup_base = null) { -$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]; + $entries = gsp_get_managed_backup_entries($backup_base); + if (count($entries) <= $max_backups) { + return array('success' => true, 'deleted' => array()); + } + $to_delete = array_slice($entries, $max_backups); + $deleted = array(); + foreach ($to_delete as $entry) { + gsp_rmdir_recursive($entry['path']); + $deleted[] = $entry['name']; + gsp_update_log('Pruned old backup: ' . $entry['path']); + } + return array('success' => true, 'deleted' => $deleted); } -$meta = [ -'backup_timestamp' => $ts, -'gsp_root' => GSP_ROOT_DIR, +function gsp_create_full_backup($update_target_type, $update_target_version, $include_apache = false, array $update_cfg = null) +{ + $ts = date('Y-m-d_H-i-s'); + $backup_base = gsp_get_backup_base($update_cfg); + $retention = gsp_get_backup_retention($update_cfg); + $backup_base_ready = gsp_ensure_directory($backup_base, 'backup base directory'); + if (!$backup_base_ready['success']) { + return array('success' => false, 'error' => $backup_base_ready['error']); + } + $backup_dir = $backup_base . '/' . $ts; + $backup_dir_ready = gsp_ensure_directory($backup_dir, 'backup directory'); + if (!$backup_dir_ready['success']) { + return array('success' => false, 'error' => $backup_dir_ready['error']); + } + + $meta = [ + 'backup_timestamp' => $ts, + 'backup_base' => $backup_base, + 'gsp_root' => GSP_ROOT_DIR, 'panel_root' => GSP_PANEL_DIR, 'website_root' => GSP_WEBSITE_DIR, 'update_target_type' => $update_target_type, @@ -593,17 +732,21 @@ $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); + @file_put_contents($backup_dir . '/backup.json', json_encode($meta, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + $prune = gsp_prune_old_backups($retention, $backup_base); + if (!empty($prune['deleted'])) { + gsp_update_log('Backup retention pruned ' . count($prune['deleted']) . ' entries from ' . $backup_base . ': ' . implode(', ', $prune['deleted'])); + } + gsp_update_log('Backup created: ' . $backup_dir); -return [ -'success' => true, -'backup_dir' => $backup_dir, -'backup_ts' => $ts, -]; + return [ + 'success' => true, + 'backup_base' => $backup_base, + 'backup_dir' => $backup_dir, + 'backup_ts' => $ts, + ]; } function gsp_download_zip($repo_owner, $repo_name, $ref, $temp_dir) @@ -1047,13 +1190,63 @@ $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, -]; + 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($layout); + gsp_rmdir_recursive($temp_dir); + if (!$sync['success']) { + return $sync; + } + $sync_validation = gsp_validate_layout_sync_result($layout, $sync); + if (!$sync_validation['success']) { + return ['success' => false, 'error' => 'Deployed file validation failed: ' . implode(' | ', $sync_validation['errors'])]; + } + gsp_update_log('Layout sync complete: copied=' . $sync['files_copied'] . ', skipped=' . count($sync['skipped'])); + gsp_update_log('Layout sync totals: Panel=' . intval($sync['panel_files_copied']) . ', Website=' . intval($sync['website_files_copied'])); + if (!empty($sync['skipped'])) { + gsp_update_log('Preserved paths: ' . implode(', ', array_slice($sync['skipped'], 0, 50))); + } + if (!empty($sync['copied_files'])) { + $copied_sample = array_slice((array)$sync['copied_files'], 0, 50); + gsp_update_log('Copied file sample: ' . implode(', ', $copied_sample)); + $addons_updates = array_values(array_filter((array)$sync['copied_files'], function ($rel) { + return strpos($rel, 'Panel/modules/addonsmanager/') === 0; + })); + if (!empty($addons_updates)) { + gsp_update_log('Addonsmanager files copied: ' . implode(', ', array_slice($addons_updates, 0, 50))); + } + } + return [ + 'success' => true, + 'files_copied' => $sync['files_copied'], + 'panel_files_copied' => $sync['panel_files_copied'], + 'website_files_copied' => $sync['website_files_copied'], + 'preserved' => $sync['skipped'], + 'copied_files' => $sync['copied_files'], + 'patches' => $patches['run'], + ]; } function gsp_checkout_update_source(array $update_cfg) @@ -1145,55 +1338,6 @@ return [ 'patches' => $patches['run'], ]; } -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($layout); -gsp_rmdir_recursive($temp_dir); -if (!$sync['success']) { -return $sync; -} -$sync_validation = gsp_validate_layout_sync_result($layout, $sync); -if (!$sync_validation['success']) { -return ['success' => false, 'error' => 'Deployed file validation failed: ' . implode(' | ', $sync_validation['errors'])]; -} -gsp_update_log('Layout sync complete: copied=' . $sync['files_copied'] . ', skipped=' . count($sync['skipped'])); -gsp_update_log('Layout sync totals: Panel=' . intval($sync['panel_files_copied']) . ', Website=' . intval($sync['website_files_copied'])); -if (!empty($sync['skipped'])) { -gsp_update_log('Preserved paths: ' . implode(', ', array_slice($sync['skipped'], 0, 50))); -} -if (!empty($sync['copied_files'])) { -$copied_sample = array_slice((array)$sync['copied_files'], 0, 50); -gsp_update_log('Copied file sample: ' . implode(', ', $copied_sample)); -$addons_updates = array_values(array_filter((array)$sync['copied_files'], function ($rel) { -return strpos($rel, 'Panel/modules/addonsmanager/') === 0; -})); -if (!empty($addons_updates)) { -gsp_update_log('Addonsmanager files copied: ' . implode(', ', array_slice($addons_updates, 0, 50))); -} -} -return [ -'success' => true, -'files_copied' => $sync['files_copied'], -'panel_files_copied' => $sync['panel_files_copied'], -'website_files_copied' => $sync['website_files_copied'], -'preserved' => $sync['skipped'], -'copied_files' => $sync['copied_files'], -'patches' => $patches['run'], -]; -} function gsp_fix_permissions($root_dir) { @@ -1299,12 +1443,12 @@ if (!$preflight['success']) { return ['success' => false, 'error' => 'Preflight failed: ' . implode(' | ', $preflight['errors'])]; } -$backup = ['success' => true, 'backup_dir' => null]; -if (!empty($update_cfg['backup_before_update'])) { -$backup = gsp_create_full_backup('git-update', $update_cfg['branch'], false); -if (!$backup['success']) { -return $backup; -} + $backup = ['success' => true, 'backup_dir' => null]; + if (!empty($update_cfg['backup_before_update'])) { + $backup = gsp_create_full_backup('git-update', $update_cfg['branch'], false, $update_cfg); + if (!$backup['success']) { + return $backup; + } gsp_update_log("Backup created before git update to {$update_cfg['branch']}: {$backup['backup_dir']}"); } @@ -1354,21 +1498,23 @@ return [ ]; } -function gsp_safe_component_backup($component, $source_path, $backup_base) +function gsp_safe_component_backup($component, $source_path, $backup_base, $retention = GSP_DEFAULT_BACKUP_RETENTION) { -$component = preg_replace('/[^A-Za-z0-9._-]/', '_', (string)$component); -$backup_base = rtrim((string)$backup_base, '/'); -if ($backup_base === '' || strpos($backup_base, '/') !== 0) { -$backup_base = GSP_BACKUP_BASE; -} -if (!is_dir($backup_base) && !@mkdir($backup_base, 0755, true)) { -return ['success' => false, 'error' => 'Cannot create backup path: ' . $backup_base]; -} -$ts = date('Y-m-d_H-i-s'); -$backup_dir = $backup_base . '/component_' . $component . '_' . $ts; -if (!@mkdir($backup_dir, 0755, true)) { -return ['success' => false, 'error' => 'Cannot create component backup directory: ' . $backup_dir]; -} + $component = preg_replace('/[^A-Za-z0-9._-]/', '_', (string)$component); + $backup_base = rtrim((string)$backup_base, '/'); + if ($backup_base === '' || strpos($backup_base, '/') !== 0) { + $backup_base = gsp_get_backup_base(); + } + $backup_base_ready = gsp_ensure_directory($backup_base, 'component backup path'); + if (!$backup_base_ready['success']) { + return ['success' => false, 'error' => $backup_base_ready['error']]; + } + $ts = date('Y-m-d_H-i-s'); + $backup_dir = $backup_base . '/component_' . $component . '_' . $ts; + $backup_dir_ready = gsp_ensure_directory($backup_dir, 'component backup directory'); + if (!$backup_dir_ready['success']) { + return ['success' => false, 'error' => $backup_dir_ready['error']]; + } if (!is_dir($source_path)) { return ['success' => true, 'backup_dir' => $backup_dir, 'skipped' => true]; } @@ -1380,10 +1526,14 @@ return $archive_result; @file_put_contents($backup_dir . '/backup.json', json_encode([ 'component' => $component, 'source_path' => $source_path, -'created_at' => date('Y-m-d H:i:s'), -'archive' => basename($archive), -], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); -return ['success' => true, 'backup_dir' => $backup_dir, 'archive' => $archive]; + 'created_at' => date('Y-m-d H:i:s'), + 'archive' => basename($archive), + ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + $prune = gsp_prune_old_backups((int)$retention, $backup_base); + if (!empty($prune['deleted'])) { + gsp_update_log('Component backup retention pruned ' . count($prune['deleted']) . ' entries from ' . $backup_base . ': ' . implode(', ', $prune['deleted'])); + } + return ['success' => true, 'backup_dir' => $backup_dir, 'archive' => $archive]; } function gsp_component_source_path($source_root, $relative) @@ -1518,7 +1668,7 @@ return ['success' => false, 'error' => 'Destination path for ' . $component . ' } $backup = ['success' => true, 'backup_dir' => null]; if (!empty($update_cfg['backup_before_update'])) { -$backup = gsp_safe_component_backup($component, $dest, $update_cfg['backup_path']); + $backup = gsp_safe_component_backup($component, $dest, gsp_get_backup_base($update_cfg), gsp_get_backup_retention($update_cfg)); if (!$backup['success']) { gsp_rmdir_recursive($checkout['temp_dir']); return $backup; @@ -1680,17 +1830,18 @@ gsp_update_log('Remote agent update requested: id=' . $remote_id . ' component=' return ['success' => true, 'results' => $results, 'success_count' => $success_count]; } -function gsp_get_available_backups() +function gsp_get_available_backups($backup_base = null) { -$backups = []; -if (!is_dir(GSP_BACKUP_BASE)) { -return $backups; -} -foreach ((array)scandir(GSP_BACKUP_BASE) as $entry) { + $backups = []; + $backup_base = $backup_base ? rtrim((string)$backup_base, '/') : gsp_get_backup_base(); + if (!is_dir($backup_base)) { + return $backups; + } + foreach ((array)scandir($backup_base) as $entry) { if ($entry === '.' || $entry === '..') { continue; } -$dir = GSP_BACKUP_BASE . '/' . $entry; + $dir = $backup_base . '/' . $entry; if (!is_dir($dir)) { continue; } @@ -1763,10 +1914,10 @@ return ['success' => false, 'error' => 'MySQL restore failed: ' . implode(' | ', return ['success' => true]; } -function gsp_do_revert($backup_ts, $restore_apache = false) +function gsp_do_revert($backup_ts, $restore_apache = false, array $update_cfg = null) { -global $db; -$backup_dir = GSP_BACKUP_BASE . '/' . $backup_ts; + global $db; + $backup_dir = gsp_get_backup_base($update_cfg) . '/' . $backup_ts; if (!is_dir($backup_dir)) { return ['success' => false, 'error' => 'Backup not found: ' . $backup_ts]; } @@ -2058,37 +2209,12 @@ return [ function gsp_fix_apache_paths($confirmed, $reload_apache) { -if (!$confirmed) { -return ['success' => false, 'error' => 'Apache path fix requires explicit confirmation.']; -} - -function gsp_disable_ssl_vhost($vhost_file, $confirmed) -{ -if (!$confirmed) { -return ['success' => false, 'error' => 'SSL vhost disable requires confirmation.']; -} -$vhost = basename((string)$vhost_file); -if (!preg_match('/\.conf$/', $vhost) || strpos($vhost, '-ssl') === false) { -return ['success' => false, 'error' => 'Only SSL vhost .conf files can be disabled from this action.']; -} -$enabled_path = '/etc/apache2/sites-enabled/' . $vhost; -if (!file_exists($enabled_path) && !is_link($enabled_path)) { -return ['success' => true, 'message' => $vhost . ' is already disabled.']; -} -if (!@unlink($enabled_path)) { -return ['success' => false, 'error' => 'Failed to disable SSL site: ' . $vhost]; -} -$test = gsp_apache_configtest(); -if (!$test['success']) { -return ['success' => false, 'error' => 'Disabled site but apache2ctl configtest still failed: ' . $test['output']]; -} -$reload = gsp_apache_reload(); -gsp_update_log('Disabled SSL vhost in sites-enabled: ' . $vhost); -return ['success' => true, 'configtest' => $test, 'reload' => $reload, 'message' => 'Disabled SSL vhost: ' . $vhost]; -} -$scan = gsp_scan_apache_configs(); -if (!$scan['available']) { -return ['success' => false, 'error' => 'Apache config folder not available.']; + 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); @@ -2174,8 +2300,33 @@ return [ 'backup_dir' => $backup['backup_dir'], 'configtest' => $test, 'reload' => $reload, -'planned_replacements' => $planned, -]; + 'planned_replacements' => $planned, + ]; +} + +function gsp_disable_ssl_vhost($vhost_file, $confirmed) +{ + if (!$confirmed) { + return ['success' => false, 'error' => 'SSL vhost disable requires confirmation.']; + } + $vhost = basename((string)$vhost_file); + if (!preg_match('/\.conf$/', $vhost) || strpos($vhost, '-ssl') === false) { + return ['success' => false, 'error' => 'Only SSL vhost .conf files can be disabled from this action.']; + } + $enabled_path = '/etc/apache2/sites-enabled/' . $vhost; + if (!file_exists($enabled_path) && !is_link($enabled_path)) { + return ['success' => true, 'message' => $vhost . ' is already disabled.']; + } + if (!@unlink($enabled_path)) { + return ['success' => false, 'error' => 'Failed to disable SSL site: ' . $vhost]; + } + $test = gsp_apache_configtest(); + if (!$test['success']) { + return ['success' => false, 'error' => 'Disabled site but apache2ctl configtest still failed: ' . $test['output']]; + } + $reload = gsp_apache_reload(); + gsp_update_log('Disabled SSL vhost in sites-enabled: ' . $vhost); + return ['success' => true, 'configtest' => $test, 'reload' => $reload, 'message' => 'Disabled SSL vhost: ' . $vhost]; } function gsp_get_patch_overview() @@ -2255,10 +2406,11 @@ $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); + $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); + try { if ($action === 'preflight') { $preflight_result = gsp_preflight_check(); @@ -2297,8 +2449,8 @@ print_success(htmlspecialchars(isset($disable['message']) ? $disable['message'] } else { print_failure('Disable SSL vhost failed: ' . htmlspecialchars($disable['error'])); } -} elseif ($action === 'backup_only') { -$result = gsp_create_full_backup('backup-only', 'manual', false); + } elseif ($action === 'backup_only') { + $result = gsp_create_full_backup('backup-only', 'manual', false, $update_cfg); if ($result['success']) { print_success('Backup created: ' . htmlspecialchars($result['backup_dir']) . ''); } else { @@ -2315,9 +2467,10 @@ $new_cfg = [ 'linux_agent_source_path' => isset($_POST['gsp_update_linux_agent_source_path']) ? trim((string)$_POST['gsp_update_linux_agent_source_path'], '/') : '', 'windows_agent_source_path' => isset($_POST['gsp_update_windows_agent_source_path']) ? trim((string)$_POST['gsp_update_windows_agent_source_path'], '/') : '', 'website_source_path' => isset($_POST['gsp_update_website_source_path']) ? trim((string)$_POST['gsp_update_website_source_path'], '/') : '', -'git_path' => isset($_POST['gsp_update_git_path']) ? trim((string)$_POST['gsp_update_git_path']) : '', -'backup_path' => isset($_POST['gsp_update_backup_path']) ? rtrim(trim((string)$_POST['gsp_update_backup_path']), '/') : '', -'panel_post_update_command' => isset($_POST['gsp_update_panel_post_update_command']) ? trim((string)$_POST['gsp_update_panel_post_update_command']) : '', + 'git_path' => isset($_POST['gsp_update_git_path']) ? trim((string)$_POST['gsp_update_git_path']) : '', + 'backup_path' => isset($_POST['gsp_update_backup_path']) ? rtrim(trim((string)$_POST['gsp_update_backup_path']), '/') : '', + 'backup_retention' => isset($_POST['gsp_update_backup_retention']) ? trim((string)$_POST['gsp_update_backup_retention']) : '', + 'panel_post_update_command' => isset($_POST['gsp_update_panel_post_update_command']) ? trim((string)$_POST['gsp_update_panel_post_update_command']) : '', 'website_post_update_command' => isset($_POST['gsp_update_website_post_update_command']) ? trim((string)$_POST['gsp_update_website_post_update_command']) : '', 'linux_agent_post_update_command' => isset($_POST['gsp_update_linux_agent_post_update_command']) ? trim((string)$_POST['gsp_update_linux_agent_post_update_command']) : '', 'windows_agent_post_update_command' => isset($_POST['gsp_update_windows_agent_post_update_command']) ? trim((string)$_POST['gsp_update_windows_agent_post_update_command']) : '', @@ -2337,9 +2490,10 @@ $db->setSettings([ 'gsp_update_linux_agent_source_path' => $new_cfg['linux_agent_source_path'], 'gsp_update_windows_agent_source_path' => $new_cfg['windows_agent_source_path'], 'gsp_update_website_source_path' => $new_cfg['website_source_path'], -'gsp_update_git_path' => $new_cfg['git_path'], -'gsp_update_backup_path' => $new_cfg['backup_path'], -'gsp_update_panel_post_update_command' => $new_cfg['panel_post_update_command'], + 'gsp_update_git_path' => $new_cfg['git_path'], + 'gsp_update_backup_path' => $new_cfg['backup_path'], + 'gsp_update_backup_retention' => $new_cfg['backup_retention'], + 'gsp_update_panel_post_update_command' => $new_cfg['panel_post_update_command'], 'gsp_update_website_post_update_command' => $new_cfg['website_post_update_command'], 'gsp_update_linux_agent_post_update_command' => $new_cfg['linux_agent_post_update_command'], 'gsp_update_windows_agent_post_update_command' => $new_cfg['windows_agent_post_update_command'], @@ -2354,9 +2508,10 @@ $settings['gsp_update_panel_source_path'] = $new_cfg['panel_source_path']; $settings['gsp_update_linux_agent_source_path'] = $new_cfg['linux_agent_source_path']; $settings['gsp_update_windows_agent_source_path'] = $new_cfg['windows_agent_source_path']; $settings['gsp_update_website_source_path'] = $new_cfg['website_source_path']; -$settings['gsp_update_git_path'] = $new_cfg['git_path']; -$settings['gsp_update_backup_path'] = $new_cfg['backup_path']; -$settings['gsp_update_panel_post_update_command'] = $new_cfg['panel_post_update_command']; + $settings['gsp_update_git_path'] = $new_cfg['git_path']; + $settings['gsp_update_backup_path'] = $new_cfg['backup_path']; + $settings['gsp_update_backup_retention'] = $new_cfg['backup_retention']; + $settings['gsp_update_panel_post_update_command'] = $new_cfg['panel_post_update_command']; $settings['gsp_update_website_post_update_command'] = $new_cfg['website_post_update_command']; $settings['gsp_update_linux_agent_post_update_command'] = $new_cfg['linux_agent_post_update_command']; $settings['gsp_update_windows_agent_post_update_command'] = $new_cfg['windows_agent_post_update_command']; @@ -2375,9 +2530,10 @@ $update_cfg = [ 'linux_agent_source_path' => isset($_POST['gsp_update_linux_agent_source_path']) ? trim((string)$_POST['gsp_update_linux_agent_source_path'], '/') : $update_cfg['linux_agent_source_path'], 'windows_agent_source_path' => isset($_POST['gsp_update_windows_agent_source_path']) ? trim((string)$_POST['gsp_update_windows_agent_source_path'], '/') : $update_cfg['windows_agent_source_path'], 'website_source_path' => isset($_POST['gsp_update_website_source_path']) ? trim((string)$_POST['gsp_update_website_source_path'], '/') : $update_cfg['website_source_path'], -'git_path' => isset($_POST['gsp_update_git_path']) ? trim((string)$_POST['gsp_update_git_path']) : $update_cfg['git_path'], -'backup_path' => isset($_POST['gsp_update_backup_path']) ? rtrim(trim((string)$_POST['gsp_update_backup_path']), '/') : $update_cfg['backup_path'], -'panel_post_update_command' => isset($_POST['gsp_update_panel_post_update_command']) ? trim((string)$_POST['gsp_update_panel_post_update_command']) : $update_cfg['panel_post_update_command'], + 'git_path' => isset($_POST['gsp_update_git_path']) ? trim((string)$_POST['gsp_update_git_path']) : $update_cfg['git_path'], + 'backup_path' => isset($_POST['gsp_update_backup_path']) ? rtrim(trim((string)$_POST['gsp_update_backup_path']), '/') : $update_cfg['backup_path'], + 'backup_retention' => isset($_POST['gsp_update_backup_retention']) ? trim((string)$_POST['gsp_update_backup_retention']) : $update_cfg['backup_retention'], + 'panel_post_update_command' => isset($_POST['gsp_update_panel_post_update_command']) ? trim((string)$_POST['gsp_update_panel_post_update_command']) : $update_cfg['panel_post_update_command'], 'website_post_update_command' => isset($_POST['gsp_update_website_post_update_command']) ? trim((string)$_POST['gsp_update_website_post_update_command']) : $update_cfg['website_post_update_command'], 'linux_agent_post_update_command' => isset($_POST['gsp_update_linux_agent_post_update_command']) ? trim((string)$_POST['gsp_update_linux_agent_post_update_command']) : $update_cfg['linux_agent_post_update_command'], 'windows_agent_post_update_command' => isset($_POST['gsp_update_windows_agent_post_update_command']) ? trim((string)$_POST['gsp_update_windows_agent_post_update_command']) : $update_cfg['windows_agent_post_update_command'], @@ -2442,9 +2598,10 @@ $update_cfg = [ 'linux_agent_source_path' => isset($_POST['gsp_update_linux_agent_source_path']) ? trim((string)$_POST['gsp_update_linux_agent_source_path'], '/') : $update_cfg['linux_agent_source_path'], 'windows_agent_source_path' => isset($_POST['gsp_update_windows_agent_source_path']) ? trim((string)$_POST['gsp_update_windows_agent_source_path'], '/') : $update_cfg['windows_agent_source_path'], 'website_source_path' => isset($_POST['gsp_update_website_source_path']) ? trim((string)$_POST['gsp_update_website_source_path'], '/') : $update_cfg['website_source_path'], -'git_path' => isset($_POST['gsp_update_git_path']) ? trim((string)$_POST['gsp_update_git_path']) : $update_cfg['git_path'], -'backup_path' => isset($_POST['gsp_update_backup_path']) ? rtrim(trim((string)$_POST['gsp_update_backup_path']), '/') : $update_cfg['backup_path'], -'panel_post_update_command' => isset($_POST['gsp_update_panel_post_update_command']) ? trim((string)$_POST['gsp_update_panel_post_update_command']) : $update_cfg['panel_post_update_command'], + 'git_path' => isset($_POST['gsp_update_git_path']) ? trim((string)$_POST['gsp_update_git_path']) : $update_cfg['git_path'], + 'backup_path' => isset($_POST['gsp_update_backup_path']) ? rtrim(trim((string)$_POST['gsp_update_backup_path']), '/') : $update_cfg['backup_path'], + 'backup_retention' => isset($_POST['gsp_update_backup_retention']) ? trim((string)$_POST['gsp_update_backup_retention']) : $update_cfg['backup_retention'], + 'panel_post_update_command' => isset($_POST['gsp_update_panel_post_update_command']) ? trim((string)$_POST['gsp_update_panel_post_update_command']) : $update_cfg['panel_post_update_command'], 'website_post_update_command' => isset($_POST['gsp_update_website_post_update_command']) ? trim((string)$_POST['gsp_update_website_post_update_command']) : $update_cfg['website_post_update_command'], 'linux_agent_post_update_command' => isset($_POST['gsp_update_linux_agent_post_update_command']) ? trim((string)$_POST['gsp_update_linux_agent_post_update_command']) : $update_cfg['linux_agent_post_update_command'], 'windows_agent_post_update_command' => isset($_POST['gsp_update_windows_agent_post_update_command']) ? trim((string)$_POST['gsp_update_windows_agent_post_update_command']) : $update_cfg['windows_agent_post_update_command'], @@ -2460,9 +2617,10 @@ $db->setSettings([ 'gsp_update_linux_agent_source_path' => $update_cfg['linux_agent_source_path'], 'gsp_update_windows_agent_source_path' => $update_cfg['windows_agent_source_path'], 'gsp_update_website_source_path' => $update_cfg['website_source_path'], -'gsp_update_git_path' => $update_cfg['git_path'], -'gsp_update_backup_path' => $update_cfg['backup_path'], -'gsp_update_panel_post_update_command' => $update_cfg['panel_post_update_command'], + 'gsp_update_git_path' => $update_cfg['git_path'], + 'gsp_update_backup_path' => $update_cfg['backup_path'], + 'gsp_update_backup_retention' => $update_cfg['backup_retention'], + 'gsp_update_panel_post_update_command' => $update_cfg['panel_post_update_command'], 'gsp_update_website_post_update_command' => $update_cfg['website_post_update_command'], 'gsp_update_linux_agent_post_update_command' => $update_cfg['linux_agent_post_update_command'], 'gsp_update_windows_agent_post_update_command' => $update_cfg['windows_agent_post_update_command'], @@ -2526,32 +2684,41 @@ $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); + $result = gsp_do_revert($backup_ts, $restore_apache, $update_cfg); if ($result['success']) { print_success('Reverted to backup ' . htmlspecialchars($backup_ts) . '.'); } else { print_failure('Revert failed: ' . htmlspecialchars($result['error'])); } } -} + } -$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); -} + $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); + } catch (Throwable $e) { + $finished_at = date('Y-m-d H:i:s'); + gsp_update_log('Update action failure [' . $action . ']: ' . $e->getMessage() . ' @ ' . $e->getFile() . ':' . $e->getLine()); + gsp_log_update_to_db('panel-update', null, 'error', 'Admin action failed: ' . $action . ' | ' . $e->getMessage(), null, null, null, $started_at, $finished_at); + print_failure('Update action failed: ' . htmlspecialchars($e->getMessage())); + } + } $_SESSION['gsp_update_csrf'] = gsp_random_token(); $csrf_token = $_SESSION['gsp_update_csrf']; } $current_version = gsp_get_current_version(); -$current_branch = gsp_get_current_branch(); -$git_commit = gsp_get_git_commit(); + $current_branch = gsp_get_current_branch(); + $git_commit = gsp_get_git_commit(); $last_layout = isset($_SESSION['gsp_last_update_layout']) && is_array($_SESSION['gsp_last_update_layout']) ? $_SESSION['gsp_last_update_layout'] : null; -$vinfo = gsp_read_version_json(); -$latest_release = 'N/A'; -$backups = gsp_get_available_backups(); -$patch_overview = gsp_get_patch_overview(); + $vinfo = gsp_read_version_json(); + $latest_release = 'N/A'; + $backup_base = gsp_get_backup_base($update_cfg); + $backup_retention = gsp_get_backup_retention($update_cfg); + $managed_backups = gsp_get_managed_backup_entries($backup_base); + $backups = gsp_get_available_backups($backup_base); + $patch_overview = gsp_get_patch_overview(); if ($apache_scan_result === null) { $apache_scan_result = gsp_scan_apache_configs(); } @@ -2570,7 +2737,9 @@ if ($git_commit) { echo "Git Commit:" . htmlspecialchars(substr($git_commit, 0, 12)) . "\n"; } echo "Update Trace Log:" . htmlspecialchars(GSP_UPDATE_LOG) . "\n"; -echo "Backups Stored:" . intval(count($backups)) . " (retention: 5)\n"; + echo "Backup Path:" . htmlspecialchars($backup_base) . "\n"; + echo "Managed Backups Stored:" . intval(count($managed_backups)) . " (retention: " . intval($backup_retention) . ")\n"; + echo "Rollback Backups Available:" . intval(count($backups)) . "\n"; echo "\n"; if ($ssl_issue_count > 0) { echo "

SSL warning: " . intval($ssl_issue_count) . " Apache SSL certificate issue(s) detected. Updates are not blocked by this. See Advanced Diagnostics for details.

\n"; @@ -2595,9 +2764,10 @@ echo "Panel Source FolderLinux Agent Source Folder\n"; echo "Windows Agent Source Folder\n"; echo "Website Source Folder\n"; -echo "Git Executable Usually git.\n"; -echo "Backup Path\n"; -echo "Backup Before Update\n"; + echo "Git Executable Usually git.\n"; + echo "Backup Path\n"; + echo "Backup Retention Keep newest backups only.\n"; + echo "Backup Before Update\n"; echo "Panel Post-update Command Admin-only dangerous command.\n"; echo "Website Post-update Command\n"; echo "Linux Agent Post-update Command\n"; diff --git a/Panel/modules/update/tests/update_config_smoke.php b/Panel/modules/update/tests/update_config_smoke.php index 2c4425fa..6814dda6 100644 --- a/Panel/modules/update/tests/update_config_smoke.php +++ b/Panel/modules/update/tests/update_config_smoke.php @@ -13,8 +13,11 @@ function gsp_update_smoke_assert($condition, $message) $cfg = gsp_update_settings(); gsp_update_smoke_assert(function_exists('gsp_do_configured_git_update'), 'configured Git updater helper is top-level'); +gsp_update_smoke_assert(function_exists('gsp_checkout_update_source'), 'configured Git checkout helper is top-level'); +gsp_update_smoke_assert(function_exists('gsp_disable_ssl_vhost'), 'SSL vhost disable helper is top-level'); gsp_update_smoke_assert($cfg['repo_url'] === 'http://forge.runlevelsystems.com/dev/GSP.git', 'default repo URL is Forgejo'); gsp_update_smoke_assert($cfg['branch'] === 'Panel-unstable', 'default branch is Panel-unstable'); +gsp_update_smoke_assert((int)$cfg['backup_retention'] === 5, 'default backup retention is 5'); gsp_update_smoke_assert($cfg['panel_source_path'] === 'Panel', 'default Panel source folder'); gsp_update_smoke_assert($cfg['linux_agent_source_path'] === 'Agent_Linux', 'default Linux agent source folder'); gsp_update_smoke_assert($cfg['windows_agent_source_path'] === 'Agent-Windows', 'default Windows agent source folder'); diff --git a/Panel/modules/update/update.php b/Panel/modules/update/update.php index 8e5df585..a829c3d8 100644 --- a/Panel/modules/update/update.php +++ b/Panel/modules/update/update.php @@ -30,6 +30,17 @@ function exec_ogp_module() } require_once(dirname(__FILE__) . '/../administration/panel_update.php'); - gsp_panel_update_section(); + if (!function_exists('gsp_panel_update_section')) { + print_failure('Update module is unavailable because the updater helper file did not load correctly.'); + return; + } + try { + gsp_panel_update_section(); + } catch (Throwable $e) { + if (function_exists('gsp_update_log')) { + gsp_update_log('Unhandled update page error: ' . $e->getMessage() . ' @ ' . $e->getFile() . ':' . $e->getLine()); + } + print_failure('Update module failed: ' . htmlspecialchars($e->getMessage())); + } } ?> diff --git a/docs/modules/PANEL_UPDATE.md b/docs/modules/PANEL_UPDATE.md new file mode 100644 index 00000000..ab7b7154 --- /dev/null +++ b/docs/modules/PANEL_UPDATE.md @@ -0,0 +1,200 @@ +# Panel Update Module + +## Scope + +This document covers the admin Panel updater implemented through: + +- `Panel/modules/update/update.php` +- `Panel/modules/administration/panel_update.php` +- `Panel/includes/lib_remote.php` for remote agent component updates + +## Current Workflow + +```text +Admin opens home.php?m=update + -> update.php loads panel_update.php + -> gsp_panel_update_section() renders UI + +Configured Git update: + -> load saved settings + -> validate repo/branch/paths/retention + -> preflight path and write checks + -> create backup when enabled + -> git clone configured branch to temp checkout + -> resolve source layout + -> self-update updater files first if needed + -> apply patches + -> sync Panel and Website files + -> preserve config.inc.php and protected paths + -> write LAST_UPDATE.txt and version metadata + -> clear cache / fix permissions + -> prune backups by retention +``` + +## Backup Workflow + +Backup helpers: + +- `gsp_get_backup_base()` +- `gsp_get_backup_retention()` +- `gsp_create_full_backup()` +- `gsp_safe_component_backup()` +- `gsp_get_available_backups()` +- `gsp_get_managed_backup_entries()` +- `gsp_prune_old_backups()` + +Behavior: + +- full updater backups use the configured `gsp_update_backup_path` +- component-only backups also use the configured backup path +- missing parent directories are created recursively +- failures are logged to `logs/update_trace.log` with path diagnostics +- retention is enforced after full backups and component backups + +Managed backup directory names: + +- full backups: `YYYY-MM-DD_HH-MM-SS` +- component backups: `component__YYYY-MM-DD_HH-MM-SS` + +Rollback uses only full backups. Component backups are retained and pruned, but are not shown in the rollback selector. + +## Retention Workflow + +Retention setting: + +- `gsp_update_backup_retention` +- default: `5` + +Behavior: + +1. Create backup. +2. Scan all updater-managed backup directories under the configured backup path. +3. Sort newest to oldest. +4. Delete entries beyond retention. +5. Log deleted backup directories to `logs/update_trace.log`. + +This applies to both: + +- full updater backups +- component backups created by local component updates + +## Required Folders And Permissions + +Minimum writable locations: + +- configured repository root +- configured Panel path +- configured backup path +- `logs/` under repository root for `update_trace.log` + +Required tools when used: + +- `git` +- `tar` +- `mysqldump` for database backup +- `mysql` for restore + +## Error Handling + +The update page should not white-screen for routine helper/runtime failures. + +Current protections: + +- `update.php` checks that `gsp_panel_update_section()` exists before calling it +- `update.php` catches `Throwable` and logs unexpected failures +- `gsp_panel_update_section()` catches action-time `Throwable` and keeps rendering the UI +- backup directory creation logs detailed path diagnostics +- missing helper regressions are surfaced as user-visible failures instead of fatal white screens where possible + +Log file: + +- `GSP_ROOT/logs/update_trace.log` + +## Helper Scope Warnings + +These helpers must remain top-level: + +- `gsp_update_settings()` +- `gsp_validate_update_settings()` +- `gsp_checkout_update_source()` +- `gsp_do_configured_git_update()` +- `gsp_disable_ssl_vhost()` + +If any of these are accidentally nested inside another function, the update page can pass `php -l` but still fail at runtime with `Call to undefined function ...`. + +## Scheduler Audit + +Current finding: + +- the existing `cron` scheduler is game-server oriented +- it schedules `gamemanager` and `server_content` actions through `ogp_api.php` +- it does **not** currently expose a first-class scheduled self-update path for: + - Panel updates + - Website updates + - remote Linux agent code updates + - remote Windows agent code updates + +Result: + +- backup/retention integration for scheduled self-updates is currently not applicable because those scheduled self-update jobs do not exist in the current codebase +- game-server Steam/content scheduler jobs are separate and do not use this updater module + +## Remote Agent Update Flow + +Remote component updates use: + +- `lib_remote.php::component_update()` +- agent-side `component_update` RPC + +The Panel: + +1. validates configured repo and source folder +2. selects Linux or Windows agent source folder +3. builds an encoded payload +4. sends the payload over XML-RPC +5. shows queued/failed results per remote host + +## Troubleshooting + +### Undefined function crash + +Check: + +- `php -l Panel/modules/administration/panel_update.php` +- `php -r 'require "Panel/modules/administration/panel_update.php"; var_dump(function_exists("gsp_checkout_update_source"));'` + +If `php -l` passes but `function_exists(...)` is false, a helper was nested inside another function. + +### Backup directory creation fails + +Check: + +- configured `gsp_update_backup_path` +- parent directory existence +- web server write access +- `logs/update_trace.log` for the exact `mkdir` failure + +### Retention appears wrong + +Remember: + +- rollback list shows only full backups +- managed backup count includes full backups and component backups +- pruning applies to all updater-managed backup directories + +### Update page loads but remote agent updates fail + +Check: + +- PHP XML-RPC extension on the Panel host +- agent reachability and encryption key +- agent-side `component_update` support + +## Validation Commands + +```bash +php -l Panel/modules/administration/panel_update.php +php -l Panel/modules/update/update.php +php Panel/modules/update/tests/update_config_smoke.php +php -r 'require "Panel/modules/administration/panel_update.php"; foreach (["gsp_checkout_update_source","gsp_do_configured_git_update","gsp_disable_ssl_vhost"] as $f) echo $f,":",function_exists($f)?"yes":"no",PHP_EOL;' +``` diff --git a/docs/modules/SCHEDULER.md b/docs/modules/SCHEDULER.md index 1c2d23d2..8a65230b 100644 --- a/docs/modules/SCHEDULER.md +++ b/docs/modules/SCHEDULER.md @@ -94,3 +94,6 @@ If scheduler behavior needs deeper investigation, start with: - `Agent_Linux/ogp_agent.pl` scheduler subroutines - `Agent-Windows/ogp_agent.pl` scheduler subroutines +## Current Panel Update Finding + +The current scheduler does not provide a first-class path for scheduling Panel, Website, or remote agent self-updates. Existing scheduled update actions are game-server oriented, for example `gamemanager/update&type=steam` and Server Content actions. Backup and retention behavior in `Panel/modules/administration/panel_update.php` therefore applies to manual/admin-triggered updater flows, not to any existing scheduled self-update job. diff --git a/docs/modules/UPDATE.md b/docs/modules/UPDATE.md index daaeb9b6..560012e7 100644 --- a/docs/modules/UPDATE.md +++ b/docs/modules/UPDATE.md @@ -1,5 +1,9 @@ # Update Module +Primary operational reference: + +- `docs/modules/PANEL_UPDATE.md` + ## Role `Panel/modules/update` exposes the admin Panel update page. The page delegates most update behavior to: diff --git a/logs/update_trace.log b/logs/update_trace.log new file mode 100644 index 00000000..7783f2da --- /dev/null +++ b/logs/update_trace.log @@ -0,0 +1,2 @@ +[2026-06-07 14:29:08] Pruned old backup: /tmp/gsp_backup_test_2775997/component_panel_2026-06-01_00-00-01 +[2026-06-07 14:32:04] Pruned old backup: /tmp/gsp_retention_6a2580e44c1eb/component_panel_2026-06-07_09-00-00