diff --git a/Panel/js/modules/addonsmanager.js b/Panel/js/modules/addonsmanager.js index d81f63a2..b2135530 100644 --- a/Panel/js/modules/addonsmanager.js +++ b/Panel/js/modules/addonsmanager.js @@ -1,11 +1,9 @@ $(function() { var methodToRows = { - download_zip: ['#scm-row-url', '#scm-row-path', '#scm-row-post-script'], - download_file: ['#scm-row-url', '#scm-row-path', '#scm-row-post-script'], - steam_workshop: ['#scm-row-workshop-id', '#scm-row-workshop-app-id', '#scm-row-target-path-template', '#scm-row-optional-folder-name', '#scm-row-launch-param-additions', '#scm-row-config-edit-rule', '#scm-row-post-script'], + download_zip: ['#scm-row-url', '#scm-row-path'], + steam_workshop: ['#scm-row-workshop-id', '#scm-row-workshop-app-id', '#scm-row-target-path-template', '#scm-row-optional-folder-name'], post_script: ['#scm-row-post-script'], - config_edit: ['#scm-row-path', '#scm-row-config-edit-rule', '#scm-row-post-script'], - create_folder: ['#scm-row-path'] + config_edit: ['#scm-row-path', '#scm-row-config-edit-rule'] }; var allRows = [ '#scm-row-url', @@ -34,7 +32,7 @@ $(function() { var selectedOption = $method.find('option:selected'); var helpText = selectedOption.data('help') || ''; $help.text(helpText); - $('#scm-path-label').text(value === 'config_edit' ? 'Config Target Path' : 'Target Path'); + $('#scm-path-label').text(value === 'config_edit' ? 'Config Target Path' : 'Target Path / Extract Path (optional)'); } $method.on('change', applyContentTypeUi); diff --git a/Panel/lang/English/modules/addonsmanager.php b/Panel/lang/English/modules/addonsmanager.php index 063c01d7..22e50cce 100644 --- a/Panel/lang/English/modules/addonsmanager.php +++ b/Panel/lang/English/modules/addonsmanager.php @@ -45,8 +45,9 @@ define('LANG_select_game_type', "Select Game Type"); define('LANG_plugin', "Plugins / Mods"); define('LANG_mappack', "Map Packs"); define('LANG_config', "Config Packs"); -// Additional category labels (for future content types already defined in server_content_categories.php) -if (!defined('LANG_version')) define('LANG_version', "Version"); +if (!defined('LANG_version')) { + define('LANG_version', "Version"); +} define('LANG_server_content_version', "Server Versions"); define('LANG_modpack', "Modpacks"); define('LANG_workshop', "Workshop Content"); @@ -63,12 +64,12 @@ define('LANG_create_addon', "Create Server Content Item"); define('LANG_addons_db', "Server Content Database"); define('LANG_addon_has_been_created', "The server content item \"%s\" has been created."); define('LANG_remove_addon', "Remove"); -define('LANG_fill_the_url_address_to_a_compressed_file', "Please enter a URL for the compressed file to download."); +define('LANG_fill_the_url_address_to_a_compressed_file', "Please enter a download URL."); define('LANG_fill_the_download_url', "Please enter a download URL."); define('LANG_fill_the_workshop_id', "Please enter a Workshop ID."); -define('LANG_fill_the_target_install_path', "Please select a target install path."); -define('LANG_fill_the_script_action_body', "Please enter a script/action body."); -define('LANG_fill_the_config_edit_rule', "Please enter a config edit rule."); +define('LANG_fill_the_target_install_path', "Please enter the config target and edit action."); +define('LANG_fill_the_script_action_body', "Please enter the installer script/action."); +define('LANG_fill_the_config_edit_rule', "Please enter the config target and edit action."); define('LANG_fill_the_addon_name', "Please enter a name for the server content item."); define('LANG_select_an_addon_type', "Please select a content type."); define('LANG_select_a_game_type', "Please select a game type."); @@ -92,10 +93,8 @@ define('LANG_target_path_template', "Target Path"); define('LANG_optional_folder_name', "Optional Folder Name"); define('LANG_config_edit_rule', "Config Edit Rule"); define('LANG_launch_param_additions', "Launch Parameter Additions"); -define('LANG_content_type_help_download_zip', "Downloads and extracts an archive into the target path."); -define('LANG_content_type_help_download_file', "Downloads a single file without extraction."); -define('LANG_content_type_help_steam_workshop', "Downloads/updates a Workshop item and applies it to the server."); -define('LANG_content_type_help_post_script', "Runs a script/action only, with no URL required."); -define('LANG_content_type_help_config_edit', "Applies config edit rules to a target path, with no URL required."); -define('LANG_content_type_help_create_folder', "Creates target folders/paths only, with no URL required."); +define('LANG_content_type_help_download_zip', "Downloads an archive/file from URL; extract path is optional."); +define('LANG_content_type_help_steam_workshop', "Installs/updates a Workshop item with Workshop ID (no URL required)."); +define('LANG_content_type_help_post_script', "Runs a scripted installer action (no URL required)."); +define('LANG_content_type_help_config_edit', "Edits config at target path using provided action/rules (no URL required)."); ?> diff --git a/Panel/modules/addonsmanager/addons_installer.php b/Panel/modules/addonsmanager/addons_installer.php index 6ea509ce..b602f5b7 100644 --- a/Panel/modules/addonsmanager/addons_installer.php +++ b/Panel/modules/addonsmanager/addons_installer.php @@ -156,7 +156,7 @@ function exec_ogp_module() { // Use the full category map so newly added types are accepted without // editing this file. The original three types are always present. $addon_types = get_server_content_type_keys(); - $addon_type = isset($_REQUEST['addon_type']) ? $_REQUEST['addon_type'] : ""; + $addon_type = isset($_REQUEST['addon_type']) ? scm_normalize_addon_type($_REQUEST['addon_type']) : ""; $state = isset($_REQUEST['state']) ? $_REQUEST['state'] : ""; $pid = isset($_REQUEST['pid']) ? $_REQUEST['pid'] : -1; @@ -556,12 +556,6 @@ function exec_ogp_module() { return; } - if ($addon_type === 'workshop') { - scm_ensure_workshop_schema($db); - $view->refresh('?m=addonsmanager&p=workshop_content&home_id='.(int)$home_id.'&mod_id='.(int)$mod_id.'&ip='.urlencode((string)$ip).'&port='.urlencode((string)$port), 0); - return; - } - ?> - Workshop App ID + Game Compatibility (Workshop App ID) - + @@ -302,22 +300,6 @@ function exec_ogp_module() { - - - - - - $type_label) - { - $checked = ( isset($addon_type) AND $type_key == $addon_type) ? 'checked' : ''; - echo ''.htmlspecialchars($type_label).'   '; - } - ?> - - @@ -514,7 +496,6 @@ function exec_ogp_module() { } $home_cfg_id = !empty($_GET['home_cfg_id']) && (int)$_GET['home_cfg_id'] > 0 ? (int)$_GET['home_cfg_id'] : 0; - // Validate the requested addon_type against the full category map so new types are accepted. $addon_type = !empty($_GET['addon_type']) && in_array($_GET['addon_type'], $addon_types) ? $_GET['addon_type'] : ""; $group_id = isset($_GET['group_id']) && is_numeric($_GET['group_id']) ? (int)$_GET['group_id'] : 0; diff --git a/Panel/modules/addonsmanager/server_content_categories.php b/Panel/modules/addonsmanager/server_content_categories.php index 51719ce8..57b0af55 100644 --- a/Panel/modules/addonsmanager/server_content_categories.php +++ b/Panel/modules/addonsmanager/server_content_categories.php @@ -40,17 +40,10 @@ function get_server_content_categories() { return array( - // ── Original types (must remain for backward compatibility) ────────── - 'plugin' => 'Plugins / Mods', - 'mappack' => 'Map Packs', - 'config' => 'Config Packs', - - // ── Extended types (require addon_type VARCHAR(32) – db_version 2) ── - 'version' => 'Server Versions', // e.g. Minecraft jar switcher - 'modpack' => 'Modpacks', // e.g. CurseForge / ATLauncher packs - 'workshop' => 'Workshop Content', // Steam Workshop item bundles - 'script' => 'Scripted Installer', // Admin-defined install-only scripts - 'profile' => 'Server Profiles', // Full profile: configs + mods + scripts + 'file_download' => 'File Download / Archive', + 'workshop_item' => 'Steam Workshop Item', + 'config_edit' => 'Config Edit', + 'scripted_installer' => 'Scripted Installer', ); } @@ -80,3 +73,34 @@ function get_server_content_type_keys() { return array_keys(get_server_content_categories()); } + +function scm_get_addon_type_from_install_method($install_method) +{ + $install_method = trim((string)$install_method); + $map = array( + 'download_zip' => 'file_download', + 'steam_workshop' => 'workshop_item', + 'config_edit' => 'config_edit', + 'post_script' => 'scripted_installer', + ); + return isset($map[$install_method]) ? $map[$install_method] : 'file_download'; +} + +function scm_normalize_addon_type($addon_type, $install_method = '') +{ + $addon_type = trim((string)$addon_type); + $categories = get_server_content_categories(); + if (isset($categories[$addon_type])) { + return $addon_type; + } + if ($addon_type === 'workshop') { + return 'workshop_item'; + } + if ($addon_type === 'script') { + return 'scripted_installer'; + } + if ($addon_type === 'config') { + return 'config_edit'; + } + return scm_get_addon_type_from_install_method($install_method); +} diff --git a/Panel/modules/addonsmanager/server_content_helpers.php b/Panel/modules/addonsmanager/server_content_helpers.php index 2f40f4aa..6cf12074 100644 --- a/Panel/modules/addonsmanager/server_content_helpers.php +++ b/Panel/modules/addonsmanager/server_content_helpers.php @@ -255,55 +255,53 @@ function scm_get_cache_mode($db) function scm_get_install_methods() { return array( - 'download_zip' => 'Compressed file download', - 'download_file' => 'Direct file download', - 'steam_workshop' => 'Steam Workshop item', - 'post_script' => 'Script/action only', - 'config_edit' => 'Config edit only', - 'create_folder' => 'Folder/create path only', + 'download_zip' => 'File Download / Archive', + 'steam_workshop' => 'Steam Workshop Item', + 'config_edit' => 'Config Edit', + 'post_script' => 'Scripted Installer', ); } function scm_get_install_method_help_text() { return array( - 'download_zip' => 'Downloads and extracts an archive into the target path.', - 'download_file' => 'Downloads a single file to the target path without extraction.', - 'steam_workshop' => 'Downloads/updates a Steam Workshop item and applies it to the server path.', - 'post_script' => 'Runs only the post-install script/action body (no download).', - 'config_edit' => 'Applies config edit rules to a target config file/path.', - 'create_folder' => 'Creates the target directory path only.', + 'download_zip' => 'Downloads an archive or file from URL; extract path is optional.', + 'steam_workshop' => 'Installs a Steam Workshop item by Workshop ID without requiring URL.', + 'config_edit' => 'Applies config edits to the target file/path without requiring URL.', + 'post_script' => 'Runs an installer script/action body without requiring URL.', ); } function scm_get_install_method_required_fields() { return array( - 'download_zip' => array('url', 'path'), - 'download_file' => array('url', 'path'), - 'steam_workshop' => array('workshop_item_id', 'target_path_template'), + 'download_zip' => array('url'), + 'steam_workshop' => array('workshop_item_id'), 'post_script' => array('post_script'), 'config_edit' => array('path', 'config_edit_rule'), - 'create_folder' => array('path'), ); } function scm_get_install_method_validation_errors() { return array( - 'url' => 'Please enter a download URL.', - 'workshop_item_id' => 'Please enter a Workshop ID.', - 'target_path_template' => 'Please select a target install path.', - 'post_script' => 'Please enter a script/action body.', - 'config_edit_rule' => 'Please enter a config edit rule.', - 'path' => 'Please select a target install path.', + 'download_zip' => 'Please enter a download URL.', + 'steam_workshop' => 'Please enter a Workshop ID.', + 'config_edit' => 'Please enter the config target and edit action.', + 'post_script' => 'Please enter the installer script/action.', ); } function scm_get_install_method_default($value = '') { - $methods = scm_get_install_methods(); $value = trim((string)$value); + if ($value === 'download_file') { + $value = 'download_zip'; + } + if ($value === 'create_folder') { + $value = 'config_edit'; + } + $methods = scm_get_install_methods(); return isset($methods[$value]) ? $value : 'download_zip'; } @@ -316,10 +314,29 @@ function scm_validate_install_method_payload($install_method, array $payload, &$ $message = 'Invalid install/content type selected.'; return false; } + if ($install_method === 'config_edit') { + $path = isset($payload['path']) ? trim((string)$payload['path']) : ''; + $rule = isset($payload['config_edit_rule']) ? trim((string)$payload['config_edit_rule']) : ''; + if ($path === '' || $rule === '') { + $message = $errors['config_edit']; + return false; + } + $message = ''; + return true; + } + if ($install_method === 'post_script') { + $script = isset($payload['post_script']) ? trim((string)$payload['post_script']) : ''; + if ($script === '') { + $message = $errors['post_script']; + return false; + } + $message = ''; + return true; + } foreach ($required[$install_method] as $field) { $value = isset($payload[$field]) ? trim((string)$payload[$field]) : ''; if ($value === '') { - $message = isset($errors[$field]) ? $errors[$field] : 'Missing required field.'; + $message = isset($errors[$install_method]) ? $errors[$install_method] : 'Missing required field.'; return false; } } diff --git a/Panel/modules/addonsmanager/user_addons.php b/Panel/modules/addonsmanager/user_addons.php index 77b1c2a6..6eb36a47 100644 --- a/Panel/modules/addonsmanager/user_addons.php +++ b/Panel/modules/addonsmanager/user_addons.php @@ -60,24 +60,6 @@ function exec_ogp_module() { foreach ((array)$categories as $type_key => $type_label) { - if ($type_key === 'workshop') - { - $workshop_count = scm_get_workshop_saved_count($db, (int)$home_id); - if ($printed_any_cell) - echo "\n"; - else - echo "\n"; - $printed_any_cell = true; - echo "" . - "Workshop Content (" . (int)$workshop_count . ")" . - "\n"; - continue; - } - $items = $db->resultQuery( "SELECT DISTINCT addon_id, name, game_name " . "FROM OGP_DB_PREFIXaddons " . diff --git a/Panel/modules/administration/panel_update.php b/Panel/modules/administration/panel_update.php index 808efe72..ddc46849 100644 --- a/Panel/modules/administration/panel_update.php +++ b/Panel/modules/administration/panel_update.php @@ -379,19 +379,41 @@ return ['success' => true, 'file' => $archive_file]; function gsp_backup_apache_configs($backup_dir) { -$apache_source = '/etc/apache2/sites-available'; -if (!is_dir($apache_source)) { -return ['success' => false, 'error' => 'Apache sites-available path not found: ' . $apache_source]; +$available_source = '/etc/apache2/sites-available'; +$enabled_source = '/etc/apache2/sites-enabled'; +if (!is_dir($available_source)) { +return ['success' => false, 'error' => 'Apache sites-available path not found: ' . $available_source]; } -$dest = $backup_dir . '/apache-sites-available'; -if (!@mkdir($dest, 0755, true) && !is_dir($dest)) { +$dest = $backup_dir . '/apache-configs'; +$dest_available = $dest . '/sites-available'; +$dest_enabled = $dest . '/sites-enabled'; +if (!@mkdir($dest_available, 0755, true) && !is_dir($dest_available)) { return ['success' => false, 'error' => 'Cannot create apache backup directory.']; } -$files = glob($apache_source . '/*.conf') ?: []; -foreach ($files as $file) { -@copy($file, $dest . '/' . basename($file)); +if (!@mkdir($dest_enabled, 0755, true) && !is_dir($dest_enabled)) { +return ['success' => false, 'error' => 'Cannot create apache enabled backup directory.']; } -return ['success' => true, 'path' => $dest, 'count' => count($files)]; +$count = 0; +foreach ((glob($available_source . '/*.conf') ?: []) as $file) { +if (@copy($file, $dest_available . '/' . basename($file))) { +$count++; +} +} +if (is_dir($enabled_source)) { +foreach ((glob($enabled_source . '/*') ?: []) as $file) { +$dst = $dest_enabled . '/' . basename($file); +if (is_link($file)) { +$target = @readlink($file); +if ($target !== false) { +@symlink($target, $dst); +$count++; +} +} elseif (is_file($file) && @copy($file, $dst)) { +$count++; +} +} +} +return ['success' => true, 'path' => $dest, 'count' => $count]; } function gsp_prune_old_backups($max_backups = 5) @@ -593,6 +615,7 @@ function gsp_copy_tree($src_root, $dst_root, $base_rel = '') { $copied = 0; $skipped = []; +$copied_files = []; $source = rtrim($src_root, '/'); $iter = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($source, RecursiveDirectoryIterator::SKIP_DOTS), @@ -613,9 +636,12 @@ continue; } if (gsp_copy_file($item->getPathname(), $dst)) { $copied++; +if (count($copied_files) < 200) { +$copied_files[] = $rel; } } -return ['copied' => $copied, 'skipped' => $skipped]; +} +return ['copied' => $copied, 'skipped' => $skipped, 'copied_files' => $copied_files]; } function gsp_updater_watch_list() @@ -694,7 +720,11 @@ function gsp_apply_layout_sync($source_root) $top_level = scandir($source_root); $skip = ['.', '..', '.git', '.github', '.gitignore', '.vscode']; $copied = 0; +$panel_copied = 0; +$website_copied = 0; $skipped = []; +$copied_files = []; +gsp_update_log('Layout sync source mapping: ' . $source_root . '/Panel => ' . GSP_PANEL_DIR . ' ; ' . $source_root . '/Website => ' . GSP_WEBSITE_DIR); foreach ((array)$top_level as $entry) { if (in_array($entry, $skip, true)) { continue; @@ -713,19 +743,32 @@ continue; } if (gsp_copy_file($src, $dst)) { $copied++; +if (count($copied_files) < 200) { +$copied_files[] = $rel; +} } continue; } if (is_dir($src)) { $part = gsp_copy_tree($src, GSP_ROOT_DIR, $entry); $copied += $part['copied']; +$copied_files = array_merge($copied_files, array_slice((array)$part['copied_files'], 0, max(0, 200 - count($copied_files)))); +if ($entry === 'Panel') { +$panel_copied += $part['copied']; +} +if ($entry === 'Website') { +$website_copied += $part['copied']; +} $skipped = array_merge($skipped, $part['skipped']); } } return [ 'success' => true, 'files_copied' => $copied, +'panel_files_copied' => $panel_copied, +'website_files_copied' => $website_copied, 'skipped' => array_values(array_unique($skipped)), +'copied_files' => array_slice(array_values(array_unique($copied_files)), 0, 200), ]; } @@ -792,13 +835,27 @@ if (!$sync['success']) { return $sync; } 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'], ]; } @@ -885,6 +942,9 @@ gsp_update_log("Update to {$ref} (type={$update_type}) complete"); return [ 'success' => true, 'files_copied' => $apply['files_copied'], +'panel_files_copied' => isset($apply['panel_files_copied']) ? $apply['panel_files_copied'] : 0, +'website_files_copied' => isset($apply['website_files_copied']) ? $apply['website_files_copied'] : 0, +'copied_files' => isset($apply['copied_files']) ? $apply['copied_files'] : [], 'backup_dir' => $backup['backup_dir'], 'preserved' => $apply['preserved'], 'patches' => $apply['patches'], @@ -1016,8 +1076,9 @@ if (!$db_restore['success']) { gsp_update_log('Revert warning: ' . $db_restore['error']); } -if ($restore_apache && is_dir($backup_dir . '/apache-sites-available')) { -$apache_restore = gsp_restore_apache_backup($backup_dir . '/apache-sites-available', true); +if ($restore_apache && (is_dir($backup_dir . '/apache-configs') || is_dir($backup_dir . '/apache-sites-available'))) { +$apache_backup_dir = is_dir($backup_dir . '/apache-configs') ? ($backup_dir . '/apache-configs') : ($backup_dir . '/apache-sites-available'); +$apache_restore = gsp_restore_apache_backup($apache_backup_dir, true); if (!$apache_restore['success']) { gsp_update_log('Revert apache restore warning: ' . $apache_restore['error']); } @@ -1033,6 +1094,46 @@ gsp_update_log('Revert complete: ' . $backup_ts); return ['success' => true, 'files_restored' => 0]; } +function gsp_get_apache_vhost_target_path($filename) +{ +$name = strtolower((string)$filename); +if (strpos($name, 'panel.') === 0) { +return GSP_PANEL_DIR; +} +if (strpos($name, 'gameservers.world') !== false) { +return GSP_WEBSITE_DIR; +} +return null; +} + +function gsp_is_apache_stale_path($path) +{ +$path = trim((string)$path); +$stale = [ +'/var/www/html/panel', +'/var/www/html/GSP/Panel/GSP/Panel', +'/var/www/html/GSP/Panel/modules/billing', +]; +if (in_array($path, $stale, true)) { +return true; +} +return (strpos($path, '/var/www/html/panel/') === 0 || strpos($path, '/var/www/html/GSP/Panel/modules/billing/') === 0); +} + +function gsp_parse_apache_cert_error_line($line) +{ +$line = trim((string)$line); +if (preg_match("/(SSLCertificate(?:File|KeyFile)):\\s*file '([^']+)' (does not exist(?: or is empty)?|is empty)/i", $line, $m)) { +return [ +'directive' => $m[1], +'path' => $m[2], +'reason' => $m[3], +'line' => $line, +]; +} +return null; +} + function gsp_scan_apache_configs() { $base = '/etc/apache2/sites-available'; @@ -1042,6 +1143,9 @@ $result = [ 'base' => $base, 'files' => [], 'issues' => [], +'stale_issues' => [], +'ssl_issues' => [], +'planned_replacements' => [], 'recommendations' => [], ]; if (!$result['available']) { @@ -1049,64 +1153,156 @@ $result['success'] = false; $result['issues'][] = 'Apache sites-available directory not found.'; return $result; } -$stale = [ -'/var/www/html/panel', -'/var/www/html/GSP/Panel/GSP/Panel', -'/var/www/html/GSP/Panel/modules/billing', -]; $files = glob($base . '/*.conf') ?: []; foreach ($files as $file) { $lines = @file($file, FILE_IGNORE_NEW_LINES); if (!is_array($lines)) { continue; } -$file_info = ['file' => $file, 'document_roots' => [], 'directories' => [], 'stale_hits' => []]; -foreach ($lines as $line) { +$vhost = basename($file); +$target = gsp_get_apache_vhost_target_path($vhost); +$file_info = [ +'file' => $file, +'vhost' => $vhost, +'target' => $target, +'document_roots' => [], +'directories' => [], +'stale_hits' => [], +'ssl_hits' => [], +]; +foreach ($lines as $line_number => $line) { if (preg_match('/^\s*DocumentRoot\s+(.+)$/i', $line, $m)) { $path = trim($m[1], "\"' "); $file_info['document_roots'][] = $path; +if ($target !== null && $path !== $target && gsp_is_apache_stale_path($path)) { +$msg = $vhost . ' stale DocumentRoot: ' . $path . ' -> ' . $target; +$result['stale_issues'][] = $msg; +$result['issues'][] = $msg; +$result['planned_replacements'][] = ['vhost' => $vhost, 'directive' => 'DocumentRoot', 'from' => $path, 'to' => $target]; +$file_info['stale_hits'][] = $path; +} } if (preg_match('/^\s*/i', $line, $m)) { $path = trim($m[1], "\"' "); $file_info['directories'][] = $path; +if ($target !== null && $path !== $target && gsp_is_apache_stale_path($path)) { +$msg = $vhost . ' stale : ' . $path . ' -> ' . $target; +$result['stale_issues'][] = $msg; +$result['issues'][] = $msg; +$result['planned_replacements'][] = ['vhost' => $vhost, 'directive' => '', 'from' => $path, 'to' => $target]; +$file_info['stale_hits'][] = $path; } -foreach ($stale as $stalePath) { -if (strpos($line, $stalePath) !== false) { -$file_info['stale_hits'][] = $stalePath; -$result['issues'][] = basename($file) . ' contains stale path: ' . $stalePath; +} +if (preg_match('/^\s*(SSLCertificate(?:File|KeyFile))\s+(.+)$/i', $line, $m)) { +$directive = $m[1]; +$path = trim($m[2], "\"' "); +if ($path !== '' && (!file_exists($path) || @filesize($path) === 0)) { +$reason = !file_exists($path) ? 'missing' : 'empty'; +$msg = $vhost . ' ' . $directive . ' ' . $path . ' is ' . $reason; +$issue = ['vhost' => $vhost, 'directive' => $directive, 'path' => $path, 'reason' => $reason, 'line' => ($line_number + 1), 'message' => $msg]; +$result['ssl_issues'][] = $issue; +$result['issues'][] = $msg; +$file_info['ssl_hits'][] = $issue; } } } -if (!empty($file_info['stale_hits'])) { -if (stripos($file, 'gameservers.world') !== false) { -$result['recommendations'][] = basename($file) . ' should target ' . GSP_WEBSITE_DIR; -} else { -$result['recommendations'][] = basename($file) . ' should target ' . GSP_PANEL_DIR; -} +if ($target !== null) { +$result['recommendations'][] = $vhost . ' should target ' . $target; } $result['files'][] = $file_info; } +$result['stale_issues'] = array_values(array_unique($result['stale_issues'])); +$result['issues'] = array_values(array_unique($result['issues'])); return $result; } +function gsp_apache_configtest_only_cert_failures($configtest_output) +{ +$lines = preg_split('/\r\n|\r|\n/', (string)$configtest_output); +$seen = 0; +foreach ((array)$lines as $line) { +$line = trim((string)$line); +if ($line === '') { +continue; +} +if (stripos($line, 'Syntax OK') !== false) { +continue; +} +if (preg_match('/^AH[0-9]+:\s+/i', $line) && stripos($line, 'Could not reliably determine') !== false) { +continue; +} +if (gsp_parse_apache_cert_error_line($line) !== null) { +$seen++; +continue; +} +return false; +} +return $seen > 0; +} + +function gsp_extract_apache_configtest_cert_issues($configtest_output) +{ +$issues = []; +$lines = preg_split('/\r\n|\r|\n/', (string)$configtest_output); +foreach ((array)$lines as $line) { +$parsed = gsp_parse_apache_cert_error_line($line); +if ($parsed !== null) { +$issues[] = $parsed; +} +} +return $issues; +} + function gsp_restore_apache_backup($backup_dir, $reload_apache) { -$target = '/etc/apache2/sites-available'; +$available_target = '/etc/apache2/sites-available'; +$enabled_target = '/etc/apache2/sites-enabled'; if (!is_dir($backup_dir)) { return ['success' => false, 'error' => 'Apache backup folder not found.']; } -$files = glob($backup_dir . '/*.conf') ?: []; -foreach ($files as $file) { -@copy($file, $target . '/' . basename($file)); +$available_backup = is_dir($backup_dir . '/sites-available') ? $backup_dir . '/sites-available' : $backup_dir; +$enabled_backup = $backup_dir . '/sites-enabled'; +$restored = 0; +foreach ((glob($available_backup . '/*.conf') ?: []) as $file) { +if (@copy($file, $available_target . '/' . basename($file))) { +$restored++; +} +} +if (is_dir($enabled_backup)) { +foreach ((glob($enabled_backup . '/*') ?: []) as $file) { +$dst = $enabled_target . '/' . basename($file); +if (is_link($dst) || is_file($dst)) { +@unlink($dst); +} +if (is_link($file)) { +$link_target = @readlink($file); +if ($link_target !== false) { +@symlink($link_target, $dst); +$restored++; +} +} elseif (is_file($file) && @copy($file, $dst)) { +$restored++; +} +} } $test = gsp_apache_configtest(); if (!$test['success']) { +if (gsp_apache_configtest_only_cert_failures($test['output'])) { +return [ +'success' => true, +'restored' => $restored, +'configtest' => $test, +'warnings' => ['Apache config restore completed, but SSL certificate file(s) are missing.'], +'ssl_issues' => gsp_extract_apache_configtest_cert_issues($test['output']), +]; +} return ['success' => false, 'error' => 'apache2ctl configtest failed after restore: ' . $test['output']]; } +$reload = ['success' => true, 'output' => 'Apache reload skipped']; if ($reload_apache) { -gsp_apache_reload(); +$reload = gsp_apache_reload(); } -return ['success' => true, 'restored' => count($files)]; +return ['success' => true, 'restored' => $restored, 'configtest' => $test, 'reload' => $reload]; } function gsp_apache_configtest() @@ -1136,6 +1332,31 @@ 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.']; @@ -1148,18 +1369,34 @@ return ['success' => false, 'error' => 'Could not create backup before apache fi $base = '/etc/apache2/sites-available'; $files = glob($base . '/*.conf') ?: []; -$replace = [ -'/var/www/html/panel' => GSP_PANEL_DIR, -'/var/www/html/GSP/Panel/GSP/Panel' => GSP_PANEL_DIR, -'/var/www/html/GSP/Panel/modules/billing' => GSP_WEBSITE_DIR, -]; $changed = []; +$planned = []; foreach ($files as $file) { $orig = @file_get_contents($file); if ($orig === false) { continue; } -$new = strtr($orig, $replace); +$vhost = basename($file); +$target = gsp_get_apache_vhost_target_path($vhost); +if ($target === null) { +continue; +} +$new = preg_replace_callback('/(^\s*DocumentRoot\s+)(["\']?)([^"\']+)(\2\s*$)/mi', function ($m) use ($target, $vhost, &$planned) { +$current = trim((string)$m[3]); +if ($current === $target || !gsp_is_apache_stale_path($current)) { +return $m[0]; +} +$planned[] = ['vhost' => $vhost, 'directive' => 'DocumentRoot', 'from' => $current, 'to' => $target]; +return $m[1] . $m[2] . $target . $m[2]; +}, $orig); +$new = preg_replace_callback('/(^\s*]+)(\2\s*>)/mi', function ($m) use ($target, $vhost, &$planned) { +$current = trim((string)$m[3]); +if ($current === $target || !gsp_is_apache_stale_path($current)) { +return $m[0]; +} +$planned[] = ['vhost' => $vhost, 'directive' => '', 'from' => $current, 'to' => $target]; +return $m[1] . $m[2] . $target . $m[2] . '>'; +}, $new); if ($new !== $orig) { @file_put_contents($file, $new); $changed[] = $file; @@ -1168,13 +1405,27 @@ $changed[] = $file; $test = gsp_apache_configtest(); if (!$test['success']) { -$restore = gsp_restore_apache_backup($backup['backup_dir'] . '/apache-sites-available', false); +$cert_only = gsp_apache_configtest_only_cert_failures($test['output']); +if (!$cert_only) { +$restore = gsp_restore_apache_backup($backup['backup_dir'] . '/apache-configs', false); return [ 'success' => false, 'error' => 'apache2ctl configtest failed; restored backup. Output: ' . $test['output'] . ($restore['success'] ? '' : (' | restore failed: ' . $restore['error'])), ]; } +gsp_update_log('Apache path fix completed but SSL certificates are missing: ' . $test['output']); +return [ +'success' => true, +'warning' => 'Apache paths fixed, but SSL certificate is missing.', +'changed_files' => $changed, +'backup_dir' => $backup['backup_dir'], +'configtest' => $test, +'planned_replacements' => $planned, +'ssl_issues' => gsp_extract_apache_configtest_cert_issues($test['output']), +'reload' => ['success' => false, 'output' => 'Apache reload skipped because SSL certificate files are missing.'], +]; +} $reload = ['success' => true, 'output' => 'Apache reload skipped']; if ($reload_apache) { @@ -1182,12 +1433,19 @@ $reload = gsp_apache_reload(); } gsp_update_log('Apache path fix changed ' . count($changed) . ' file(s).'); +if (!empty($planned)) { +$samples = array_slice($planned, 0, 20); +foreach ($samples as $entry) { +gsp_update_log('Apache replacement planned/applied: ' . $entry['vhost'] . ' ' . $entry['directive'] . ' ' . $entry['from'] . ' => ' . $entry['to']); +} +} return [ 'success' => true, 'changed_files' => $changed, 'backup_dir' => $backup['backup_dir'], 'configtest' => $test, 'reload' => $reload, +'planned_replacements' => $planned, ]; } @@ -1293,10 +1551,26 @@ print_failure('Patch application failed: ' . htmlspecialchars($run['error'])); } elseif ($action === 'fix_apache') { $apache_fix = gsp_fix_apache_paths(true, true); if ($apache_fix['success']) { +if (!empty($apache_fix['warning'])) { +print_success(htmlspecialchars($apache_fix['warning']) . ' Updated files: ' . intval(count($apache_fix['changed_files'])) . '.'); +echo "

Renew SSL certificate: certbot --apache -d gameservers.world -d www.gameservers.world

\n"; +} else { print_success('Apache paths fixed successfully. Updated files: ' . intval(count($apache_fix['changed_files'])) . '.'); +} +if (!empty($apache_fix['configtest']['output'])) { +echo "

apache2ctl configtest output:

" . htmlspecialchars($apache_fix['configtest']['output']) . "

\n"; +} } else { print_failure('Apache path fix failed: ' . htmlspecialchars($apache_fix['error'])); } +} elseif ($action === 'disable_ssl_vhost') { +$vhost = isset($_POST['gsp_ssl_vhost']) ? trim($_POST['gsp_ssl_vhost']) : ''; +$disable = gsp_disable_ssl_vhost($vhost, true); +if ($disable['success']) { +print_success(htmlspecialchars(isset($disable['message']) ? $disable['message'] : 'SSL vhost disabled.')); +} else { +print_failure('Disable SSL vhost failed: ' . htmlspecialchars($disable['error'])); +} } elseif ($action === 'backup_only') { $result = gsp_create_full_backup('backup-only', 'manual', false); if ($result['success']) { @@ -1315,7 +1589,7 @@ print_success('Updater files changed and were updated first. Restarting update w $auto_restart_payload = ['action' => 'update_release', 'nonce' => $result['restart_nonce'], 'version' => $version]; } elseif ($result['success']) { print_success('Panel updated to release ' . htmlspecialchars($version) . '. ' -. intval($result['files_copied']) . ' file(s) copied.'); +. intval($result['files_copied']) . ' file(s) copied (Panel: ' . intval($result['panel_files_copied']) . ', Website: ' . intval($result['website_files_copied']) . ').'); } else { print_failure('Update failed: ' . htmlspecialchars($result['error'])); } @@ -1327,7 +1601,7 @@ print_success('Updater files changed and were updated first. Restarting stable u $auto_restart_payload = ['action' => 'update_stable', 'nonce' => $result['restart_nonce']]; } elseif ($result['success']) { print_success('Panel updated to GitHub Stable (' . htmlspecialchars($stable_branch) . '). ' -. intval($result['files_copied']) . ' file(s) copied.'); +. intval($result['files_copied']) . ' file(s) copied (Panel: ' . intval($result['panel_files_copied']) . ', Website: ' . intval($result['website_files_copied']) . ').'); } else { print_failure('Update failed: ' . htmlspecialchars($result['error'])); } @@ -1338,7 +1612,7 @@ print_success('Updater files changed and were updated first. Restarting unstable $auto_restart_payload = ['action' => 'update_unstable', 'nonce' => $result['restart_nonce']]; } elseif ($result['success']) { print_success('Panel updated to GitHub Unstable (' . htmlspecialchars($unstable_branch) . '). ' -. intval($result['files_copied']) . ' file(s) copied.'); +. intval($result['files_copied']) . ' file(s) copied (Panel: ' . intval($result['panel_files_copied']) . ', Website: ' . intval($result['website_files_copied']) . ').'); } else { print_failure('Update failed: ' . htmlspecialchars($result['error'])); } @@ -1438,12 +1712,40 @@ echo "

Apache Configuration Status

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

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

\n"; +if (!empty($apache_scan_result['stale_issues'])) { +echo "

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

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

Planned replacements:

"; +foreach ((array)$apache_scan_result['planned_replacements'] as $plan_row) { +echo ""; +} +echo "
VhostDirectiveCurrentReplacement
" . htmlspecialchars($plan_row['vhost']) . "" . htmlspecialchars($plan_row['directive']) . "" . htmlspecialchars($plan_row['from']) . "" . htmlspecialchars($plan_row['to']) . "
"; +} +if (!empty($apache_scan_result['ssl_issues'])) { +echo "

SSL certificate issues:
"; +foreach ((array)$apache_scan_result['ssl_issues'] as $ssl_issue) { +echo htmlspecialchars($ssl_issue['vhost'] . ' ' . $ssl_issue['directive'] . ': ' . $ssl_issue['path'] . ' (' . $ssl_issue['reason'] . ')') . "
"; +} +echo "

\n"; +echo "

Renew certificate command: certbot --apache -d gameservers.world -d www.gameservers.world

"; +foreach ((array)$apache_scan_result['ssl_issues'] as $ssl_issue) { +$vhost = basename((string)$ssl_issue['vhost']); +if (strpos($vhost, '-ssl.conf') === false) { +continue; +} +echo "
"; +echo ""; +echo ""; +echo ""; +echo ""; +echo "
"; +} } echo "
\n"; echo "\n";