Panel/Panel/modules/addonsmanager/addons_installer.php
copilot-swe-agent[bot] 7a80812fe7
Add Phase 1 Workshop Content flow to addonsmanager
Agent-Logs-Url: https://github.com/GameServerPanel/GSP/sessions/7643e55f-473c-4084-baa0-cf8ae8c9a10a

Co-authored-by: iaretechnician <2749183+iaretechnician@users.noreply.github.com>
2026-05-18 21:40:24 +00:00

387 lines
16 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/*
*
* GSP - Game Server Panel (a heavily customized fork of OGP maintained by WDS)
*
* Server Content Installer (module: addonsmanager, page: addons)
* ─────────────────────────────────────────────────────────────────────────────
* This file handles the actual download+extraction and post-install script
* execution for a Server Content item selected by a user.
*
* CURRENT FLOW:
* 1. User selects a content type (plugin / mappack / config / ...) from
* user_addons.php which links here with addon_type=<type>.
* 2. User picks a specific content item from a dropdown.
* 3. On form submit, state=start is set and start_file_download() is called
* on the remote agent with the configured URL and target path.
* 4. The agent downloads and extracts the archive.
* 5. If a post_script is defined it is run on the agent after extraction.
* 6. The page auto-refreshes (state=refresh) to show download/script progress.
*
* POST-INSTALL SCRIPT REPLACEMENT VARIABLES:
* %home_path% absolute path of the game server home directory
* %home_name% display name of the game server home
* %control_password% RCON / control password for this server instance
* %max_players% maximum player count configured for this mod slot
* %ip% IP address bound to this server instance
* %port% game port bound to this server instance
* %query_port% query/status port (derived from game XML rules)
* %incremental% internal incremental run counter for this mod/home
*
* SECURITY NOTES:
* - Users CANNOT supply arbitrary scripts; only the admin-defined post_script
* is executed. Users only pick from the approved list.
* - Paths are passed to the agent which is responsible for enforcing that
* all paths stay inside the assigned home directory.
* - TODO (next phase): add explicit server-side path validation before
* sending the command to the agent to block ../ traversal at the panel.
*
* ─── FUTURE WORK (TODO next phase) ────────────────────────────────────────
* The items below are intentionally NOT implemented here yet. They are
* documented so the next contributor knows exactly where to add them.
*
* TODO: requires_stop flag
* If the content item sets requires_stop=1, stop the server before
* initiating the download. Poll is_server_running() and abort if it
* cannot be stopped within a timeout.
*
* TODO: backup_before_install flag
* If backup_before_install=1, call the agent's backup function or
* compress the target path into a timestamped .tar.gz before extraction.
*
* TODO: restart_after_install flag
* If restart_after_install=1, trigger a server start after a successful
* install (i.e. after post_script completes with exit code 0).
*
* TODO: install_method field
* Current method is always 'download_zip'. Future methods:
* 'download_file' single-file download, no extraction
* 'post_script' run only the post_script, no download
* 'steam_workshop' pass workshop item IDs to the agent's workshop helper
* 'minecraft_jar' download a Minecraft server jar + update start script
* 'profile_copy' copy a profile directory tree into the server home
*
* TODO: content_version field
* Store the installed version tag so the UI can display "installed: 1.21.1"
* and detect whether an update is available.
*
* TODO: safe script templates
* Provide a set of admin-approved script templates so admins do not have to
* write raw bash from scratch. Templates are stored in the DB and referenced
* by content items.
*
* TODO: install history / logging
* Write a row to a new install_history table (or log file) each time a
* content item is installed:
* home_id, addon_id, installed_by (user_id), installed_at, result, log_output
*
* TODO: user-friendly status output
* Replace the raw progress-bar with a card-style status block showing:
* content item name, version, download progress, script output, final status.
*
* TODO: Steam Workshop integration
* When install_method='steam_workshop', pass the workshop item ID list to
* the agent. See SERVER_CONTENT_ROADMAP.md Part 6 for the full design.
*
* TODO: Minecraft jar / version switching
* When install_method='minecraft_jar', download the jar from Mojang/Paper/
* Purpur/Fabric API, place it at the configured server path, and patch the
* startup command line. See SERVER_CONTENT_ROADMAP.md Part 7.
* ─────────────────────────────────────────────────────────────────────────────
*/
function do_progress($kbytes,$totalsize)
{
$mbytes = round($kbytes / 1024, 2);
if($kbytes > 0)
{
$pct = round(( $kbytes / $totalsize ) * 100, 2);
}
else
{
$pct = "-";
}
#echo "Percent is $pct";
return "$totalsize;$mbytes;$pct";
}
require_once("includes/lib_remote.php");
require_once("modules/config_games/server_config_parser.php");
require_once("protocol/lgsl/lgsl_protocol.php");
// Central category map — all valid addon_type values and their labels.
require_once(dirname(__FILE__) . '/server_content_categories.php');
require_once(dirname(__FILE__) . '/server_content_helpers.php');
function exec_ogp_module() {
global $db,$view;
$home_id = $_REQUEST['home_id'];
$mod_id = $_REQUEST['mod_id'];
$ip = $_REQUEST['ip'];
$port = $_REQUEST['port'];
$user_id = $_SESSION['user_id'];
$isAdmin = $db->isAdmin( $_SESSION['user_id'] );
$query_groups = "";
if($isAdmin)
$home_info = $db->getGameHome($home_id);
else
{
$home_info = $db->getUserGameHome($user_id,$home_id);
$groups = $db->getUsersGroups($_SESSION['user_id']);
if (!is_array($groups)) {
$groups = [];
}
$query_groups .= " AND (";
foreach ((array)$groups as $group)
$query_groups .= "group_id=".$group['group_id']." OR ";
$query_groups .= "group_id=0 OR group_id IS NULL)";
}
if ( $home_info === FALSE )
{
print_failure(get_lang('no_rights'));
echo create_back_button("addonsmanager","user_addons");
return;
}
$home_cfg_id = $home_info['home_cfg_id'];
$server_xml = read_server_config(SERVER_CONFIG_LOCATION."/".$home_info['home_cfg_file']);
// 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'] : "";
$state = isset($_REQUEST['state']) ? $_REQUEST['state'] : "";
$pid = isset($_REQUEST['pid']) ? $_REQUEST['pid'] : -1;
if ( $state != "" )
{
$addon_id = (int)$_REQUEST['addon_id'];
$addons_rows = $db->resultQuery("SELECT url, path, post_script FROM OGP_DB_PREFIXaddons WHERE addon_id=".$addon_id.$query_groups);
if (!is_array($addons_rows)) {
$addons_rows = [];
}
if (!$addons_rows) {
print_failure(get_lang('invalid_addon'));
$view->refresh('?m=addonsmanager&p=user_addons&home_id='. $home_id .'&mod_id='. $mod_id .'&ip='. $ip .'&port='.$port);
return;
}
$remote = new OGPRemoteLibrary($home_info['agent_ip'],$home_info['agent_port'],$home_info['encryption_key'],$home_info['timeout']);
$addon_info = $addons_rows[0];
$url = $addon_info['url'];
$filename = basename($url);
#### Replace template variables in the post-install script with
#### live server data before sending to the agent.
#### Each variable is replaced case-insensitively.
#### SECURITY: only admin-defined variables are substituted; users
#### cannot inject additional commands through these fields.
if($addon_info['post_script'] != "")
{
$addon_info['post_script'] = strip_real_escape_string($addon_info['post_script']);
$check_passed = FALSE;
$address_at_post = $ip.":".$port;
$ip_ports = $db->getHomeIpPorts($home_info['home_id']);
if (!is_array($ip_ports)) {
$ip_ports = [];
}
foreach ((array)$ip_ports as $ip_port);
{
$address_owned = $ip_port['ip'].":".$ip_port['port'];
if($address_owned == $address_at_post)
{
$check_passed = TRUE;
$ip = $ip_port['ip'];
$port = $ip_port['port'];
}
}
if($check_passed)
{
$home_info['ip'] = $ip;
$home_info['port'] = $port;
if( isset($server_xml->gameq_query_name) )
{
require_once("modules/gamemanager/home_handling_functions.php");
$home_info['query_port'] = get_query_port($server_xml, $home_info['port']);
}
elseif( isset($server_xml->lgsl_query_name) )
{
$get_q_and_s = lgsl_port_conversion((string)$server_xml->lgsl_query_name, $home_info['port'], "", "");
$home_info['query_port'] = $get_q_and_s['1'];
}
$home_info["incremental"] = $db->incrementalNumByHomeId( $home_info['home_id'], $home_info['mods'][$mod_id]['mod_cfg_id'], $home_info['remote_server_id'] );
$post_script = preg_replace( "/\%home_path\%/i", $home_info['home_path'], $addon_info['post_script']);
$post_script = preg_replace( "/\%home_name\%/i", $home_info['home_name'], $post_script);
$post_script = preg_replace( "/\%control_password\%/i", $home_info['control_password'], $post_script);
$post_script = preg_replace( "/\%max_players\%/i", $home_info['mods'][$mod_id]['max_players'], $post_script);
$post_script = preg_replace( "/\%ip\%/i", $home_info['ip'], $post_script);
$post_script = preg_replace( "/\%port\%/i", $home_info['port'], $post_script);
$post_script = preg_replace( "/\%query_port\%/i", $home_info['query_port'], $post_script);
$post_script = preg_replace( "/\%incremental\%/i", $home_info['incremental'], $post_script);
}
}
#### end of replacements
if ( $state == "start" AND $addon_id != "" )
$pid = $remote->start_file_download( $addon_info['url'], $home_info['home_path']."/".$addon_info['path'], $filename, "uncompress", $post_script);
$headers = get_headers($url, 1);
$download_available = !$headers ? FALSE : TRUE;
// Check if any error occured
if($download_available)
{
$bytes = is_array($headers['Content-Length']) ? $headers['Content-Length'][1] : $headers['Content-Length'];
// Display the File Size
$totalsize = $bytes / 1024;
clearstatcache();
}
$kbytes = $remote->rsync_progress($home_info['home_path']."/".$addon_info['path']."/".$filename);
list($totalsize,$mbytes,$pct) = explode(";",do_progress($kbytes,$totalsize));
$totalmbytes = round($totalsize / 1024, 2);
$pct = $pct > 100 ? 100 : $pct;
echo "<h2>" . htmlentities($home_info['home_name']) . "</h2>";
echo '<div class="dragbox bloc rounded" style="background-color:#dce9f2;" >
<h4>'.get_lang('install')." ".$filename." ${mbytes}MB/${totalmbytes}MB</h4>
<div style='background-color:#dce9f2;' >
";
$bar = '';
for( $i = 1; $i <= $pct; $i++ )
{
$bar .= '<img style="width:0.92%;vertical-align:middle;" src="images/progressBar.png">';
}
echo "<center>$bar <b style='vertical-align:top;display:inline;font-size:1.2em;color:red;' >$pct%</b></center>
</div>
</div>";
if ( ( $pct == "100" or !$download_available ) AND $post_script != "" )
{
$log_retval = $remote->get_log( "post_script",
$pid,
clean_path($home_info['home_path']."/".$server_xml->exe_location),
$script_log);
if ($log_retval == 0)
{
print_failure(get_lang('agent_offline'));
}
elseif ($log_retval == 1 || $log_retval == 2)
{
echo "<pre class='log'>".$script_log."</pre>";
}
elseif( $remote->is_screen_running("post_script",$pid) == 1 )
{
print_failure(get_lang_f('unable_to_get_log',$log_retval));
}
}
if( $pct == "100" or !$download_available or ( $download_available and $pct == "-" and $pid > 0 ) )
{
if(!$download_available)
{
print_failure(get_lang('failed_to_start_file_download'));
}
elseif( $remote->is_file_download_in_progress($pid) === 1 )
{
print_success(get_lang_f('wait_while_decompressing', $filename));
echo "<p><a href=\"?m=addonsmanager&amp;p=addons&amp;state=refresh&amp;home_id=$home_id&amp;mod_id=$mod_id".
"&amp;ip=$ip&amp;port=$port&amp;addon_id=$addon_id&amp;pid=$pid\">".get_lang('refresh')."</a></p>";
$view->refresh("?m=addonsmanager&amp;p=addons&amp;state=refresh&amp;home_id=$home_id&amp;mod_id=$mod_id".
"&amp;ip=$ip&amp;port=$port&addon_id=$addon_id&amp;pid=$pid",5);
}
elseif( $remote->is_file_download_in_progress($pid) === 0 AND $remote->is_screen_running("post_script",$pid) === 0 )
{
print_success(get_lang('addon_installed_successfully'));
echo "<p><a href=\"?m=addonsmanager&amp;p=user_addons&amp;home_id=$home_id".
"&amp;mod_id=$mod_id&amp;ip=$ip&amp;port=$port\">".get_lang('back')."</a></p>";
$view->refresh("?m=addonsmanager&amp;p=user_addons&amp;home_id=$home_id".
"&amp;mod_id=$mod_id&amp;ip=$ip&amp;port=$port",10);
return;
}
}
else
{
echo "<p><a href=\"?m=addonsmanager&amp;p=addons&amp;state=refresh&amp;home_id=$home_id&amp;mod_id=$mod_id".
"&amp;ip=$ip&amp;port=$port&amp;addon_id=$addon_id&amp;pid=$pid\">".get_lang('refresh')."</a></p>";
$view->refresh("?m=addonsmanager&amp;p=addons&amp;state=refresh&amp;home_id=$home_id&amp;mod_id=$mod_id".
"&amp;ip=$ip&amp;port=$port&amp;addon_id=$addon_id&amp;pid=$pid",5);
}
}
elseif( $addon_type != "" )
{
if (!(is_array($addon_types) && in_array($addon_type, $addon_types))) {
print_failure(get_lang('invalid_addon_type'));
$view->refresh('?m=addonsmanager&p=user_addons&home_id='. $home_id .'&mod_id='. $mod_id .'&ip='. $ip .'&port='.$port);
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;
}
?>
<h2><?php echo htmlentities($home_info['home_name'])."&nbsp;".get_lang($addon_type) ;?></h2>
<table class='center'>
<form method='get'>
<input type='hidden' name='m' value='addonsmanager' />
<input type='hidden' name='p' value='addons' />
<input type='hidden' name='home_id' value='<?php echo $home_id; ?>' />
<input type='hidden' name='mod_id' value='<?php echo $mod_id; ?>' />
<input type='hidden' name='ip' value='<?php echo $ip; ?>' />
<input type='hidden' name='port' value='<?php echo $port; ?>' />
<input type='hidden' name='state' value='start' />
<tr><td align='right'><?php print_lang('game_name'); ?>: </td><td align='left'><?php echo $home_info['game_name']; ?></td></tr>
<tr><td align='right'><?php print_lang('directory'); ?>: </td><td align='left'><?php echo $home_info['home_path']; ?></td></tr>
<tr><td align='right'><?php print_lang('remote_server'); ?>: </td>
<td align='left'><?php echo "$home_info[remote_server_name] ($home_info[agent_ip]:$home_info[agent_port])"; ?></td></tr>
<tr><td align='right'><?php print_lang('select_addon'); ?>: </td>
<td align='left'>
<select name="addon_id">
<?php
$addons = $db->resultQuery("SELECT addon_id, name FROM OGP_DB_PREFIXaddons WHERE addon_type='".$addon_type."' AND home_cfg_id=" . $home_cfg_id . $query_groups . " ORDER BY name ASC");
if (!is_array($addons)) {
$addons = [];
}
foreach ((array)$addons as $addon)
{
?>
<option value="<?php echo $addon['addon_id']; ?>"><?php echo $addon['name']; ?></option>
<?php
}
?>
</select>
</td></tr>
<tr><td colspan='2' class='info'>&nbsp;</td></tr>
<td align='left'>
&nbsp;
</td></tr><tr><td align="right">
<input type="submit" name="update" value="<?php print_lang('install'); ?>" />
</form></td><td>
<form method="get">
<input type="hidden" name="m" value="addonsmanager" />
<input type="hidden" name="p" value="user_addons" />
<input type="hidden" name="home_id" value="<?php echo $home_id; ?>" />
<input type="hidden" name="mod_id" value="<?php echo $mod_id; ?>" />
<input type="hidden" name="ip" value="<?php echo $ip; ?>" />
<input type="hidden" name="port" value="<?php echo $port; ?>" />
<input type="submit" value="<?php print_lang('back'); ?>" />
</form>
</td></tr>
</table>
<?php
}
}
?>