Merge pull request #161 from GameServerPanel/copilot/fix-update-panel-apache-paths

This commit is contained in:
Frank Harris 2026-05-19 14:35:26 -05:00 committed by GitHub
commit c56cce73fe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 449 additions and 152 deletions

View file

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

View file

@ -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).");
?>

View file

@ -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;
}
?>
<?php
$addon_type_lang_key = "server_content_".$addon_type;

View file

@ -47,11 +47,12 @@ function exec_ogp_module() {
$fields['name'] = isset($_POST['name']) ? trim((string)$_POST['name']) : '';
$fields['url'] = isset($_POST['url']) ? trim((string)$_POST['url']) : '';
$fields['path'] = isset($_POST['path']) ? trim((string)$_POST['path']) : '';
$fields['addon_type'] = isset($_POST['addon_type']) ? trim((string)$_POST['addon_type']) : '';
$fields['addon_type'] = '';
$fields['home_cfg_id'] = isset($_POST['home_cfg_id']) ? (int)$_POST['home_cfg_id'] : 0;
$fields['post_script'] = isset($_POST['post_script']) ? trim((string)$_POST['post_script']) : '';
$fields['group_id'] = isset($_POST['group_id']) ? (int)$_POST['group_id'] : 0;
$fields['install_method'] = in_array($_POST['install_method'], $valid_install_methods) ? $_POST['install_method'] : 'download_zip';
$posted_install_method = isset($_POST['install_method']) ? $_POST['install_method'] : '';
$fields['install_method'] = in_array($posted_install_method, $valid_install_methods) ? $posted_install_method : 'download_zip';
$fields['content_version'] = isset($_POST['content_version']) ? $_POST['content_version'] : '';
$fields['requires_stop'] = !empty($_POST['requires_stop']) ? 1 : 0;
$fields['backup_before_install'] = !empty($_POST['backup_before_install']) ? 1 : 0;
@ -64,15 +65,12 @@ function exec_ogp_module() {
$fields['optional_folder_name']= isset($_POST['optional_folder_name']) ? trim((string)$_POST['optional_folder_name']) : '';
$fields['config_edit_rule'] = isset($_POST['config_edit_rule']) ? trim((string)$_POST['config_edit_rule']) : '';
$fields['launch_param_additions'] = isset($_POST['launch_param_additions']) ? trim((string)$_POST['launch_param_additions']) : '';
$fields['addon_type'] = scm_get_addon_type_from_install_method($fields['install_method']);
if ($fields['name'] === '')
{
print_failure(get_lang("fill_the_addon_name"));
}
elseif (empty($fields['addon_type']) || !in_array($fields['addon_type'], $addon_types))
{
print_failure(get_lang("select_an_addon_type"));
}
elseif (empty($fields['home_cfg_id']))
{
print_failure(get_lang("select_a_game_type"));
@ -135,7 +133,7 @@ function exec_ogp_module() {
$path = isset($addon_info['path']) ? $addon_info['path'] : "";
$post_script = isset($addon_info['post_script']) ? $addon_info['post_script'] : "";
$home_cfg_id = isset($addon_info['home_cfg_id']) ? $addon_info['home_cfg_id'] : "";
$addon_type = isset($addon_info['addon_type']) ? $addon_info['addon_type'] : "";
$addon_type = scm_normalize_addon_type(isset($addon_info['addon_type']) ? $addon_info['addon_type'] : "", $install_method);
$group_id = isset($addon_info['group_id']) ? $addon_info['group_id'] : "";
$install_method = isset($addon_info['install_method']) ? $addon_info['install_method'] : "download_zip";
$content_version = isset($addon_info['content_version']) ? $addon_info['content_version'] : "";
@ -208,10 +206,10 @@ function exec_ogp_module() {
</tr>
<tr id="scm-row-workshop-app-id">
<td align="right">
<b>Workshop App ID</b>
<b>Game Compatibility (Workshop App ID)</b>
</td>
<td align="left">
<input type="text" value="<?php echo htmlspecialchars($workshop_app_id, ENT_QUOTES, 'UTF-8'); ?>" name="workshop_app_id" size="85" placeholder="Optional override, e.g. 221100" />
<input type="text" value="<?php echo htmlspecialchars($workshop_app_id, ENT_QUOTES, 'UTF-8'); ?>" name="workshop_app_id" size="85" placeholder="Optional App ID override, e.g. 221100" />
</td>
</tr>
<tr id="scm-row-target-path-template">
@ -302,22 +300,6 @@ function exec_ogp_module() {
</select>
</td>
</tr>
<tr>
<td align="right">
<b><?php print_lang('type'); ?></b>
</td>
<td align="left">
<?php
// Render a radio button for every registered content type.
// New types automatically appear here once added to server_content_categories.php.
foreach ((array)$addon_type_labels as $type_key => $type_label)
{
$checked = ( isset($addon_type) AND $type_key == $addon_type) ? 'checked' : '';
echo '<input type="radio" name="addon_type" value="'.htmlspecialchars($type_key).'" '.$checked.'>'.htmlspecialchars($type_label).' &nbsp; ';
}
?>
</td>
</tr>
<tr>
<td align="right">
<b><?php print_lang('show_to_group'); ?></b>
@ -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;

View file

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

View file

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

View file

@ -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 "</td><td>\n";
else
echo "<td>\n";
$printed_any_cell = true;
echo "<a href='?m=addonsmanager&amp;p=workshop_content" .
"&amp;home_id=" . (int)$home_id .
"&amp;mod_id=" . (int)$mod_id .
"&amp;ip=" . htmlspecialchars($ip) .
"&amp;port=" . htmlspecialchars($port) . "'>" .
"Workshop Content (" . (int)$workshop_count . ")" .
"</a>\n";
continue;
}
$items = $db->resultQuery(
"SELECT DISTINCT addon_id, name, game_name " .
"FROM OGP_DB_PREFIXaddons " .

View file

@ -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*<Directory\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 <Directory>: ' . $path . ' -> ' . $target;
$result['stale_issues'][] = $msg;
$result['issues'][] = $msg;
$result['planned_replacements'][] = ['vhost' => $vhost, 'directive' => '<Directory>', '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*<Directory\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' => '<Directory>', '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 "<p style='color:#8a6d3b;'><strong>Renew SSL certificate:</strong> <code>certbot --apache -d gameservers.world -d www.gameservers.world</code></p>\n";
} else {
print_success('Apache paths fixed successfully. Updated files: ' . intval(count($apache_fix['changed_files'])) . '.');
}
if (!empty($apache_fix['configtest']['output'])) {
echo "<p><strong>apache2ctl configtest output:</strong><br><pre style='white-space:pre-wrap;max-height:220px;overflow:auto;'>" . htmlspecialchars($apache_fix['configtest']['output']) . "</pre></p>\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 <strong>' . htmlspecialchars($version) . '</strong>. '
. 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 (<strong>' . htmlspecialchars($stable_branch) . '</strong>). '
. 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 (<strong>' . htmlspecialchars($unstable_branch) . '</strong>). '
. 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 "<br><br><h3>Apache Configuration Status</h3>\n";
echo "<table class='center'>\n";
echo "<tr><td><strong>Config Directory:</strong></td><td><code>" . htmlspecialchars($apache_scan_result['base']) . "</code></td></tr>\n";
echo "<tr><td><strong>Configs Found:</strong></td><td>" . intval(count($apache_scan_result['files'])) . "</td></tr>\n";
echo "<tr><td><strong>Stale Path Hits:</strong></td><td>" . intval(count($apache_scan_result['issues'])) . "</td></tr>\n";
echo "<tr><td><strong>Stale Path Hits:</strong></td><td>" . intval(count($apache_scan_result['stale_issues'])) . "</td></tr>\n";
echo "<tr><td><strong>SSL Certificate Issues:</strong></td><td>" . intval(count($apache_scan_result['ssl_issues'])) . "</td></tr>\n";
echo "<tr><td><strong>Recommended Panel Path:</strong></td><td><code>" . htmlspecialchars(GSP_PANEL_DIR) . "</code></td></tr>\n";
echo "<tr><td><strong>Recommended Website Path:</strong></td><td><code>" . htmlspecialchars(GSP_WEBSITE_DIR) . "</code></td></tr>\n";
echo "</table>\n";
if (!empty($apache_scan_result['issues'])) {
echo "<p style='color:#a94442;'><strong>Apache path issues:</strong><br>" . implode('<br>', array_map('htmlspecialchars', array_unique($apache_scan_result['issues']))) . "</p>\n";
if (!empty($apache_scan_result['stale_issues'])) {
echo "<p style='color:#a94442;'><strong>Apache stale path issues:</strong><br>" . implode('<br>', array_map('htmlspecialchars', array_unique($apache_scan_result['stale_issues']))) . "</p>\n";
}
if (!empty($apache_scan_result['planned_replacements'])) {
echo "<p><strong>Planned replacements:</strong></p><table class='center'><tr><th>Vhost</th><th>Directive</th><th>Current</th><th>Replacement</th></tr>";
foreach ((array)$apache_scan_result['planned_replacements'] as $plan_row) {
echo "<tr><td>" . htmlspecialchars($plan_row['vhost']) . "</td><td>" . htmlspecialchars($plan_row['directive']) . "</td><td><code>" . htmlspecialchars($plan_row['from']) . "</code></td><td><code>" . htmlspecialchars($plan_row['to']) . "</code></td></tr>";
}
echo "</table>";
}
if (!empty($apache_scan_result['ssl_issues'])) {
echo "<p style='color:#8a6d3b;'><strong>SSL certificate issues:</strong><br>";
foreach ((array)$apache_scan_result['ssl_issues'] as $ssl_issue) {
echo htmlspecialchars($ssl_issue['vhost'] . ' ' . $ssl_issue['directive'] . ': ' . $ssl_issue['path'] . ' (' . $ssl_issue['reason'] . ')') . "<br>";
}
echo "</p>\n";
echo "<p style='color:#8a6d3b;'><strong>Renew certificate command:</strong> <code>certbot --apache -d gameservers.world -d www.gameservers.world</code></p>";
foreach ((array)$apache_scan_result['ssl_issues'] as $ssl_issue) {
$vhost = basename((string)$ssl_issue['vhost']);
if (strpos($vhost, '-ssl.conf') === false) {
continue;
}
echo "<form method='POST' style='display:inline-block;margin-right:8px;'>";
echo "<input type='hidden' name='gsp_update_action' value='disable_ssl_vhost'>";
echo "<input type='hidden' name='gsp_update_csrf' value='" . htmlspecialchars($csrf_token) . "'>";
echo "<input type='hidden' name='gsp_ssl_vhost' value='" . htmlspecialchars($vhost) . "'>";
echo "<button type='submit' onclick='return confirm(\"Disable broken SSL site " . htmlspecialchars($vhost) . " in sites-enabled?\\n\\nThis keeps path fixes while SSL certs are missing.\");'>Disable Broken SSL Vhost: " . htmlspecialchars($vhost) . "</button>";
echo "</form>";
}
}
echo "<form method='POST'>\n";
echo "<input type='hidden' name='gsp_update_action' value='fix_apache'>\n";