fixed updated

This commit is contained in:
Frank Harris 2026-06-07 15:37:43 -05:00
parent a1e5331f4a
commit 5ead40a761
7 changed files with 606 additions and 213 deletions

View file

@ -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: <code>' . htmlspecialchars($result['backup_dir']) . '</code>');
} 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 <strong>' . htmlspecialchars($backup_ts) . '</strong>.');
} 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 "<tr><td><strong>Git Commit:</strong></td><td>" . htmlspecialchars(substr($git_commit, 0, 12)) . "</td></tr>\n";
}
echo "<tr><td><strong>Update Trace Log:</strong></td><td><code>" . htmlspecialchars(GSP_UPDATE_LOG) . "</code></td></tr>\n";
echo "<tr><td><strong>Backups Stored:</strong></td><td>" . intval(count($backups)) . " (retention: 5)</td></tr>\n";
echo "<tr><td><strong>Backup Path:</strong></td><td><code>" . htmlspecialchars($backup_base) . "</code></td></tr>\n";
echo "<tr><td><strong>Managed Backups Stored:</strong></td><td>" . intval(count($managed_backups)) . " (retention: " . intval($backup_retention) . ")</td></tr>\n";
echo "<tr><td><strong>Rollback Backups Available:</strong></td><td>" . intval(count($backups)) . "</td></tr>\n";
echo "</table>\n";
if ($ssl_issue_count > 0) {
echo "<p style='color:#8a6d3b;'><strong>SSL warning:</strong> " . intval($ssl_issue_count) . " Apache SSL certificate issue(s) detected. Updates are not blocked by this. See Advanced Diagnostics for details.</p>\n";
@ -2595,9 +2764,10 @@ echo "<tr><td><strong>Panel Source Folder</strong></td><td><input type='text' na
echo "<tr><td><strong>Linux Agent Source Folder</strong></td><td><input type='text' name='gsp_update_linux_agent_source_path' value='" . htmlspecialchars($update_cfg['linux_agent_source_path'], ENT_QUOTES, 'UTF-8') . "' size='45'></td></tr>\n";
echo "<tr><td><strong>Windows Agent Source Folder</strong></td><td><input type='text' name='gsp_update_windows_agent_source_path' value='" . htmlspecialchars($update_cfg['windows_agent_source_path'], ENT_QUOTES, 'UTF-8') . "' size='45'></td></tr>\n";
echo "<tr><td><strong>Website Source Folder</strong></td><td><input type='text' name='gsp_update_website_source_path' value='" . htmlspecialchars($update_cfg['website_source_path'], ENT_QUOTES, 'UTF-8') . "' size='45'></td></tr>\n";
echo "<tr><td><strong>Git Executable</strong></td><td><input type='text' name='gsp_update_git_path' value='" . htmlspecialchars($update_cfg['git_path'], ENT_QUOTES, 'UTF-8') . "' size='45'> <small>Usually <code>git</code>.</small></td></tr>\n";
echo "<tr><td><strong>Backup Path</strong></td><td><input type='text' name='gsp_update_backup_path' value='" . htmlspecialchars($update_cfg['backup_path'], ENT_QUOTES, 'UTF-8') . "' size='85'></td></tr>\n";
echo "<tr><td><strong>Backup Before Update</strong></td><td><label><input type='checkbox' name='gsp_update_backup_before' value='1' " . (!empty($update_cfg['backup_before_update']) ? "checked" : "") . "> create backup before updating</label></td></tr>\n";
echo "<tr><td><strong>Git Executable</strong></td><td><input type='text' name='gsp_update_git_path' value='" . htmlspecialchars($update_cfg['git_path'], ENT_QUOTES, 'UTF-8') . "' size='45'> <small>Usually <code>git</code>.</small></td></tr>\n";
echo "<tr><td><strong>Backup Path</strong></td><td><input type='text' name='gsp_update_backup_path' value='" . htmlspecialchars($update_cfg['backup_path'], ENT_QUOTES, 'UTF-8') . "' size='85'></td></tr>\n";
echo "<tr><td><strong>Backup Retention</strong></td><td><input type='number' min='1' max='200' name='gsp_update_backup_retention' value='" . htmlspecialchars($update_cfg['backup_retention'], ENT_QUOTES, 'UTF-8') . "' style='width:90px;'> <small>Keep newest backups only.</small></td></tr>\n";
echo "<tr><td><strong>Backup Before Update</strong></td><td><label><input type='checkbox' name='gsp_update_backup_before' value='1' " . (!empty($update_cfg['backup_before_update']) ? "checked" : "") . "> create backup before updating</label></td></tr>\n";
echo "<tr><td><strong>Panel Post-update Command</strong></td><td><input type='text' name='gsp_update_panel_post_update_command' value='" . htmlspecialchars($update_cfg['panel_post_update_command'], ENT_QUOTES, 'UTF-8') . "' size='85'> <small>Admin-only dangerous command.</small></td></tr>\n";
echo "<tr><td><strong>Website Post-update Command</strong></td><td><input type='text' name='gsp_update_website_post_update_command' value='" . htmlspecialchars($update_cfg['website_post_update_command'], ENT_QUOTES, 'UTF-8') . "' size='85'></td></tr>\n";
echo "<tr><td><strong>Linux Agent Post-update Command</strong></td><td><input type='text' name='gsp_update_linux_agent_post_update_command' value='" . htmlspecialchars($update_cfg['linux_agent_post_update_command'], ENT_QUOTES, 'UTF-8') . "' size='85'></td></tr>\n";

View file

@ -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');

View file

@ -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()));
}
}
?>

View file

@ -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_<name>_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;'
```

View file

@ -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.

View file

@ -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:

2
logs/update_trace.log Normal file
View file

@ -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