Removed steam feom server content

This commit is contained in:
Frank Harris 2026-06-09 11:11:33 -05:00
parent 93a3ff11a4
commit 8eee9385ef
14 changed files with 2817 additions and 1421 deletions

View file

@ -113,13 +113,18 @@ 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');
require_once(dirname(__FILE__) . '/workshop_action.php');
if (file_exists(dirname(__FILE__) . '/../steam_workshop/includes/functions.php')) {
require_once(dirname(__FILE__) . '/../steam_workshop/includes/functions.php');
}
function exec_ogp_module() {
if (isset($install_method) && $install_method === 'steam_workshop') {
print_failure('Steam Workshop has been removed from Server Content. Use the Steam Workshop module instead.');
return;
}
global $db,$view;
$home_id = $_REQUEST['home_id'];
$mod_id = $_REQUEST['mod_id'];
@ -549,7 +554,7 @@ function exec_ogp_module() {
return;
}
// Workshop items are managed through the dedicated workshop_content page
// Steam Workshop is disabled in Server Content. Use the legacy steam_workshop module.
// where users enter their own Workshop IDs. Redirect there immediately.
if ($addon_type === 'workshop_item') {
$first_addon_id = 0;

View file

@ -0,0 +1,654 @@
<script type="text/javascript" src="js/modules/addonsmanager.js"></script>
<?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');
require_once(dirname(__FILE__) . '/workshop_action.php');
if (file_exists(dirname(__FILE__) . '/../steam_workshop/includes/functions.php')) {
require_once(dirname(__FILE__) . '/../steam_workshop/includes/functions.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']) ? scm_normalize_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, addon_type, install_method, content_version, requires_stop, restart_after_install, workshop_item_id, workshop_app_id, target_path_template, optional_folder_name, config_edit_rule, launch_param_additions, name 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];
$install_method = scm_get_install_method_default(isset($addon_info['install_method']) ? $addon_info['install_method'] : 'download_zip');
$content_version = isset($addon_info['content_version']) ? $addon_info['content_version'] : '';
$requires_stop = !empty($addon_info['requires_stop']) ? 1 : 0;
$user_override_keys = ($install_method === 'steam_workshop')
? array('workshop_item_id')
: array();
$install_payload = scm_collect_install_payload($addon_info, $_REQUEST, $user_override_keys);
$post_script = '';
$validation_message = '';
if ($state == "start" && !scm_validate_install_method_payload($install_method, $install_payload, $validation_message)) {
print_failure($validation_message);
return;
}
// ── requires_stop guard ───────────────────────────────────────────────
// If the content item requires the server to be stopped first, check
// whether the server is currently running and block the install if so.
// (Phase 2 blocks install; automatic stop/start is Phase 3.)
if ( $state == "start" && $requires_stop ) {
$is_running = $remote->is_screen_running( $home_info['home_name'], $home_info['home_id'] );
if ( $is_running === 1 ) {
print_failure('This content item requires the server to be stopped before installing. Please stop the server and try again.');
echo "<p><a href=\"?m=addonsmanager&amp;p=addons&amp;addon_type=".urlencode($addon_info['addon_type'] ?? '')."&amp;home_id=$home_id&amp;mod_id=$mod_id&amp;ip=$ip&amp;port=$port\">".get_lang('back')."</a></p>";
return;
}
}
$url = $install_payload['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 != "" ) {
// Record install attempt in history before triggering download.
$cache_mode = scm_get_cache_mode($db);
$history_id = scm_record_install_start(
$db,
$home_id,
$addon_id,
$user_id,
$addon_info['url'],
$content_version,
$install_method,
$cache_mode
);
$_SESSION['scm_history_id_' . $home_id . '_' . $addon_id] = $history_id;
$log_target_path = (string)$install_payload['path'];
if ($install_method === 'steam_workshop') {
$log_target_path = scm_extract_workshop_install_path($server_xml);
if ($log_target_path === '') {
$log_target_path = scm_get_default_workshop_target_template(scm_detect_workshop_install_strategy($home_info, $server_xml));
}
}
scm_log_content_install_action(array(
'addon_id' => (int)$addon_id,
'addon_name' => isset($addon_info['name']) ? $addon_info['name'] : '',
'content_type' => $install_method,
'home_id' => (int)$home_id,
'home_cfg_id' => (int)$home_info['home_cfg_id'],
'workshop_id' => isset($install_payload['workshop_item_id']) ? (string)$install_payload['workshop_item_id'] : '',
'target_path' => $log_target_path,
'action' => 'started',
));
if ($install_method === 'steam_workshop') {
scm_ensure_workshop_schema($db);
$workshop_runtime = scm_build_workshop_runtime_context($db, $home_info, $server_xml, $install_payload, $validation_message);
if ($workshop_runtime === false) {
print_failure($validation_message);
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>";
return;
}
$workshop_item_id = (string)$workshop_runtime['workshop_item_id'];
$target_path_template = (string)$workshop_runtime['target_path_template'];
$workshop_app_id = (string)$workshop_runtime['workshop_app_id'];
$steam_app_id = (string)$workshop_runtime['steam_app_id'];
$target_path_resolved = (string)$workshop_runtime['target_path_resolved'];
$extra_manifest = array(
'addon_id' => (int)$addon_id,
'target_path_template' => $target_path_template,
'target_path_resolved' => $target_path_resolved,
'config_edit_rule' => trim((string)$addon_info['config_edit_rule']),
'launch_param_additions' => isset($server_xml->workshop_support->startup_param_format) ? trim((string)$server_xml->workshop_support->startup_param_format) : '',
'workshop_app_id' => $workshop_app_id,
'steam_app_id' => $steam_app_id,
'steamcmd_path' => isset($workshop_runtime['steamcmd_path']) ? (string)$workshop_runtime['steamcmd_path'] : '',
'workshop_download_dir' => isset($workshop_runtime['workshop_download_dir']) ? (string)$workshop_runtime['workshop_download_dir'] : '',
'server_root' => isset($workshop_runtime['server_root']) ? (string)$workshop_runtime['server_root'] : rtrim((string)$home_info['home_path'], '/'),
'post_install_script' => scm_workshop_post_install_action($server_xml),
);
$workshop_error = '';
$workshop_ok = scm_workshop_write_manifest_and_run($db, $home_info, $server_xml, 'install', array($workshop_item_id), $workshop_error, $extra_manifest);
if ($workshop_ok) {
scm_record_install_done($db, (int)$history_id, 'installed', 0, 'workshop_install_ok');
scm_upsert_manifest($db, $home_id, $addon_id, array(
'install_method' => $install_method,
'content_version' => $content_version,
'install_state' => 'installed',
'source_url' => 'steam://workshop/' . $workshop_item_id,
'installed_by' => $user_id,
));
scm_log_content_install_action(array(
'addon_id' => (int)$addon_id,
'addon_name' => isset($addon_info['name']) ? $addon_info['name'] : '',
'content_type' => $install_method,
'home_id' => (int)$home_id,
'home_cfg_id' => (int)$home_info['home_cfg_id'],
'workshop_id' => $workshop_item_id,
'steam_app_id' => $steam_app_id,
'workshop_app_id' => $workshop_app_id,
'target_path' => $target_path_resolved,
'final_folder_path' => $target_path_resolved,
'action' => 'succeeded',
));
print_success(get_lang('addon_installed_successfully'));
} else {
scm_record_install_done($db, (int)$history_id, 'failed', 1, $workshop_error);
scm_log_content_install_action(array(
'addon_id' => (int)$addon_id,
'addon_name' => isset($addon_info['name']) ? $addon_info['name'] : '',
'content_type' => $install_method,
'home_id' => (int)$home_id,
'home_cfg_id' => (int)$home_info['home_cfg_id'],
'workshop_id' => $workshop_item_id,
'steam_app_id' => $steam_app_id,
'workshop_app_id' => $workshop_app_id,
'target_path' => $target_path_resolved,
'action' => 'failed',
'error' => $workshop_error,
));
print_failure($workshop_error);
}
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>";
return;
}
if ($install_method === 'post_script') {
$script_command = "cd " . escapeshellarg($home_info['home_path']) . " && /bin/bash -lc " . escapeshellarg((string)$post_script) . " ; echo __GSP_SCRIPT_EXIT:$?";
$script_output = $remote->exec($script_command);
$script_ok = is_string($script_output) && preg_match('/__GSP_SCRIPT_EXIT:(\d+)/', $script_output, $sm) && (int)$sm[1] === 0;
if ($script_ok) {
scm_record_install_done($db, (int)$history_id, 'installed', 0, trim((string)$script_output));
scm_upsert_manifest($db, $home_id, $addon_id, array(
'install_method' => $install_method,
'content_version' => $content_version,
'install_state' => 'installed',
'source_url' => '',
'installed_by' => $user_id,
));
scm_log_content_install_action(array('addon_id' => (int)$addon_id, 'addon_name' => isset($addon_info['name']) ? $addon_info['name'] : '', 'content_type' => $install_method, 'home_id' => (int)$home_id, 'home_cfg_id' => (int)$home_info['home_cfg_id'], 'action' => 'succeeded'));
print_success(get_lang('addon_installed_successfully'));
} else {
$error_msg = 'Script/action failed.';
scm_record_install_done($db, (int)$history_id, 'failed', 1, trim((string)$script_output));
scm_log_content_install_action(array('addon_id' => (int)$addon_id, 'addon_name' => isset($addon_info['name']) ? $addon_info['name'] : '', 'content_type' => $install_method, 'home_id' => (int)$home_id, 'home_cfg_id' => (int)$home_info['home_cfg_id'], 'action' => 'failed', 'error' => trim((string)$script_output)));
print_failure($error_msg);
}
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>";
return;
}
if ($install_method === 'config_edit' || $install_method === 'create_folder') {
$placeholder_map = scm_build_placeholder_map($home_info, array('exe_location' => (string)$server_xml->exe_location));
$target_template = trim((string)$addon_info['path']);
$resolved_path = scm_apply_placeholders($target_template, $placeholder_map);
if ($resolved_path === '' || strpos($resolved_path, '/') !== 0) {
$resolved_path = clean_path(rtrim($home_info['home_path'], '/') . '/' . ltrim($resolved_path, '/'));
}
$ok = false;
if ($install_method === 'create_folder') {
$ok = is_string($remote->exec("mkdir -p " . escapeshellarg($resolved_path) . " && echo __GSP_FOLDER_OK"));
} else {
$config_rule = trim((string)$addon_info['config_edit_rule']);
$dir = dirname($resolved_path);
$cmd = "mkdir -p " . escapeshellarg($dir) . " && touch " . escapeshellarg($resolved_path) . " && printf %s " . escapeshellarg($config_rule . PHP_EOL) . " >> " . escapeshellarg($resolved_path) . " && echo __GSP_CONFIG_OK";
$out = $remote->exec($cmd);
$ok = is_string($out) && strpos($out, '__GSP_CONFIG_OK') !== false;
}
if ($ok) {
scm_record_install_done($db, (int)$history_id, 'installed', 0, $install_method . '_ok');
scm_upsert_manifest($db, $home_id, $addon_id, array('install_method' => $install_method, 'content_version' => $content_version, 'install_state' => 'installed', 'source_url' => '', 'installed_by' => $user_id));
scm_log_content_install_action(array('addon_id' => (int)$addon_id, 'addon_name' => isset($addon_info['name']) ? $addon_info['name'] : '', 'content_type' => $install_method, 'home_id' => (int)$home_id, 'home_cfg_id' => (int)$home_info['home_cfg_id'], 'target_path' => $resolved_path, 'action' => 'succeeded'));
print_success(get_lang('addon_installed_successfully'));
} else {
scm_record_install_done($db, (int)$history_id, 'failed', 1, $install_method . '_failed');
scm_log_content_install_action(array('addon_id' => (int)$addon_id, 'addon_name' => isset($addon_info['name']) ? $addon_info['name'] : '', 'content_type' => $install_method, 'home_id' => (int)$home_id, 'home_cfg_id' => (int)$home_info['home_cfg_id'], 'target_path' => $resolved_path, 'action' => 'failed', 'error' => $install_method . '_failed'));
print_failure('Content action failed.');
}
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>";
return;
}
$download_action = ($install_method === 'download_file') ? "" : "uncompress";
$pid = $remote->start_file_download( $addon_info['url'], $home_info['home_path']."/".$addon_info['path'], $filename, $download_action, $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'));
scm_log_content_install_action(array(
'addon_id' => (int)$addon_id,
'addon_name' => isset($addon_info['name']) ? $addon_info['name'] : '',
'content_type' => $install_method,
'home_id' => (int)$home_id,
'home_cfg_id' => (int)$home_info['home_cfg_id'],
'action' => 'failed',
'error' => 'failed_to_start_file_download',
));
}
elseif( $remote->is_file_download_in_progress($pid) === 1 )
{
if ($install_method === 'download_zip')
print_success(get_lang_f('wait_while_decompressing', $filename));
else
print_success(get_lang('install') . " " . $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'));
// Update install history and manifest on successful completion.
$history_key = 'scm_history_id_' . $home_id . '_' . $addon_id;
if (!empty($_SESSION[$history_key])) {
scm_record_install_done($db, (int)$_SESSION[$history_key], 'installed', 0);
unset($_SESSION[$history_key]);
}
scm_upsert_manifest($db, $home_id, $addon_id, array(
'install_method' => $install_method,
'content_version' => $content_version,
'install_state' => 'installed',
'source_url' => $addon_info['url'],
'installed_by' => $user_id,
));
scm_log_content_install_action(array(
'addon_id' => (int)$addon_id,
'addon_name' => isset($addon_info['name']) ? $addon_info['name'] : '',
'content_type' => $install_method,
'home_id' => (int)$home_id,
'home_cfg_id' => (int)$home_info['home_cfg_id'],
'target_path' => isset($addon_info['path']) ? (string)$addon_info['path'] : '',
'action' => 'succeeded',
));
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;
}
// Workshop items are managed through the dedicated workshop_content page
// where users enter their own Workshop IDs. Redirect there immediately.
if ($addon_type === 'workshop_item') {
$first_addon_id = 0;
$wk_addons = $db->resultQuery(
"SELECT addon_id FROM OGP_DB_PREFIXaddons
WHERE addon_type='workshop_item' AND home_cfg_id=" . (int)$home_cfg_id . $query_groups . "
ORDER BY name ASC LIMIT 1"
);
if (is_array($wk_addons) && !empty($wk_addons[0]['addon_id'])) {
$first_addon_id = (int)$wk_addons[0]['addon_id'];
}
$redirect = "?m=addonsmanager&p=workshop_content&home_id=" . (int)$home_id .
"&mod_id=" . (int)$mod_id . "&ip=" . urlencode($ip) . "&port=" . urlencode($port) .
($first_addon_id > 0 ? "&addon_id=" . $first_addon_id : '');
$view->refresh($redirect);
echo "<p>Redirecting to Workshop Content manager…<br>";
echo "<a href='" . htmlspecialchars($redirect, ENT_QUOTES, 'UTF-8') . "'>Click here if not redirected.</a></p>";
return;
}
?>
<?php
$category_labels = get_server_content_categories();
$addon_type_lang = isset($category_labels[$addon_type]) ? $category_labels[$addon_type] : ucfirst(str_replace('_', ' ', $addon_type));
$addons = $db->resultQuery(
"SELECT addon_id, name, install_method, workshop_item_id, workshop_app_id, target_path_template, optional_folder_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 = [];
}
$selected_addon = isset($addons[0]) ? $addons[0] : array();
$default_install_method = isset($selected_addon['install_method']) ? scm_get_install_method_default($selected_addon['install_method']) : '';
$is_workshop_default = ($default_install_method === 'steam_workshop');
$default_workshop_app_id = scm_extract_workshop_app_id($server_xml);
$default_target_template = scm_extract_workshop_install_path($server_xml);
if ($default_target_template === '') {
$default_target_template = scm_get_default_workshop_target_template(scm_detect_workshop_install_strategy($home_info, $server_xml));
}
?>
<h2><?php echo htmlentities($home_info['home_name'])."&nbsp;".$addon_type_lang ;?></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" id="scm-user-addon-select">
<?php foreach ((array)$addons as $addon) { ?>
<option
value="<?php echo (int)$addon['addon_id']; ?>"
data-install-method="<?php echo htmlspecialchars(scm_get_install_method_default(isset($addon['install_method']) ? $addon['install_method'] : ''), ENT_QUOTES, 'UTF-8'); ?>"
data-workshop-item-id="<?php echo htmlspecialchars(isset($addon['workshop_item_id']) ? $addon['workshop_item_id'] : '', ENT_QUOTES, 'UTF-8'); ?>"
><?php echo htmlspecialchars($addon['name']); ?></option>
<?php } ?>
</select>
</td></tr>
<tr class="scm-user-workshop-row" <?php echo $is_workshop_default ? '' : 'style="display:none;"'; ?>><td align='right'><strong><?php print_lang('workshop_id'); ?></strong></td><td align='left'>
<input type="text" id="scm-user-workshop-id" name="workshop_item_id" size="50" value="<?php echo htmlspecialchars(isset($selected_addon['workshop_item_id']) ? $selected_addon['workshop_item_id'] : '', ENT_QUOTES, 'UTF-8'); ?>" placeholder="Example Arma 3 Workshop ID: 450814997" />
<div class="info" style="margin-top:4px;">Install a Steam Workshop mod using Workshop ID. URL is not required.</div>
</td></tr>
<tr class="scm-user-workshop-row" <?php echo $is_workshop_default ? '' : 'style="display:none;"'; ?>><td align='right'><strong>Target Path Preview</strong></td><td align='left'>
<code id="scm-user-target-path-preview"
data-server-root="<?php echo htmlspecialchars(rtrim((string)$home_info['home_path'], '/'), ENT_QUOTES, 'UTF-8'); ?>"
data-game-root="<?php echo htmlspecialchars(rtrim((string)$home_info['home_path'], '/'), ENT_QUOTES, 'UTF-8'); ?>"
data-steam-app-id="<?php echo htmlspecialchars(scm_extract_workshop_steam_app_id($server_xml), ENT_QUOTES, 'UTF-8'); ?>"
data-workshop-app-id="<?php echo htmlspecialchars($default_workshop_app_id, ENT_QUOTES, 'UTF-8'); ?>"
data-target-template="<?php echo htmlspecialchars($default_target_template, ENT_QUOTES, 'UTF-8'); ?>"
><?php echo htmlspecialchars($default_target_template, ENT_QUOTES, 'UTF-8'); ?></code>
<div class="info" style="margin-top:4px;">Workshop App ID, install path, and launch parameter format are defined in the game XML.</div>
</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
}
}
?>

View file

@ -30,6 +30,12 @@ require_once("modules/config_games/server_config_parser.php");
function exec_ogp_module() {
if (isset($install_method) && $install_method === 'steam_workshop') {
print_failure('Steam Workshop has been removed from Server Content. Use the Steam Workshop module instead.');
return;
}
global $db;
// Ensure Phase 2 schema is present (idempotent).

View file

@ -0,0 +1,532 @@
<script type="text/javascript" src="js/modules/addonsmanager.js"></script>
<?php
/*
*
* GSP - Game Server Panel (a heavily customized fork of OGP maintained by WDS)
*
* Admin page: Server Content Manager (module: addonsmanager)
* ─────────────────────────────────────────────────────────────────────────────
* This page lets admins create, edit, and remove Server Content items.
*
* A "Server Content item" is anything that can be pushed to a game server:
* 1. A zip/file package extracted into the server directory.
* 2. A downloaded file placed into the server directory.
* 3. A script-driven installer (post_script only, no download required).
* 4. A Minecraft server jar / version switcher (future: install_method=minecraft_jar).
* 5. A DayZ/Epoch/Arma profile copy (future: install_method=profile_copy).
* 6. A Steam Workshop entry point (install behavior comes from game XML).
* 7. A config preset (type=config).
* 8. A full server profile built from multiple actions (type=profile).
*
* DB table: OGP_DB_PREFIXaddons (unchanged for backward compatibility).
* See SERVER_CONTENT_ROADMAP.md for the full migration plan.
*
*/
// Central category map — defines all valid addon_type values and their labels.
require_once(dirname(__FILE__) . '/server_content_categories.php');
require_once(dirname(__FILE__) . '/server_content_helpers.php');
require_once("modules/config_games/server_config_parser.php");
function exec_ogp_module() {
global $db;
// Ensure Phase 2 schema is present (idempotent).
scm_ensure_phase2_schema($db);
// Build the complete list of allowed content types from the category map.
// Admins can create items of any registered type; the original three types
// (plugin, mappack, config) are always included.
$addon_types = get_server_content_type_keys(); // all keys
$addon_type_labels = get_server_content_categories(); // key => label
$install_methods = scm_get_install_methods(); // install_method keys => labels
if (isset($_POST['create_addon']))
{
$valid_install_methods = array_keys($install_methods);
$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'] = '';
$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;
$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;
$fields['restart_after_install'] = !empty($_POST['restart_after_install']) ? 1 : 0;
$fields['is_cacheable'] = !empty($_POST['is_cacheable']) ? 1 : 0;
$fields['description'] = isset($_POST['description']) ? $_POST['description'] : '';
$fields['workshop_item_id'] = '';
$fields['workshop_app_id'] = '';
$fields['target_path_template']= '';
$fields['optional_folder_name']= '';
$fields['config_edit_rule'] = isset($_POST['config_edit_rule']) ? trim((string)$_POST['config_edit_rule']) : '';
$fields['launch_param_additions'] = '';
$fields['addon_type'] = scm_get_addon_type_from_install_method($fields['install_method']);
if ($fields['install_method'] === 'steam_workshop') {
$fields['url'] = '';
$fields['path'] = '';
$fields['post_script'] = '';
$fields['config_edit_rule'] = '';
}
if ($fields['name'] === '')
{
print_failure(get_lang("fill_the_addon_name"));
}
elseif (empty($fields['home_cfg_id']))
{
print_failure(get_lang("select_a_game_type"));
}
elseif ($fields['install_method'] === 'steam_workshop' && (!($game_cfg = $db->getGameCfg($fields['home_cfg_id'])) || !($server_xml = read_server_config(SERVER_CONFIG_LOCATION . "/" . $game_cfg['home_cfg_file'])) || !scm_workshop_is_supported($server_xml)))
{
print_failure('Steam Workshop support must be configured in the selected game XML before creating a Workshop content entry.');
}
else
{
$validation_payload = array(
'url' => $fields['url'],
'path' => $fields['path'],
'workshop_item_id' => $fields['workshop_item_id'],
'workshop_app_id' => $fields['workshop_app_id'],
'target_path_template' => $fields['target_path_template'],
'post_script' => $fields['post_script'],
'config_edit_rule' => $fields['config_edit_rule'],
);
$validation_message = '';
if (!scm_validate_install_method_payload($fields['install_method'], $validation_payload, $validation_message))
{
print_failure($validation_message);
}
elseif (is_numeric($db->resultInsertId('addons', $fields)))
{
print_success(get_lang_f("addon_has_been_created", $fields['name']));
if (isset($_POST['addon_id']) && (int)$_POST['addon_id'] > 0 && isset($_POST['edit']))
$db->query("DELETE FROM OGP_DB_PREFIXaddons WHERE addon_id=" . (int)$_POST['addon_id']);
}
}
}
echo "<h2>".get_lang('addons_manager')."</h2>";
$name = isset($_POST['name']) ? $_POST['name'] : "";
$url = isset($_POST['url']) ? $_POST['url'] : "";
$path = isset($_POST['path']) ? $_POST['path'] : "";
$post_script = isset($_POST['post_script']) ? $_POST['post_script'] : "";
$home_cfg_id = isset($_POST['home_cfg_id']) ? $_POST['home_cfg_id'] : "";
$addon_type = isset($_POST['addon_type']) ? $_POST['addon_type'] : "";
$group_id = isset($_POST['group_id']) ? $_POST['group_id'] : "";
$install_method = isset($_POST['install_method']) ? $_POST['install_method'] : "download_zip";
$content_version = isset($_POST['content_version']) ? $_POST['content_version'] : "";
$requires_stop = isset($_POST['requires_stop']) ? (int)$_POST['requires_stop'] : 1;
$backup_before_install = isset($_POST['backup_before_install']) ? (int)$_POST['backup_before_install'] : 1;
$restart_after_install = isset($_POST['restart_after_install']) ? (int)$_POST['restart_after_install'] : 0;
$is_cacheable = isset($_POST['is_cacheable']) ? (int)$_POST['is_cacheable'] : 0;
$description = isset($_POST['description']) ? $_POST['description'] : "";
$config_edit_rule = isset($_POST['config_edit_rule']) ? $_POST['config_edit_rule'] : "";
if (isset($_POST['addon_id']) && (int)$_POST['addon_id'] > 0 && isset($_POST['edit']))
{
$addons_rows = $db->resultQuery("SELECT * FROM OGP_DB_PREFIXaddons WHERE addon_id=".(int)$_POST['addon_id']);
if (!is_array($addons_rows)) {
$addons_rows = [];
}
$addon_info = $addons_rows[0];
$name = isset($addon_info['name']) ? $addon_info['name'] : "";
$url = isset($addon_info['url']) ? $addon_info['url'] : "";
$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 = 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'] : "";
$requires_stop = isset($addon_info['requires_stop']) ? (int)$addon_info['requires_stop'] : 1;
$backup_before_install = isset($addon_info['backup_before_install']) ? (int)$addon_info['backup_before_install'] : 1;
$restart_after_install = isset($addon_info['restart_after_install']) ? (int)$addon_info['restart_after_install'] : 0;
$is_cacheable = isset($addon_info['is_cacheable']) ? (int)$addon_info['is_cacheable'] : 0;
$description = isset($addon_info['description']) ? $addon_info['description'] : "";
$config_edit_rule = isset($addon_info['config_edit_rule']) ? $addon_info['config_edit_rule'] : "";
}
?>
<form action="" method="post">
<table class="center">
<tr>
<td align="right">
<b><?php print_lang('addon_name'); ?></b>
</td>
<td align="left">
<input type="text" value="<?php echo $name; ?>" name="name" size="85" title="<?php print_lang('addon_name_info'); ?>" />
</td>
</tr>
<tr id="scm-row-install-method">
<td align="right">
<b><?php print_lang('content_type'); ?></b>
</td>
<td align="left">
<select name="install_method" id="scm-install-method">
<?php
$install_help = scm_get_install_method_help_text();
foreach ((array)$install_methods as $method_key => $method_label) {
$sel = ($method_key == $install_method) ? 'selected="selected"' : '';
$help = isset($install_help[$method_key]) ? $install_help[$method_key] : '';
echo '<option value="'.htmlspecialchars($method_key).'" data-help="'.htmlspecialchars($help, ENT_QUOTES, 'UTF-8').'" '.$sel.'>'.htmlspecialchars($method_label).'</option>'."\n";
}
?>
</select>
<div id="scm-install-method-help" style="color:#666;margin-top:4px;"></div>
</td>
</tr>
<tr id="scm-row-url">
<td align="right">
<b><?php print_lang('url'); ?></b>
</td>
<td align="left">
<input type="text" value="<?php echo $url; ?>" name="url" size="85" title="<?php print_lang('url_info'); ?>" />
</td>
</tr>
<!-- Destination path — must be relative to the game server home directory.
Path traversal (../) is not allowed; the agent enforces this. -->
<tr id="scm-row-path">
<td align="right">
<b id="scm-path-label"><?php print_lang('path'); ?></b>
</td>
<td align="left">
<input type="text" value="<?php echo $path; ?>" name="path" size="85" title="<?php print_lang('path_info'); ?>" />
</td>
</tr>
<tr id="scm-row-workshop-xml-info">
<td align="right">
<b>Steam Workshop</b>
</td>
<td align="left">
<div class="info">Steam Workshop behavior is configured by the selected game's XML <code>workshop_support</code> block. Users install Workshop items from their server management page.</div>
</td>
</tr>
<tr id="scm-row-post-script">
<td align="right">
<b>Post-Install Script / Action</b><br>
<u><?php print_lang('replacements'); ?></u><br>
%home_path%<br>
%home_name%<br>
%control_password%<br>
%max_players%<br>
%ip%<br>
%port%<br>
%query_port%<br>
%incremental%<br>
</td>
<td align="left">
<textarea name="post_script" style="width:99%;height:175px;" title="<?php print_lang('post-script_info'); ?>" ><?php echo strip_real_escape_string($post_script); ?></textarea>
</td>
</tr>
<tr id="scm-row-config-edit-rule">
<td align="right">
<b><?php print_lang('config_edit_rule'); ?></b>
</td>
<td align="left">
<textarea name="config_edit_rule" style="width:99%;height:90px;" placeholder="Text/rules to append or apply to the target config."><?php echo htmlspecialchars($config_edit_rule, ENT_QUOTES, 'UTF-8'); ?></textarea>
</td>
</tr>
<tr>
<td align="right">
<b><?php print_lang('select_game_type'); ?></b>
</td>
<td align="left">
<select name='home_cfg_id'>
<?php
$game_cfgs = $db->getGameCfgs();
if (!is_array($game_cfgs)) {
$game_cfgs = [];
}
echo "<option style='background:black;color:white;' value=''>".get_lang('linux_games')."</option>\n";
foreach ((array)$game_cfgs as $row)
{
if ( preg_match("/linux/", $row['game_key']) )
{
$selected = (isset($home_cfg_id) AND $row['home_cfg_id'] == $home_cfg_id) ? 'selected="selected"' : '';
echo "<option $selected value='".$row['home_cfg_id']."'>".$row['game_name'];
if ( preg_match("/64/", $row['game_key']) ) echo " (64bit)";
echo "</option>\n";
}
}
echo "<option style='background:black;color:white;' value=''>".get_lang('windows_games')."</option>\n";
foreach ((array)$game_cfgs as $row)
{
if ( preg_match("/win/", $row['game_key']) )
{
$selected = (isset($home_cfg_id) AND $row['home_cfg_id'] == $home_cfg_id) ? 'selected=selected' : '';
echo "<option $selected value='".$row['home_cfg_id']."'>".$row['game_name'];
if ( preg_match("/64/", $row['game_key']) ) echo " (64bit)";
echo "</option>\n";
}
}
?>
</select>
</td>
</tr>
<tr>
<td align="right">
<b><?php print_lang('show_to_group'); ?></b>
</td>
<td align="left">
<select name='group_id'>
<option value="0"><?php print_lang('all_groups'); ?></option>
<?php
$groups = $db->getGroupList();
if (!is_array($groups)) {
$groups = [];
}
foreach ((array)$groups as $group)
{
$selected = (isset($group_id) AND $group['group_id'] == $group_id) ? 'selected=selected' : '';
echo "<option value='".$group['group_id']."' $selected>".$group['group_name']."</option>\n";
}
?>
</select>
</td>
</tr>
<tr>
<td align="right">
<b>Content Version</b>
</td>
<td align="left">
<input type="text" value="<?php echo htmlspecialchars($content_version, ENT_QUOTES, 'UTF-8'); ?>" name="content_version" size="40" placeholder="e.g. 1.21.1 or 2024-05-01" />
<small style="color:#666;"> Optional version tag shown in the installed-content list.</small>
</td>
</tr>
<tr>
<td align="right">
<b>Description</b>
</td>
<td align="left">
<textarea name="description" style="width:99%;height:60px;" placeholder="Short description shown to users."><?php echo htmlspecialchars($description, ENT_QUOTES, 'UTF-8'); ?></textarea>
</td>
</tr>
<tr>
<td align="right">
<b>Behaviour Options</b>
</td>
<td align="left">
<label>
<input type="checkbox" name="requires_stop" value="1" <?php echo $requires_stop ? 'checked' : ''; ?> />
Stop server before installing
</label>
&nbsp;&nbsp;
<label>
<input type="checkbox" name="backup_before_install" value="1" <?php echo $backup_before_install ? 'checked' : ''; ?> />
Backup target path before installing
</label>
&nbsp;&nbsp;
<label>
<input type="checkbox" name="restart_after_install" value="1" <?php echo $restart_after_install ? 'checked' : ''; ?> />
Restart server after successful install
</label>
</td>
</tr>
<tr>
<td align="right">
<b>Content Reuse</b>
</td>
<td align="left">
<label>
<input type="checkbox" name="is_cacheable" value="1" <?php echo $is_cacheable ? 'checked' : ''; ?> />
Mark as cacheable / reusable
</label>
<small style="color:#666;">
Only check this for public, non-sensitive content (maps, mods, jars).
<strong>Never</strong> check for configs, saves, credentials, or user-edited files.
Caching only activates when the <em>Server Content Cache Mode</em> panel
setting (in Panel Settings) is set to something other than <em>Disabled</em>.
</small>
</td>
</tr>
<tr>
<td colspan="2" align="center">
<?php
if (isset($_POST['addon_id']) && isset($_POST['edit']))
{
echo '<input type="hidden" name="addon_id" value="'.$_POST['addon_id'].'" >';
echo '<input type="hidden" name="edit" value="'.$_POST['edit'].'" >';
?>
<button name="create_addon" type="submit">
<?php print_lang('edit_addon'); ?>
</button>
<?php
}
else
{
?>
<button name="create_addon" type="submit">
<?php print_lang('create_addon'); ?>
</button>
<?php
}
?>
</td>
</tr>
</table>
</form>
<br>
<h2><?php print_lang('addons_db'); ?></h2>
<table class="center">
<tr>
<td align="center">
<form name="remove" action="" method="get">
<input name="m" type="hidden" value="addonsmanager"/>
<input name="p" type="hidden" value="addons_manager"/>
<b><?php print_lang('game'); ?></b> <select name='home_cfg_id'>
<?php
echo "<option style='background:black;color:white;' value=''>".get_lang('linux_games')."</option>\n";
foreach ((array)$game_cfgs as $row)
{
if ( preg_match("/linux/", $row['game_key']) )
{
if(isset($_GET['home_cfg_id']) AND $row['home_cfg_id'] == $_GET['home_cfg_id'])
$selected = "selected='selected'";
else
$selected = "";
echo "<option value='".$row['home_cfg_id']."' $selected >".$row['game_name'];
if ( preg_match("/64/", $row['game_key']) ) echo " (64bit)";
echo "</option>\n";
}
}
echo "<option style='background:black;color:white;' value=''>".get_lang('windows_games')."</option>\n";
foreach ((array)$game_cfgs as $row)
{
if(isset($_GET['home_cfg_id']) AND $row['home_cfg_id'] == $_GET['home_cfg_id'])
$selected = "selected='selected'";
else
$selected = "";
if ( preg_match("/win/", $row['game_key']) )
{
echo "<option value='".$row['home_cfg_id']."' $selected >".$row['game_name'];
if ( preg_match("/64/", $row['game_key']) ) echo " (64bit)";
echo "</option>\n";
}
}
?>
</select>
<b><?php print_lang('type'); ?></b>
<select name="addon_type">
<?php
$option = '';
foreach ((array)$addon_type_labels as $k => $label) {
$option .= '<option';
if (isset($_GET['addon_type']) && $_GET['addon_type'] == $k) {
$option .= ' selected';
}
$option .= ' value="'. htmlspecialchars($k) .'">'.htmlspecialchars($label).'</option>';
}
echo $option;
?>
</select>
<b><?php print_lang('group'); ?></b>
<select name='group_id'>
<option value="0"><?php print_lang('all_groups'); ?></option>
<?php
foreach ((array)$groups as $group)
{
$selected = (isset($_GET['group_id']) AND $group['group_id'] == $_GET['group_id']) ? 'selected=selected' : '';
echo "<option value='".$group['group_id']."' $selected>".$group['group_name']."</option>\n";
}
?>
</select>
<button name="show" type="submit">
<?php print_lang('show'); ?>
</button>
</td>
</tr>
<tr>
<td>
<input name="show_game" type="submit" value="<?php print_lang('show_addons_for_selected_game'); ?>"/>
<input name="show_type" type="submit" value="<?php print_lang('show_addons_for_selected_type'); ?>"/>
<input name="show_group" type="submit" value="<?php print_lang('show_addons_for_selected_group'); ?>"/>
<input name="show_all" type="submit" value="<?php print_lang('show_all_addons'); ?>"/>
</td>
</tr>
</form>
</table>
<?php
if (isset($_POST['addon_id']) && (int)$_POST['addon_id'] > 0 && isset($_POST['remove']))
{
if (!$db->query("DELETE FROM OGP_DB_PREFIXaddons WHERE addon_id=" . (int)$_POST['addon_id']))
print_lang('can_not_remove_addon');
}
$home_cfg_id = !empty($_GET['home_cfg_id']) && (int)$_GET['home_cfg_id'] > 0 ? (int)$_GET['home_cfg_id'] : 0;
$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;
if ( isset($_GET['show']) )
{
$result = $db->resultQuery("SELECT DISTINCT addon_id, name, game_name, url, path, group_id FROM OGP_DB_PREFIXaddons NATURAL JOIN OGP_DB_PREFIXconfig_homes WHERE addon_type='".$addon_type."' AND home_cfg_id=".$home_cfg_id);
}
elseif ( isset($_GET['show_all']) )
{
$result = $db->resultQuery("SELECT DISTINCT addon_id, name, game_name, url, path, group_id FROM OGP_DB_PREFIXaddons NATURAL JOIN OGP_DB_PREFIXconfig_homes");
}
elseif ( isset($_GET['show_type']))
{
$result = $db->resultQuery("SELECT DISTINCT addon_id, name, game_name, url, path, group_id FROM OGP_DB_PREFIXaddons NATURAL JOIN OGP_DB_PREFIXconfig_homes WHERE addon_type='".$addon_type."'");
}
elseif ( isset($_GET['show_game']))
{
$result = $db->resultQuery("SELECT DISTINCT addon_id, name, game_name, url, path, group_id FROM OGP_DB_PREFIXaddons NATURAL JOIN OGP_DB_PREFIXconfig_homes WHERE home_cfg_id=".$home_cfg_id);
}
elseif ( isset($_GET['show_group']))
{
$group_id = $group_id == '0' ? $group_id." OR group_id IS NULL" : $group_id;
$result = $db->resultQuery("SELECT DISTINCT addon_id, name, game_name, url, path, group_id FROM OGP_DB_PREFIXaddons NATURAL JOIN OGP_DB_PREFIXconfig_homes WHERE group_id=".$group_id);
}
if (isset($result) && !is_array($result)) {
$result = [];
}
?>
<table class="center">
<?php
$group_names = array();
foreach ((array)$groups as $group)
$group_names[$group['group_id']] = $group['group_name'];
if (isset($result) and is_array($result) and (is_array($result) ? count((array)$result) : 0) > 0)
{
foreach ((array)$result as $row)
{
?>
<tr>
<form action="" method="post">
<td class='left'>
<b><?php echo $row['game_name']; ?></b>
</td>
<td>
<?php echo $row['name'];?>
</td>
<td>
<?php echo "[".get_lang('group').": ". (isset($group_names[$row['group_id']])?$group_names[$row['group_id']]:get_lang('all_groups')) ."]";?>
</td>
<td>
<input name="addon_id" type="hidden" value="<?php echo $row['addon_id'];?>"/>
<input name="edit" type="submit" value="<?php print_lang('edit_addon'); ?>"/>
<input name="remove" type="submit" value="<?php print_lang('remove_addon'); ?>"/>
</td>
</form>
</tr>
<?php
}
}
?>
</table>
<?php
}
?>

View file

@ -0,0 +1,293 @@
<?php
/*
*
* GSP - Game Server Panel (a heavily customized fork of OGP maintained by WDS)
*
* Module: addonsmanager → Server Content Manager
* ─────────────────────────────────────────────────────────────────────────────
* The module folder and DB table names are intentionally unchanged for
* backward compatibility. Only UI labels have been updated to the new
* "Server Content" terminology.
*
* db_version history:
* 1 initial schema (addons table, addon_type VARCHAR(7))
* 2 expand addon_type to VARCHAR(32) to support extended content types
* (workshop=8 chars, and any future type up to 32 chars)
* 3 add server_content_workshop table for per-server Workshop item selections
* 4 Phase 2: add install_method / content_version / requires_stop /
* backup_before_install / restart_after_install / is_cacheable /
* description columns to addons table; add server_content_manifest
* and server_content_install_history tables
* 5 add workshop_item_id / workshop_app_id / target_path_template /
* optional_folder_name / config_edit_rule / launch_param_additions
* columns to addons table
* 6 add admin template policy columns to addons table
* (allow_user_workshop_ids, max_workshop_ids, required_workshop_ids,
* blocked_workshop_ids); add content_id column to
* server_content_workshop so user installs link to their template
* 7 add Phase 1 Workshop runtime tracking columns to
* server_content_workshop (install_path, install_strategy, enabled,
* load_order)
*
*/
// Module general information
$module_title = "Server Content Manager";
$module_version = "2.5";
$db_version = 7;
$module_required = TRUE;
$module_menus = array(
array( 'subpage' => 'addons_manager', 'name' => 'Server Content Manager', 'group' => 'admin' )
);
// ── db_version 1 : initial install ───────────────────────────────────────────
$install_queries = array();
$install_queries[0] = array(
"CREATE TABLE IF NOT EXISTS `".OGP_DB_PREFIX."addons` (
`addon_id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
`name` VARCHAR(80) NOT NULL,
`url` VARCHAR(200) NOT NULL,
`path` VARCHAR(80) NOT NULL,
`addon_type` VARCHAR(7) NOT NULL,
`home_cfg_id` VARCHAR(7) NOT NULL,
`post_script` longtext NOT NULL,
`group_id` int(11) NULL
) ENGINE=MyISAM;"
);
// ── db_version 2 : expand addon_type to VARCHAR(32) ──────────────────────────
// Required so extended content types such as 'workshop' (8 chars) can be stored.
// MODIFY is safe on existing installs; existing 'plugin'/'mappack'/'config'
// values are preserved without alteration.
$install_queries[1] = array(
"ALTER TABLE `".OGP_DB_PREFIX."addons`
MODIFY `addon_type` VARCHAR(32) NOT NULL;"
);
// ── db_version 3 : workshop item selections per server home ───────────────────
$install_queries[2] = array(
"CREATE TABLE IF NOT EXISTS `".OGP_DB_PREFIX."server_content_workshop` (
`id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
`home_id` INT NOT NULL,
`home_cfg_id` INT NOT NULL,
`remote_server_id` INT NULL,
`workshop_app_id` VARCHAR(32) NULL,
`workshop_item_id` VARCHAR(64) NOT NULL,
`title` VARCHAR(255) NULL,
`install_state` VARCHAR(32) NOT NULL DEFAULT 'selected',
`last_installed_at` DATETIME NULL,
`last_updated_at` DATETIME NULL,
`last_error` TEXT NULL,
`created_by` INT NULL,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NULL,
UNIQUE KEY `uniq_home_workshop_item` (`home_id`, `workshop_item_id`),
KEY `idx_home_id` (`home_id`),
KEY `idx_home_cfg_id` (`home_cfg_id`),
KEY `idx_install_state` (`install_state`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;"
);
// ── db_version 4 : Phase 2 install_method, per-server manifest, install history ──
//
// Uses a PHP callable so each ALTER is applied only when the column does not
// already exist (safe for repeated runs, compatible with all MySQL versions).
//
$install_queries[3] = array(
function ($db) {
$prefix = OGP_DB_PREFIX;
// ── Extend the addons table with Phase 2 columns ──────────────────────
$new_columns = array(
'install_method' => "VARCHAR(32) NOT NULL DEFAULT 'download_zip' AFTER `group_id`",
'content_version' => "VARCHAR(64) NULL AFTER `install_method`",
'requires_stop' => "TINYINT(1) NOT NULL DEFAULT 1 AFTER `content_version`",
'backup_before_install' => "TINYINT(1) NOT NULL DEFAULT 1 AFTER `requires_stop`",
'restart_after_install' => "TINYINT(1) NOT NULL DEFAULT 0 AFTER `backup_before_install`",
'is_cacheable' => "TINYINT(1) NOT NULL DEFAULT 0 AFTER `restart_after_install`",
'description' => "TEXT NULL AFTER `is_cacheable`",
);
foreach ($new_columns as $col => $definition) {
$escaped_col = $db->realEscapeSingle($col);
$escaped_table = $db->realEscapeSingle($prefix . 'addons');
$check = $db->resultQuery(
"SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = '{$escaped_table}'
AND COLUMN_NAME = '{$escaped_col}'"
);
if (empty($check)) {
if (!$db->query("ALTER TABLE `{$prefix}addons` ADD COLUMN `{$col}` {$definition}")) {
return false;
}
}
}
// ── Per-server installed-content manifest ─────────────────────────────
if (!$db->query(
"CREATE TABLE IF NOT EXISTS `{$prefix}server_content_manifest` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
`home_id` INT NOT NULL,
`addon_id` INT NOT NULL,
`install_method` VARCHAR(32) NOT NULL DEFAULT 'download_zip',
`content_version` VARCHAR(64) NULL,
`install_state` VARCHAR(32) NOT NULL DEFAULT 'installed',
`checksum_sha256` VARCHAR(64) NULL,
`source_url` VARCHAR(255) NULL,
`installed_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`installed_by` INT NULL,
`updated_at` DATETIME NULL,
`notes` TEXT NULL,
UNIQUE KEY `uniq_home_addon` (`home_id`, `addon_id`),
KEY `idx_home_id` (`home_id`),
KEY `idx_addon_id` (`addon_id`),
KEY `idx_install_state` (`install_state`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
)) {
return false;
}
// ── Install history (one row per install attempt) ─────────────────────
if (!$db->query(
"CREATE TABLE IF NOT EXISTS `{$prefix}server_content_install_history` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
`home_id` INT NOT NULL,
`addon_id` INT NOT NULL,
`installed_by` INT NULL,
`started_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`completed_at` DATETIME NULL,
`install_state` VARCHAR(32) NOT NULL DEFAULT 'started',
`install_method` VARCHAR(32) NULL,
`content_version` VARCHAR(64) NULL,
`source_url` VARCHAR(255) NULL,
`cache_mode_used` VARCHAR(32) NULL,
`result_code` INT NULL,
`log_output` MEDIUMTEXT NULL,
KEY `idx_home_id` (`home_id`),
KEY `idx_addon_id` (`addon_id`),
KEY `idx_started_at` (`started_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
)) {
return false;
}
return true;
},
);
// ── db_version 5 : content-type specific metadata for Workshop/config/folder actions ──
$install_queries[4] = array(
function ($db) {
$prefix = OGP_DB_PREFIX;
$new_columns = array(
'workshop_item_id' => "VARCHAR(64) NULL AFTER `description`",
'workshop_app_id' => "VARCHAR(32) NULL AFTER `workshop_item_id`",
'target_path_template' => "VARCHAR(255) NULL AFTER `workshop_app_id`",
'optional_folder_name' => "VARCHAR(255) NULL AFTER `target_path_template`",
'config_edit_rule' => "TEXT NULL AFTER `optional_folder_name`",
'launch_param_additions' => "VARCHAR(255) NULL AFTER `config_edit_rule`",
);
foreach ($new_columns as $col => $definition) {
$escaped_col = $db->realEscapeSingle($col);
$escaped_table = $db->realEscapeSingle($prefix . 'addons');
$check = $db->resultQuery(
"SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = '{$escaped_table}'
AND COLUMN_NAME = '{$escaped_col}'"
);
if (empty($check)) {
if (!$db->query("ALTER TABLE `{$prefix}addons` ADD COLUMN `{$col}` {$definition}")) {
return false;
}
}
}
return true;
},
);
// ── db_version 6 : admin template policy columns + content_id on workshop rows ──
//
// allow_user_workshop_ids whether users may enter their own IDs (default 1)
// max_workshop_ids optional cap on how many IDs a user may install
// required_workshop_ids JSON list of IDs that must always be installed
// blocked_workshop_ids JSON list of IDs that must not be installed
// content_id on server_content_workshop links a user install row back to
// the admin content template so the correct workshop_app_id is used.
//
$install_queries[5] = array(
function ($db) {
$prefix = OGP_DB_PREFIX;
// ── New policy columns on the addons (content template) table ─────────
$addon_columns = array(
'allow_user_workshop_ids' => "TINYINT(1) NOT NULL DEFAULT 1 AFTER `blocked_workshop_ids`",
'max_workshop_ids' => "INT NULL AFTER `allow_user_workshop_ids`",
'required_workshop_ids' => "TEXT NULL AFTER `max_workshop_ids`",
'blocked_workshop_ids' => "TEXT NULL AFTER `launch_param_additions`",
);
foreach ($addon_columns as $col => $definition) {
$escaped_col = $db->realEscapeSingle($col);
$escaped_table = $db->realEscapeSingle($prefix . 'addons');
$check = $db->resultQuery(
"SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = '{$escaped_table}'
AND COLUMN_NAME = '{$escaped_col}'"
);
if (empty($check)) {
if (!$db->query("ALTER TABLE `{$prefix}addons` ADD COLUMN `{$col}` {$definition}")) {
return false;
}
}
}
// ── content_id on server_content_workshop ─────────────────────────────
$wk_table = $db->realEscapeSingle($prefix . 'server_content_workshop');
$col_check = $db->resultQuery(
"SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = '{$wk_table}'
AND COLUMN_NAME = 'content_id'"
);
if (empty($col_check)) {
if (!$db->query(
"ALTER TABLE `{$prefix}server_content_workshop`
ADD COLUMN `content_id` INT NULL AFTER `id`,
ADD KEY `idx_content_id` (`content_id`)"
)) {
return false;
}
}
return true;
},
);
// ── db_version 7 : Workshop Phase 1 runtime tracking columns ────────────────
$install_queries[6] = array(
function ($db) {
$prefix = OGP_DB_PREFIX;
$table = $db->realEscapeSingle($prefix . 'server_content_workshop');
$columns = array(
'install_path' => "VARCHAR(512) NULL AFTER `title`",
'install_strategy' => "VARCHAR(64) NULL AFTER `install_path`",
'enabled' => "TINYINT(1) NOT NULL DEFAULT 1 AFTER `install_strategy`",
'load_order' => "INT NOT NULL DEFAULT 0 AFTER `enabled`",
);
foreach ($columns as $col => $definition) {
$check = $db->resultQuery(
"SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = '{$table}'
AND COLUMN_NAME = '" . $db->realEscapeSingle($col) . "'"
);
if (empty($check)) {
if (!$db->query("ALTER TABLE `{$prefix}server_content_workshop` ADD COLUMN `{$col}` {$definition}")) {
return false;
}
}
}
return true;
},
);
?>

View file

@ -2,5 +2,5 @@
<page key="user_addons" file="user_addons.php" access="admin,user" />
<page key="addons_manager" file="addons_manager.php" access="admin" />
<page key="addons" file="addons_installer.php" access="admin,user" />
<page key="workshop_content" file="workshop_content.php" access="admin,user" />
<page key="server_content_workshop_disabled" file="server_content_workshop_disabled.php" access="admin,user" />
</navigation>

View file

@ -0,0 +1,6 @@
<navigation>
<page key="user_addons" file="user_addons.php" access="admin,user" />
<page key="addons_manager" file="addons_manager.php" access="admin" />
<page key="addons" file="addons_installer.php" access="admin,user" />
<page key="workshop_content" file="workshop_content.php" access="admin,user" />
</navigation>

View file

@ -1,246 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
MANIFEST_PATH="${1:-}"
if [[ -z "$MANIFEST_PATH" ]]; then
echo "Usage: $0 <manifest_path>"
exit 1
fi
python3 - "$MANIFEST_PATH" <<'PY'
import json
import os
import shutil
import subprocess
import sys
import tempfile
from datetime import datetime
manifest_path = os.path.abspath(sys.argv[1])
if not os.path.isfile(manifest_path):
print(f"Manifest not found: {manifest_path}")
sys.exit(1)
manifest_dir = os.path.dirname(manifest_path)
home_root = os.path.dirname(manifest_dir)
log_file = os.path.join(manifest_dir, 'workshop_install.log')
removed_dir = os.path.join(manifest_dir, 'workshop', 'removed')
os.makedirs(removed_dir, exist_ok=True)
def log(message, status=None):
line = f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}]"
if status:
line += f" [{status}]"
line += f" {message}"
print(line)
with open(log_file, 'a', encoding='utf-8') as handle:
handle.write(line + "\n")
def fail(message):
log(message, 'Failed')
raise RuntimeError(message)
def uniq_numeric_items(raw_items):
seen = []
for value in raw_items:
text = str(value).strip()
if text.isdigit() and text not in seen:
seen.append(text)
return seen
def render_template(template, values):
rendered = str(template or '')
for key, value in values.items():
rendered = rendered.replace('{' + key + '}', str(value))
return rendered
def ensure_under_home(path_value):
target = os.path.abspath(path_value)
try:
common = os.path.commonpath([home_root, target])
except ValueError:
common = ''
if common != os.path.abspath(home_root):
fail(f"Refusing to write outside server home: {target}")
return target
def safe_folder_name(value, fallback):
text = str(value or '').strip()
if not text or '..' in text or '/' in text or '\\' in text or '\x00' in text:
return fallback
return text
def truthy(value):
if isinstance(value, bool):
return value
return str(value).strip().lower() in ('1', 'yes', 'true', 'on')
def resolve_steamcmd(explicit_path=''):
candidates = []
explicit_path = str(explicit_path or '').strip()
if explicit_path:
candidates.append(explicit_path)
env_value = os.environ.get('STEAMCMD_PATH', '').strip()
if env_value:
candidates.append(env_value)
for path_value in (
'/home/gameserver/steamcmd/steamcmd.sh',
shutil.which('steamcmd'),
shutil.which('steamcmd.exe'),
):
if path_value:
candidates.append(path_value)
for candidate in candidates:
if candidate and os.path.isfile(candidate):
return candidate
fail('SteamCMD is missing on the agent host.')
def sync_copy(src, dst):
if not os.path.isdir(src):
fail(f"Workshop download source was not found: {src}")
os.makedirs(dst, exist_ok=True)
for entry in os.listdir(src):
source_entry = os.path.join(src, entry)
target_entry = os.path.join(dst, entry)
if os.path.isdir(source_entry):
sync_copy(source_entry, target_entry)
else:
os.makedirs(os.path.dirname(target_entry), exist_ok=True)
shutil.copy2(source_entry, target_entry)
def copy_bikeys(mod_path, keys_target, workshop_id):
if not os.path.isdir(mod_path):
return 0
keys_target = ensure_under_home(keys_target)
os.makedirs(keys_target, exist_ok=True)
copied = 0
for root, dirs, files in os.walk(mod_path):
for filename in files:
if not filename.lower().endswith('.bikey'):
continue
source_file = os.path.join(root, filename)
target_file = os.path.join(keys_target, filename)
shutil.copy2(source_file, target_file)
copied += 1
log(f"workshop_id={workshop_id} key={target_file}", 'Copying Key')
if copied == 0:
log(f"workshop_id={workshop_id} no .bikey files found; continuing", 'Copying Key')
return copied
try:
with open(manifest_path, 'r', encoding='utf-8') as handle:
manifest = json.load(handle)
extra = manifest.get('extra') or {}
action = str(manifest.get('action', '')).strip()
raw_items = manifest.get('items', [])
if isinstance(raw_items, dict):
raw_items = raw_items.get('workshop_item_ids', [])
items = uniq_numeric_items(raw_items)
if not items:
fail('No Workshop IDs were found in the manifest.')
workshop_app_id = str(extra.get('workshop_app_id') or manifest.get('workshop_app_id') or '').strip()
steam_app_id = str(extra.get('steam_app_id') or manifest.get('steam_app_id') or '').strip()
server_root = ensure_under_home(extra.get('server_root') or home_root)
steamcmd_path = ''
if action in ('install', 'update', 'check_updates', 'download_only', 'validate_files'):
steamcmd_path = resolve_steamcmd(extra.get('steamcmd_path') or '')
post_install_script = str(extra.get('post_install_script') or '').strip()
item_details = manifest.get('item_details') or extra.get('item_details') or {}
default_install_strategy = str(manifest.get('install_strategy') or extra.get('install_strategy') or 'copy_to_mod_folder').strip()
default_download_dir = extra.get('workshop_download_dir') or os.path.join(server_root, 'steamapps', 'workshop', 'content', workshop_app_id or steam_app_id)
action_label = 'Queued' if action in ('install', 'update', 'check_updates', 'download_only', 'validate_files') else action
log(f"action={action} manifest={manifest_path} steam_app_id={steam_app_id or 'n/a'} workshop_app_id={workshop_app_id or 'n/a'}", action_label)
for workshop_id in items:
detail = item_details.get(workshop_id) or item_details.get(str(workshop_id)) or {}
folder_name = safe_folder_name(detail.get('folder_name') or extra.get('optional_folder_name') or '', '@' + workshop_id)
install_strategy = str(detail.get('install_strategy') or default_install_strategy).strip() or 'copy_to_mod_folder'
template_values = {
'HOME_ID': manifest.get('home_id', ''),
'SERVER_ROOT': server_root,
'GAME_ROOT': server_root,
'WORKSHOP_ID': workshop_id,
'WORKSHOP_APP_ID': workshop_app_id,
'STEAM_APP_ID': steam_app_id,
'FOLDER_NAME': folder_name,
'MOD_FOLDER': folder_name,
}
target_template = str(detail.get('target_path_template') or extra.get('target_path_template') or '{SERVER_ROOT}/workshop/{MOD_FOLDER}')
target_path = str(detail.get('target_path_resolved') or extra.get('target_path_resolved') or '').strip()
if len(items) != 1 or not target_path:
target_path = render_template(target_template, template_values)
target_path = ensure_under_home(target_path)
keys_target_path = str(detail.get('keys_target_path') or extra.get('keys_target_path') or os.path.join(server_root, 'keys'))
should_copy_keys = truthy(detail.get('copy_keys', extra.get('copy_keys', install_strategy in ('dayz_mod_folder', 'arma_mod_folder'))))
download_dir = ensure_under_home(render_template(default_download_dir, template_values))
source_path = os.path.join(download_dir, workshop_id)
if action in ('install', 'update', 'check_updates', 'download_only', 'validate_files'):
if not workshop_app_id:
fail(f"Workshop App ID is missing for Workshop item {workshop_id}.")
fd, runscript_path = tempfile.mkstemp(prefix=f"steamcmd_workshop_{workshop_id}_", suffix=".txt", dir=manifest_dir, text=True)
with os.fdopen(fd, 'w', encoding='utf-8') as script_handle:
script_handle.write("@ShutdownOnFailedCommand 0\n")
script_handle.write("@NoPromptForPassword 1\n")
script_handle.write(f"force_install_dir {server_root}\n")
script_handle.write("login anonymous\n")
script_handle.write(f"workshop_download_item {workshop_app_id} {workshop_id} validate\n")
script_handle.write("quit\n")
command = [steamcmd_path, '+runscript', runscript_path]
log(f"workshop_id={workshop_id} runscript={runscript_path}", 'Downloading Workshop Item')
result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, cwd=server_root)
if result.stdout:
for line in result.stdout.splitlines():
log(f"steamcmd[{workshop_id}] {line}")
if result.returncode != 0:
fail(f"SteamCMD failed for Workshop item {workshop_id} with exit code {result.returncode}.")
if not os.path.isdir(source_path):
fail(f"SteamCMD did not create the expected Workshop cache path: {source_path}")
if action not in ('check_updates', 'download_only', 'validate_files'):
log(f"workshop_id={workshop_id} strategy={install_strategy} install_path={target_path}", 'Extracting/Copying')
sync_copy(source_path, target_path)
log(f"workshop_id={workshop_id} final_folder_path={target_path}", 'Applying Folder Name')
if should_copy_keys:
copy_bikeys(target_path, keys_target_path, workshop_id)
if post_install_script:
log(f"workshop_id={workshop_id} cwd={server_root}", 'Running Post-install Script')
post_result = subprocess.run(['bash', '-lc', post_install_script], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, cwd=server_root)
if post_result.stdout:
for line in post_result.stdout.splitlines():
log(f"post_install[{workshop_id}] {line}")
if post_result.returncode != 0:
fail(f"Post-install script failed for Workshop item {workshop_id} with exit code {post_result.returncode}.")
elif action == 'download_only':
log(f"workshop_id={workshop_id} cached_path={source_path}", 'Downloaded Only')
elif action == 'validate_files':
log(f"workshop_id={workshop_id} cached_path={source_path}", 'Validated')
log(f"workshop_id={workshop_id} install_path={target_path}", 'Completed')
elif action == 'remove':
if os.path.exists(target_path):
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
removed_path = os.path.join(removed_dir, f"{workshop_id}_{timestamp}")
shutil.move(target_path, removed_path)
log(f"workshop_id={workshop_id} removed_path={removed_path}", 'Completed')
else:
log(f"workshop_id={workshop_id} target_path_missing={target_path}", 'Completed')
else:
fail(f"Unknown workshop action: {action}")
except RuntimeError:
sys.exit(1)
PY

View file

@ -1,247 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
MANIFEST_PATH="${1:-}"
if [[ -z "$MANIFEST_PATH" ]]; then
echo "Usage: $0 <manifest_path>"
exit 1
fi
python3 - "$MANIFEST_PATH" <<'PY'
import json
import os
import shutil
import subprocess
import sys
import tempfile
from datetime import datetime
manifest_path = os.path.abspath(sys.argv[1])
if not os.path.isfile(manifest_path):
print(f"Manifest not found: {manifest_path}")
sys.exit(1)
manifest_dir = os.path.dirname(manifest_path)
home_root = os.path.dirname(manifest_dir)
log_file = os.path.join(manifest_dir, 'workshop_install_windows.log')
removed_dir = os.path.join(manifest_dir, 'workshop', 'removed')
os.makedirs(removed_dir, exist_ok=True)
def log(message, status=None):
line = f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}]"
if status:
line += f" [{status}]"
line += f" {message}"
print(line)
with open(log_file, 'a', encoding='utf-8') as handle:
handle.write(line + "\n")
def fail(message):
log(message, 'Failed')
raise RuntimeError(message)
def uniq_numeric_items(raw_items):
seen = []
for value in raw_items:
text = str(value).strip()
if text.isdigit() and text not in seen:
seen.append(text)
return seen
def render_template(template, values):
rendered = str(template or '')
for key, value in values.items():
rendered = rendered.replace('{' + key + '}', str(value))
return rendered
def ensure_under_home(path_value):
target = os.path.abspath(path_value)
try:
common = os.path.commonpath([home_root, target])
except ValueError:
common = ''
if common != os.path.abspath(home_root):
fail(f"Refusing to write outside server home: {target}")
return target
def safe_folder_name(value, fallback):
text = str(value or '').strip()
if not text or '..' in text or '/' in text or '\\' in text or '\x00' in text:
return fallback
return text
def truthy(value):
if isinstance(value, bool):
return value
return str(value).strip().lower() in ('1', 'yes', 'true', 'on')
def resolve_steamcmd(explicit_path=''):
candidates = []
explicit_path = str(explicit_path or '').strip()
if explicit_path:
candidates.append(explicit_path)
env_value = os.environ.get('STEAMCMD_PATH', '').strip()
if env_value:
candidates.append(env_value)
for path_value in (
'/home/gameserver/steamcmd/steamcmd.sh',
shutil.which('steamcmd'),
shutil.which('steamcmd.exe'),
shutil.which('steamcmd.sh'),
):
if path_value:
candidates.append(path_value)
for candidate in candidates:
if candidate and os.path.isfile(candidate):
return candidate
fail('SteamCMD is missing on the agent host.')
def sync_copy(src, dst):
if not os.path.isdir(src):
fail(f"Workshop download source was not found: {src}")
os.makedirs(dst, exist_ok=True)
for entry in os.listdir(src):
source_entry = os.path.join(src, entry)
target_entry = os.path.join(dst, entry)
if os.path.isdir(source_entry):
sync_copy(source_entry, target_entry)
else:
os.makedirs(os.path.dirname(target_entry), exist_ok=True)
shutil.copy2(source_entry, target_entry)
def copy_bikeys(mod_path, keys_target, workshop_id):
if not os.path.isdir(mod_path):
return 0
keys_target = ensure_under_home(keys_target)
os.makedirs(keys_target, exist_ok=True)
copied = 0
for root, dirs, files in os.walk(mod_path):
for filename in files:
if not filename.lower().endswith('.bikey'):
continue
source_file = os.path.join(root, filename)
target_file = os.path.join(keys_target, filename)
shutil.copy2(source_file, target_file)
copied += 1
log(f"workshop_id={workshop_id} key={target_file}", 'Copying Key')
if copied == 0:
log(f"workshop_id={workshop_id} no .bikey files found; continuing", 'Copying Key')
return copied
try:
with open(manifest_path, 'r', encoding='utf-8') as handle:
manifest = json.load(handle)
extra = manifest.get('extra') or {}
action = str(manifest.get('action', '')).strip()
raw_items = manifest.get('items', [])
if isinstance(raw_items, dict):
raw_items = raw_items.get('workshop_item_ids', [])
items = uniq_numeric_items(raw_items)
if not items:
fail('No Workshop IDs were found in the manifest.')
workshop_app_id = str(extra.get('workshop_app_id') or manifest.get('workshop_app_id') or '').strip()
steam_app_id = str(extra.get('steam_app_id') or manifest.get('steam_app_id') or '').strip()
server_root = ensure_under_home(extra.get('server_root') or home_root)
steamcmd_path = ''
if action in ('install', 'update', 'check_updates', 'download_only', 'validate_files'):
steamcmd_path = resolve_steamcmd(extra.get('steamcmd_path') or '')
post_install_script = str(extra.get('post_install_script') or '').strip()
item_details = manifest.get('item_details') or extra.get('item_details') or {}
default_install_strategy = str(manifest.get('install_strategy') or extra.get('install_strategy') or 'copy_to_mod_folder').strip()
default_download_dir = extra.get('workshop_download_dir') or os.path.join(server_root, 'steamapps', 'workshop', 'content', workshop_app_id or steam_app_id)
action_label = 'Queued' if action in ('install', 'update', 'check_updates', 'download_only', 'validate_files') else action
log(f"action={action} manifest={manifest_path} steam_app_id={steam_app_id or 'n/a'} workshop_app_id={workshop_app_id or 'n/a'}", action_label)
for workshop_id in items:
detail = item_details.get(workshop_id) or item_details.get(str(workshop_id)) or {}
folder_name = safe_folder_name(detail.get('folder_name') or extra.get('optional_folder_name') or '', '@' + workshop_id)
install_strategy = str(detail.get('install_strategy') or default_install_strategy).strip() or 'copy_to_mod_folder'
template_values = {
'HOME_ID': manifest.get('home_id', ''),
'SERVER_ROOT': server_root,
'GAME_ROOT': server_root,
'WORKSHOP_ID': workshop_id,
'WORKSHOP_APP_ID': workshop_app_id,
'STEAM_APP_ID': steam_app_id,
'FOLDER_NAME': folder_name,
'MOD_FOLDER': folder_name,
}
target_template = str(detail.get('target_path_template') or extra.get('target_path_template') or '{SERVER_ROOT}/workshop/{MOD_FOLDER}')
target_path = str(detail.get('target_path_resolved') or extra.get('target_path_resolved') or '').strip()
if len(items) != 1 or not target_path:
target_path = render_template(target_template, template_values)
target_path = ensure_under_home(target_path)
keys_target_path = str(detail.get('keys_target_path') or extra.get('keys_target_path') or os.path.join(server_root, 'keys'))
should_copy_keys = truthy(detail.get('copy_keys', extra.get('copy_keys', install_strategy in ('dayz_mod_folder', 'arma_mod_folder'))))
download_dir = ensure_under_home(render_template(default_download_dir, template_values))
source_path = os.path.join(download_dir, workshop_id)
if action in ('install', 'update', 'check_updates', 'download_only', 'validate_files'):
if not workshop_app_id:
fail(f"Workshop App ID is missing for Workshop item {workshop_id}.")
fd, runscript_path = tempfile.mkstemp(prefix=f"steamcmd_workshop_{workshop_id}_", suffix=".txt", dir=manifest_dir, text=True)
with os.fdopen(fd, 'w', encoding='utf-8') as script_handle:
script_handle.write("@ShutdownOnFailedCommand 0\n")
script_handle.write("@NoPromptForPassword 1\n")
script_handle.write(f"force_install_dir {server_root}\n")
script_handle.write("login anonymous\n")
script_handle.write(f"workshop_download_item {workshop_app_id} {workshop_id} validate\n")
script_handle.write("quit\n")
command = [steamcmd_path, '+runscript', runscript_path]
log(f"workshop_id={workshop_id} runscript={runscript_path}", 'Downloading Workshop Item')
result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, cwd=server_root)
if result.stdout:
for line in result.stdout.splitlines():
log(f"steamcmd[{workshop_id}] {line}")
if result.returncode != 0:
fail(f"SteamCMD failed for Workshop item {workshop_id} with exit code {result.returncode}.")
if not os.path.isdir(source_path):
fail(f"SteamCMD did not create the expected Workshop cache path: {source_path}")
if action not in ('check_updates', 'download_only', 'validate_files'):
log(f"workshop_id={workshop_id} strategy={install_strategy} install_path={target_path}", 'Extracting/Copying')
sync_copy(source_path, target_path)
log(f"workshop_id={workshop_id} final_folder_path={target_path}", 'Applying Folder Name')
if should_copy_keys:
copy_bikeys(target_path, keys_target_path, workshop_id)
if post_install_script:
log(f"workshop_id={workshop_id} cwd={server_root}", 'Running Post-install Script')
post_result = subprocess.run(['bash', '-lc', post_install_script], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, cwd=server_root)
if post_result.stdout:
for line in post_result.stdout.splitlines():
log(f"post_install[{workshop_id}] {line}")
if post_result.returncode != 0:
fail(f"Post-install script failed for Workshop item {workshop_id} with exit code {post_result.returncode}.")
elif action == 'download_only':
log(f"workshop_id={workshop_id} cached_path={source_path}", 'Downloaded Only')
elif action == 'validate_files':
log(f"workshop_id={workshop_id} cached_path={source_path}", 'Validated')
log(f"workshop_id={workshop_id} install_path={target_path}", 'Completed')
elif action == 'remove':
if os.path.exists(target_path):
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
removed_path = os.path.join(removed_dir, f"{workshop_id}_{timestamp}")
shutil.move(target_path, removed_path)
log(f"workshop_id={workshop_id} removed_path={removed_path}", 'Completed')
else:
log(f"workshop_id={workshop_id} target_path_missing={target_path}", 'Completed')
else:
fail(f"Unknown workshop action: {action}")
except RuntimeError:
sys.exit(1)
PY

View file

@ -1,137 +0,0 @@
#!/usr/bin/env bash
set -u
MANIFEST="${1:-}"
if [ -z "$MANIFEST" ] || [ ! -f "$MANIFEST" ]; then
echo "ERROR: Workshop manifest missing: $MANIFEST"
exit 2
fi
MANIFEST_DIR="$(dirname "$MANIFEST")"
LOG="$MANIFEST_DIR/workshop_install.log"
touch "$LOG"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG"
}
json_value() {
local key="$1"
KEY="$key" MANIFEST_FILE="$MANIFEST" perl -0777 -e '
my $k = $ENV{"KEY"};
my $f = $ENV{"MANIFEST_FILE"};
open my $fh, "<", $f or exit 1;
local $/;
my $json = <$fh>;
if ($json =~ /"\Q$k\E"\s*:\s*"([^"]*)"/s) {
my $v = $1;
$v =~ s/\\\//\//g;
print $v;
exit 0;
}
if ($json =~ /"\Q$k\E"\s*:\s*([0-9]+)/s) {
print $1;
exit 0;
}
exit 0;
'
}
json_items() {
MANIFEST_FILE="$MANIFEST" perl -0777 -e '
my $f = $ENV{"MANIFEST_FILE"};
open my $fh, "<", $f or exit 1;
local $/;
my $json = <$fh>;
my %seen;
if ($json =~ /"items"\s*:\s*\[(.*?)\]/s) {
my $x = $1;
while ($x =~ /"([0-9]{3,20})"/g) {
print "$1\n" unless $seen{$1}++;
}
while ($x =~ /(?<![0-9])([0-9]{3,20})(?![0-9])/g) {
print "$1\n" unless $seen{$1}++;
}
}
'
}
ACTION="$(json_value action)"
APPID="$(json_value workshop_app_id)"
STEAM_APPID="$(json_value steam_app_id)"
SERVER_PATH="$(json_value server_path)"
[ -z "$ACTION" ] && ACTION="install"
[ -z "$APPID" ] && APPID="$STEAM_APPID"
[ -z "$SERVER_PATH" ] && SERVER_PATH="$(pwd)"
if [ -z "$APPID" ]; then
log "ERROR: workshop_app_id missing from manifest."
echo "Manifest debug:"
sed -n '1,80p' "$MANIFEST"
exit 3
fi
ITEMS="$(json_items)"
if [ -z "$ITEMS" ]; then
log "ERROR: no Workshop item IDs found in manifest."
echo "Manifest debug:"
sed -n '1,80p' "$MANIFEST"
exit 4
fi
find_steamcmd() {
for c in "${STEAMCMD_PATH:-}" steamcmd steamcmd.sh steamcmd.exe \
/OGP/steamcmd/steamcmd.exe /OGP/steamcmd/steamcmd.sh \
"$SERVER_PATH/steamcmd/steamcmd.exe" "$SERVER_PATH/steamcmd/steamcmd.sh" \
/home/gameserver/steamcmd/steamcmd.sh "$HOME/steamcmd/steamcmd.sh" "$HOME/steamcmd/steamcmd.exe"
do
[ -z "$c" ] && continue
if command -v "$c" >/dev/null 2>&1; then command -v "$c"; return 0; fi
if [ -f "$c" ]; then echo "$c"; return 0; fi
done
return 1
}
log "GSP Workshop job starting. action=$ACTION appid=$APPID server_path=$SERVER_PATH"
if [ "$ACTION" = "remove" ]; then
for id in $ITEMS; do
log "Removing Workshop item files for $id if present."
rm -rf "$SERVER_PATH/@$id" "$SERVER_PATH/workshop/@$id" "$SERVER_PATH/steamapps/workshop/content/$APPID/$id"
done
log "Remove job complete."
exit 0
fi
STEAMCMD="$(find_steamcmd)" || {
log "ERROR: steamcmd was not found. Install SteamCMD on the agent host or set STEAMCMD_PATH."
exit 127
}
RUNSCRIPT="$MANIFEST_DIR/steamcmd_workshop_$$.txt"
{
echo "@ShutdownOnFailedCommand 0"
echo "@NoPromptForPassword 1"
echo "login anonymous"
echo "force_install_dir $SERVER_PATH"
for id in $ITEMS; do
echo "workshop_download_item $APPID $id validate"
done
echo "quit"
} > "$RUNSCRIPT"
log "Using SteamCMD: $STEAMCMD"
log "Running SteamCMD runscript: $RUNSCRIPT"
"$STEAMCMD" +runscript "$RUNSCRIPT" 2>&1 | tee -a "$LOG"
rc=${PIPESTATUS[0]}
if [ "$rc" -ne 0 ]; then
log "ERROR: SteamCMD failed with exit $rc"
exit "$rc"
fi
for id in $ITEMS; do
SRC="$SERVER_PATH/steamapps/workshop/content/$APPID/$id"
DST="$SERVER_PATH/@$id"
if [ ! -d "$SRC" ]; then
log "ERROR: downloaded Workshop source not found: $SRC"
exit 5
fi
log "Installing Workshop item $id to $DST"
rm -rf "$DST"
cp -a "$SRC" "$DST"
if [ -d "$DST/keys" ]; then
mkdir -p "$SERVER_PATH/keys"
find "$DST/keys" -type f -iname '*.bikey' -exec cp -f {} "$SERVER_PATH/keys/" \;
fi
done
log "Workshop job complete."
exit 0

View file

@ -694,7 +694,6 @@ function scm_get_install_methods()
{
return array(
'download_zip' => 'Downloadable Mod',
'steam_workshop' => 'Steam Workshop Mods',
'config_edit' => 'Configuration Package',
'post_script' => 'Scripted Installer',
);
@ -704,7 +703,6 @@ function scm_get_install_method_help_text()
{
return array(
'download_zip' => 'Download and extract a ZIP, RAR, or archive file.',
'steam_workshop' => 'Users install Steam Workshop items from their server page. App IDs, install paths, key-copy rules, and launch parameter format come from the game XML.',
'config_edit' => 'Install configuration files, profiles, or templates.',
'post_script' => 'Run a custom scripted installation process.',
);
@ -724,7 +722,6 @@ function scm_get_install_method_validation_errors()
{
return array(
'download_zip' => 'Please enter a download URL.',
'steam_workshop' => 'Workshop behavior is configured in the game XML.',
'config_edit' => 'Please enter the config target and edit action.',
'post_script' => 'Please enter the installer script/action.',
);

File diff suppressed because it is too large Load diff

View file

@ -1,487 +1,11 @@
<?php
/*
*
* GSP - Workshop Content actions (Phase 1)
*
/**
* Steam Workshop was removed from Server Content.
* Use Panel/modules/steam_workshop instead.
*/
require_once("includes/lib_remote.php");
require_once("modules/config_games/server_config_parser.php");
require_once(dirname(__FILE__) . '/server_content_helpers.php');
function scm_workshop_log_action($db, $home_id, $user_id, $message)
function scm_workshop_disabled_message()
{
$db->logger("server_content_workshop home_id=".(int)$home_id." user_id=".(int)$user_id." ".$message);
}
function scm_workshop_update_rows_state($db, $home_id, array $item_ids, $state, $error = null, $mark_install = false, $mark_update = false)
{
if (empty($item_ids)) {
return true;
}
$escaped_ids = array();
foreach ($item_ids as $item_id) {
$escaped_ids[] = "'" . $db->realEscapeSingle((string)$item_id) . "'";
}
$set = array(
"install_state='" . $db->realEscapeSingle($state) . "'",
"updated_at=NOW()",
);
if ($mark_install) {
$set[] = "last_installed_at=NOW()";
}
if ($mark_update) {
$set[] = "last_updated_at=NOW()";
}
if ($error === null) {
$set[] = "last_error=NULL";
} else {
$set[] = "last_error='" . $db->realEscapeSingle($error) . "'";
}
$query = "UPDATE `".OGP_DB_PREFIX."server_content_workshop`
SET ".implode(", ", $set)."
WHERE home_id=".(int)$home_id." AND workshop_item_id IN (".implode(",", $escaped_ids).")";
return (bool)$db->query($query);
}
function scm_workshop_filter_existing_ids($db, $home_id, array $item_ids)
{
if (empty($item_ids)) {
return array();
}
$escaped_ids = array();
foreach ($item_ids as $item_id) {
$escaped_ids[] = "'" . $db->realEscapeSingle((string)$item_id) . "'";
}
$rows = $db->resultQuery(
"SELECT workshop_item_id FROM `".OGP_DB_PREFIX."server_content_workshop`
WHERE home_id=".(int)$home_id." AND workshop_item_id IN (".implode(",", $escaped_ids).")"
);
$allowed = array();
if (is_array($rows)) {
foreach ((array)$rows as $row) {
$allowed[(string)$row['workshop_item_id']] = (string)$row['workshop_item_id'];
}
}
return array_values($allowed);
}
function scm_workshop_get_content_template($db, $addon_id)
{
$addon_id = (int)$addon_id;
if ($addon_id <= 0) {
return array();
}
scm_ensure_phase2_schema($db);
$rows = $db->resultQuery(
"SELECT addon_id, name, content_version, description
FROM `" . OGP_DB_PREFIX . "addons`
WHERE addon_id=" . $addon_id . " AND install_method='steam_workshop'
LIMIT 1"
);
return (is_array($rows) && !empty($rows)) ? $rows[0] : array();
}
function scm_workshop_build_manifest_context($db, array $home_info, $server_xml, array $item_ids, array $template = array())
{
$install_strategy = scm_detect_workshop_install_strategy($home_info, $server_xml, $template);
$copy_keys = scm_workshop_should_copy_keys($server_xml, $install_strategy);
$xml_install_path = scm_extract_workshop_install_path($server_xml);
$keys_target_path = scm_workshop_keys_target_path($server_xml, $home_info);
$item_details = array();
$resolved_app_id = '';
$steam_app_id = '';
foreach ($item_ids as $item_id) {
$payload = array(
'workshop_item_id' => (string)$item_id,
'install_strategy' => $install_strategy,
);
$message = '';
$runtime = scm_build_workshop_runtime_context($db, $home_info, $server_xml, $payload, $message);
if ($runtime === false) {
$runtime = array();
}
$item_app_id = isset($runtime['workshop_app_id']) ? (string)$runtime['workshop_app_id'] : '';
if ($resolved_app_id === '' && $item_app_id !== '') {
$resolved_app_id = $item_app_id;
}
if ($steam_app_id === '' && !empty($runtime['steam_app_id'])) {
$steam_app_id = (string)$runtime['steam_app_id'];
}
$item_details[(string)$item_id] = array(
'workshop_item_id' => (string)$item_id,
'title' => '',
'folder_name' => isset($runtime['folder_name']) && $runtime['folder_name'] !== '' ? (string)$runtime['folder_name'] : '@' . $item_id,
'target_path_template' => isset($runtime['target_path_template']) ? (string)$runtime['target_path_template'] : scm_get_default_workshop_target_template($install_strategy),
'target_path_resolved' => isset($runtime['target_path_resolved']) ? (string)$runtime['target_path_resolved'] : '',
'install_strategy' => $install_strategy,
'copy_keys' => $copy_keys ? 1 : 0,
'keys_target_path' => $keys_target_path,
);
}
if ($resolved_app_id === '') {
$resolved_app_id = scm_extract_workshop_app_id($server_xml);
}
if ($steam_app_id === '') {
$steam_app_id = scm_extract_workshop_steam_app_id($server_xml);
}
return array(
'workshop_app_id' => $resolved_app_id,
'steam_app_id' => $steam_app_id,
'server_root' => rtrim((string)$home_info['home_path'], '/'),
'install_strategy' => $install_strategy,
'copy_keys' => $copy_keys ? 1 : 0,
'target_path_template' => $xml_install_path !== '' ? $xml_install_path : scm_get_default_workshop_target_template($install_strategy),
'keys_target_path' => $keys_target_path,
'post_install_script' => scm_workshop_post_install_action($server_xml),
'launch_param_additions' => isset($server_xml->workshop_support->startup_param_format) ? trim((string)$server_xml->workshop_support->startup_param_format) : '',
'content_template_id' => isset($template['addon_id']) ? (int)$template['addon_id'] : 0,
'content_template_name' => isset($template['name']) ? (string)$template['name'] : '',
'item_details' => $item_details,
);
}
function scm_workshop_write_manifest_and_run($db, array $home_info, $server_xml, $action, array $item_ids, &$error = '', array $extra_manifest = array(), &$result_details = array())
{
$error = '';
$result_details = array();
if (empty($item_ids)) {
$error = 'No Workshop IDs were selected for this action.';
return false;
}
$manifest_path = scm_get_workshop_manifest_path($home_info);
if ($manifest_path === false) {
$error = 'Manifest path validation failed for this server home.';
return false;
}
$home_path = rtrim(clean_path((string)$home_info['home_path']), '/');
if (!scm_path_is_under_home($home_path, $manifest_path)) {
$error = 'Manifest path is outside of the server home.';
return false;
}
$manifest_dir = dirname($manifest_path);
$workshop_app_id = !empty($extra_manifest['workshop_app_id']) ? (string)$extra_manifest['workshop_app_id'] : scm_extract_workshop_app_id($server_xml);
$steam_app_id = !empty($extra_manifest['steam_app_id']) ? (string)$extra_manifest['steam_app_id'] : scm_extract_workshop_steam_app_id($server_xml);
if ($workshop_app_id === '' && $steam_app_id !== '') {
$workshop_app_id = $steam_app_id;
}
if ($steam_app_id === '' && $workshop_app_id !== '') {
$steam_app_id = $workshop_app_id;
}
if ($workshop_app_id === '' || !preg_match('/^[0-9]+$/', $workshop_app_id)) {
$error = 'Workshop App ID is missing from the game XML workshop_support block.';
return false;
}
$manifest = array(
'manifest_version' => 1,
'action' => (string)$action,
'home_id' => (int)$home_info['home_id'],
'home_cfg_id' => (int)$home_info['home_cfg_id'],
'game_path' => $home_path,
'server_path' => $home_path,
'workshop_app_id' => $workshop_app_id,
'steam_app_id' => $steam_app_id,
'items' => array_values($item_ids),
'item_details' => !empty($extra_manifest['item_details']) && is_array($extra_manifest['item_details']) ? $extra_manifest['item_details'] : array(),
'install_strategy' => !empty($extra_manifest['install_strategy']) ? (string)$extra_manifest['install_strategy'] : '',
'target_path' => !empty($extra_manifest['target_path_template']) ? (string)$extra_manifest['target_path_template'] : scm_get_default_workshop_target_template(!empty($extra_manifest['install_strategy']) ? (string)$extra_manifest['install_strategy'] : ''),
'generated_at' => date('Y-m-d H:i:s'),
);
if (!empty($extra_manifest)) {
$manifest['extra'] = $extra_manifest;
}
$manifest_json = json_encode($manifest);
if ($manifest_json === false) {
$error = 'Failed to encode workshop manifest JSON.';
return false;
}
$remote = new OGPRemoteLibrary(
$home_info['agent_ip'],
$home_info['agent_port'],
$home_info['encryption_key'],
$home_info['timeout']
);
$remote->exec("mkdir -p " . escapeshellarg($manifest_dir));
if ((int)$remote->remote_writefile($manifest_path, $manifest_json) !== 1) {
$error = 'Failed to write workshop manifest to remote server.';
return false;
}
$script_path = scm_prepare_workshop_script_for_agent($remote, $home_info, $server_xml, $error);
if ($script_path === false) {
return false;
}
$command = "bash " . escapeshellarg($script_path) . " " . escapeshellarg($manifest_path) . " ; echo __GSP_WORKSHOP_EXIT:$?";
$output = $remote->exec($command);
if (!is_string($output) || $output === '') {
$error = 'Workshop script did not return an execution status.';
return false;
}
if (!preg_match('/__GSP_WORKSHOP_EXIT:(\d+)/', $output, $matches)) {
$error = 'Workshop script exit marker not found in output.';
return false;
}
$result_details = array(
'manifest_path' => $manifest_path,
'script_path' => $script_path,
'log_path' => clean_path($manifest_dir . (scm_is_windows_home($home_info) ? '/workshop_install_windows.log' : '/workshop_install.log')),
'output' => trim(preg_replace('/__GSP_WORKSHOP_EXIT:\d+/', '', $output)),
);
$exit_code = (int)$matches[1];
if ($exit_code !== 0) {
$error = 'Workshop script failed (exit '.$exit_code.'): '.trim($output);
return false;
}
return true;
}
function scm_workshop_handle_action($db, array $home_info, $user_id, $action, $raw_ids, array $selected_ids, &$message, &$is_error, $addon_id = 0, array $options = array())
{
$message = '';
$is_error = true;
if (!scm_ensure_workshop_schema($db)) {
$message = 'Workshop schema migration failed.';
return false;
}
scm_ensure_phase2_schema($db);
$home_id = (int)$home_info['home_id'];
$user_id = (int)$user_id;
$addon_id = (int)$addon_id;
$server_xml = read_server_config(SERVER_CONFIG_LOCATION . "/" . $home_info['home_cfg_file']);
if ($server_xml === false) {
$message = 'Unable to read server configuration for workshop action.';
return false;
}
if (!scm_workshop_is_supported($server_xml)) {
$message = 'This game XML does not enable Steam Workshop support. Add a valid workshop_support block before installing Workshop items.';
return false;
}
$template = scm_workshop_get_content_template($db, $addon_id);
if ($action === 'install_new') {
$invalid = array();
$item_ids = scm_parse_workshop_ids($raw_ids, $invalid);
if (!empty($invalid)) {
$message = 'Invalid Workshop item entries. Use a numeric Workshop ID or Steam Workshop URL: ' . implode(', ', $invalid);
return false;
}
if (empty($item_ids)) {
$message = 'Enter at least one Steam Workshop ID or Workshop URL.';
return false;
}
$manifest_context = scm_workshop_build_manifest_context($db, $home_info, $server_xml, $item_ids, $template);
$resolved_app_id = isset($manifest_context['workshop_app_id']) ? (string)$manifest_context['workshop_app_id'] : '';
if ($resolved_app_id === '') {
$message = 'Workshop App ID is missing from the game XML workshop_support block.';
return false;
}
// Check whether the content_id column exists (added in db_version 6).
$has_content_id_col = (bool)$db->resultQuery(
"SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = '" . $db->realEscapeSingle(OGP_DB_PREFIX . 'server_content_workshop') . "'
AND COLUMN_NAME = 'content_id'"
);
$next_order_rows = $db->resultQuery(
"SELECT COALESCE(MAX(load_order), 0) AS max_order FROM `".OGP_DB_PREFIX."server_content_workshop`
WHERE home_id=".$home_id
);
$next_order = (is_array($next_order_rows) && isset($next_order_rows[0]['max_order'])) ? (int)$next_order_rows[0]['max_order'] : 0;
foreach ($item_ids as $item_id) {
$item_detail = isset($manifest_context['item_details'][(string)$item_id]) ? $manifest_context['item_details'][(string)$item_id] : array();
$install_path = isset($item_detail['target_path_resolved']) ? (string)$item_detail['target_path_resolved'] : '';
$install_strategy = isset($item_detail['install_strategy']) ? (string)$item_detail['install_strategy'] : (string)$manifest_context['install_strategy'];
$next_order++;
$content_id_col = $has_content_id_col && $addon_id > 0 ? ", content_id" : '';
$content_id_val = $has_content_id_col && $addon_id > 0 ? ", " . $addon_id : '';
$content_id_upd = $has_content_id_col && $addon_id > 0 ? ", content_id=VALUES(content_id)" : '';
$query = "INSERT INTO `".OGP_DB_PREFIX."server_content_workshop`
(home_id, home_cfg_id, remote_server_id, workshop_app_id, workshop_item_id, install_path, install_strategy, enabled, load_order, install_state, created_by, created_at, updated_at" . $content_id_col . ")
VALUES (
".$home_id.",
".(int)$home_info['home_cfg_id'].",
".(int)$home_info['remote_server_id'].",
'".$db->realEscapeSingle($resolved_app_id)."',
'".$db->realEscapeSingle($item_id)."',
'".$db->realEscapeSingle($install_path)."',
'".$db->realEscapeSingle($install_strategy)."',
1,
".$next_order.",
'queued',
".$user_id.",
NOW(),
NOW()
" . $content_id_val . "
)
ON DUPLICATE KEY UPDATE
home_cfg_id=VALUES(home_cfg_id),
remote_server_id=VALUES(remote_server_id),
workshop_app_id=VALUES(workshop_app_id),
install_path=VALUES(install_path),
install_strategy=VALUES(install_strategy),
enabled=1,
install_state='queued',
last_error=NULL,
updated_at=NOW()" . $content_id_upd;
$db->query($query);
}
scm_workshop_update_rows_state($db, $home_id, $item_ids, 'installing', null, false, false);
$error = '';
$details = array();
$ok = scm_workshop_write_manifest_and_run($db, $home_info, $server_xml, 'install', $item_ids, $error, $manifest_context, $details);
if ($ok) {
scm_workshop_update_rows_state($db, $home_id, $item_ids, 'installed', null, true, true);
scm_workshop_record_catalog_items($db, $resolved_app_id, $item_ids, $home_info, isset($manifest_context['item_details']) ? $manifest_context['item_details'] : array(), true);
scm_workshop_log_action($db, $home_id, $user_id, "install_new ids=".implode(',', $item_ids)." addon_id=".$addon_id." status=success");
$is_error = false;
$message = 'Workshop item(s) installed successfully. Manifest: '.scm_h(isset($details['manifest_path']) ? $details['manifest_path'] : '').' Log: '.scm_h(isset($details['log_path']) ? $details['log_path'] : '');
return true;
}
scm_workshop_update_rows_state($db, $home_id, $item_ids, 'failed', $error, false, false);
scm_workshop_log_action($db, $home_id, $user_id, "install_new ids=".implode(',', $item_ids)." addon_id=".$addon_id." status=failed error=".$error);
$message = $error;
return false;
}
if ($action === 'enable_selected' || $action === 'disable_selected' || $action === 'save_update_policy') {
$item_ids = scm_workshop_filter_existing_ids($db, $home_id, scm_parse_selected_workshop_ids($selected_ids));
if (empty($item_ids)) {
$message = 'Select one or more saved Workshop IDs.';
return false;
}
if ($action === 'save_update_policy') {
$policy = isset($options['update_policy']) ? (string)$options['update_policy'] : 'manual';
if (!scm_workshop_set_update_policy($db, $home_id, $item_ids, $policy)) {
$message = 'Failed to save Workshop update policy.';
return false;
}
$is_error = false;
$message = 'Workshop update policy saved for selected item(s).';
return true;
}
$enabled = ($action === 'enable_selected') ? 1 : 0;
if (!scm_workshop_set_enabled($db, $home_id, $item_ids, $enabled)) {
$message = 'Failed to update Workshop enabled state.';
return false;
}
$is_error = false;
$message = $enabled ? 'Selected Workshop item(s) enabled.' : 'Selected Workshop item(s) disabled.';
return true;
}
if ($action === 'remove_selected') {
$item_ids = scm_workshop_filter_existing_ids($db, $home_id, scm_parse_selected_workshop_ids($selected_ids));
if (empty($item_ids)) {
$message = 'Select one or more saved Workshop IDs to remove.';
return false;
}
$escaped_ids = array();
foreach ($item_ids as $item_id) {
$escaped_ids[] = "'" . $db->realEscapeSingle((string)$item_id) . "'";
}
$db->query(
"DELETE FROM `" . OGP_DB_PREFIX . "server_content_workshop`
WHERE home_id=" . (int)$home_id . "
AND workshop_item_id IN (" . implode(",", $escaped_ids) . ")"
);
scm_workshop_log_action($db, $home_id, $user_id, "remove_selected ids=" . implode(',', $item_ids) . " status=db_removed");
$is_error = false;
$message = 'Selected Workshop item(s) removed from this server list. Installed files, if any, can be cleaned up separately.';
return true;
}
if ($action === 'update_selected' || $action === 'download_selected') {
$item_ids = scm_workshop_filter_existing_ids($db, $home_id, scm_parse_selected_workshop_ids($selected_ids));
if (empty($item_ids)) {
$message = 'Select one or more saved Workshop IDs.';
return false;
}
$target_action = ($action === 'remove_selected') ? 'remove' : (($action === 'download_selected') ? 'download_only' : 'update');
$manifest_context = scm_workshop_build_manifest_context($db, $home_info, $server_xml, $item_ids, $template);
scm_workshop_update_rows_state($db, $home_id, $item_ids, 'installing', null, false, false);
$error = '';
$details = array();
$ok = scm_workshop_write_manifest_and_run($db, $home_info, $server_xml, $target_action, $item_ids, $error, $manifest_context, $details);
if ($ok) {
if ($target_action === 'remove') {
scm_workshop_update_rows_state($db, $home_id, $item_ids, 'removed', null, false, true);
} elseif ($target_action === 'download_only') {
scm_workshop_update_rows_state($db, $home_id, $item_ids, 'downloaded', null, false, true);
scm_workshop_set_update_policy($db, $home_id, $item_ids, 'install_on_restart');
scm_workshop_record_catalog_items($db, isset($manifest_context['workshop_app_id']) ? (string)$manifest_context['workshop_app_id'] : '', $item_ids, $home_info, isset($manifest_context['item_details']) ? $manifest_context['item_details'] : array(), true);
} else {
scm_workshop_update_rows_state($db, $home_id, $item_ids, 'installed', null, false, true);
scm_workshop_record_catalog_items($db, isset($manifest_context['workshop_app_id']) ? (string)$manifest_context['workshop_app_id'] : '', $item_ids, $home_info, isset($manifest_context['item_details']) ? $manifest_context['item_details'] : array(), true);
}
scm_workshop_log_action($db, $home_id, $user_id, $action." ids=".implode(',', $item_ids)." addon_id=".$addon_id." status=success");
$is_error = false;
if ($target_action === 'remove') {
$message = 'Selected Workshop item(s) removed.';
} elseif ($target_action === 'download_only') {
$message = 'Selected Workshop item(s) downloaded and marked for install on next restart.';
} else {
$message = 'Selected Workshop item(s) updated successfully.';
}
$message .= ' Log: ' . scm_h(isset($details['log_path']) ? $details['log_path'] : '');
return true;
}
scm_workshop_update_rows_state($db, $home_id, $item_ids, 'failed', $error, false, false);
scm_workshop_log_action($db, $home_id, $user_id, $action." ids=".implode(',', $item_ids)." addon_id=".$addon_id." status=failed error=".$error);
$message = $error;
return false;
}
if ($action === 'update_all') {
$rows = $db->resultQuery(
"SELECT workshop_item_id FROM `".OGP_DB_PREFIX."server_content_workshop`
WHERE home_id=".$home_id." AND install_state<>'removed'"
);
$item_ids = array();
if (is_array($rows)) {
foreach ((array)$rows as $row) {
$item_ids[] = (string)$row['workshop_item_id'];
}
}
$item_ids = scm_parse_selected_workshop_ids($item_ids);
if (empty($item_ids)) {
$message = 'No Workshop IDs are currently saved for this server.';
return false;
}
$manifest_context = scm_workshop_build_manifest_context($db, $home_info, $server_xml, $item_ids, $template);
scm_workshop_update_rows_state($db, $home_id, $item_ids, 'installing', null, false, false);
$error = '';
$details = array();
$ok = scm_workshop_write_manifest_and_run($db, $home_info, $server_xml, 'update', $item_ids, $error, $manifest_context, $details);
if ($ok) {
scm_workshop_update_rows_state($db, $home_id, $item_ids, 'installed', null, false, true);
scm_workshop_record_catalog_items($db, isset($manifest_context['workshop_app_id']) ? (string)$manifest_context['workshop_app_id'] : '', $item_ids, $home_info, isset($manifest_context['item_details']) ? $manifest_context['item_details'] : array(), true);
scm_workshop_log_action($db, $home_id, $user_id, "update_all ids=".implode(',', $item_ids)." addon_id=".$addon_id." status=success");
$is_error = false;
$message = 'All saved Workshop item(s) updated successfully. Log: ' . scm_h(isset($details['log_path']) ? $details['log_path'] : '');
return true;
}
scm_workshop_update_rows_state($db, $home_id, $item_ids, 'failed', $error, false, false);
scm_workshop_log_action($db, $home_id, $user_id, "update_all ids=".implode(',', $item_ids)." addon_id=".$addon_id." status=failed error=".$error);
$message = $error;
return false;
}
$message = 'Invalid workshop action.';
return false;
return 'Steam Workshop has been removed from Server Content. Use the Steam Workshop module instead.';
}
?>

View file

@ -1,308 +1,16 @@
<?php
/*
*
* GSP - Server Content Workshop page
*
* Users enter Steam Workshop IDs or URLs to install on their server.
* Game-specific Workshop behavior comes from the game XML workshop_support block.
*
/**
* Steam Workshop was removed from Server Content.
* Use Panel/modules/steam_workshop instead.
*/
require_once(dirname(__FILE__) . '/server_content_helpers.php');
require_once(dirname(__FILE__) . '/workshop_action.php');
function exec_ogp_module() {
global $db;
$user_id = isset($_SESSION['user_id']) ? (int)$_SESSION['user_id'] : 0;
$home_id = isset($_REQUEST['home_id']) ? (int)$_REQUEST['home_id'] : 0;
$mod_id = isset($_REQUEST['mod_id']) ? (int)$_REQUEST['mod_id'] : 0;
$ip = isset($_REQUEST['ip']) ? (string)$_REQUEST['ip'] : '';
$port = isset($_REQUEST['port']) ? (string)$_REQUEST['port'] : '';
$addon_id = isset($_REQUEST['addon_id']) ? (int)$_REQUEST['addon_id'] : 0;
if ($home_id <= 0 || $user_id <= 0) {
print_failure(get_lang('no_rights'));
echo create_back_button("addonsmanager","user_addons");
return;
if (!function_exists('print_failure')) {
function print_failure($msg) {
echo '<div class="failure">'.htmlspecialchars($msg, ENT_QUOTES, 'UTF-8').'</div>';
}
$home_info = scm_get_home_for_user($db, $home_id, $user_id);
if ($home_info === false) {
print_failure(get_lang('no_rights'));
echo create_back_button("addonsmanager","user_addons");
return;
}
if (!scm_ensure_workshop_schema($db)) {
print_failure('Failed to initialize Workshop Content storage.');
return;
}
scm_ensure_phase2_schema($db);
$server_xml = read_server_config(SERVER_CONFIG_LOCATION . "/" . $home_info['home_cfg_file']);
if ($server_xml === false || !scm_workshop_is_supported($server_xml)) {
print_failure('Steam Workshop is not enabled for this game XML. Add a valid workshop_support block before using Workshop management.');
echo create_back_button("addonsmanager","user_addons");
return;
}
// Template records are optional and used only as labels/history anchors.
$addon_template = null;
if ($addon_id > 0) {
$template_rows = $db->resultQuery(
"SELECT addon_id, name, description
FROM `" . OGP_DB_PREFIX . "addons`
WHERE addon_id=" . $addon_id . " AND install_method='steam_workshop'"
);
if (is_array($template_rows) && !empty($template_rows)) {
$addon_template = $template_rows[0];
}
}
$message = '';
$is_error = false;
$entered_ids = '';
$catalog_sort = isset($_REQUEST['catalog_sort']) ? (string)$_REQUEST['catalog_sort'] : 'last_installed';
$catalog_query = isset($_REQUEST['workshop_search']) ? trim((string)$_REQUEST['workshop_search']) : '';
$catalog_tag = isset($_REQUEST['workshop_tag']) ? trim((string)$_REQUEST['workshop_tag']) : '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$posted_home_id = isset($_POST['home_id']) ? (int)$_POST['home_id'] : 0;
$csrf_token = isset($_POST['workshop_csrf']) ? (string)$_POST['workshop_csrf'] : '';
$entered_ids = isset($_POST['workshop_ids']) ? (string)$_POST['workshop_ids'] : '';
$selected_ids = isset($_POST['selected_ids']) ? $_POST['selected_ids'] : array();
$action = isset($_POST['workshop_action']) ? (string)$_POST['workshop_action'] : '';
$posted_addon_id = isset($_POST['addon_id']) ? (int)$_POST['addon_id'] : 0;
$options = array(
'update_policy' => isset($_POST['update_policy']) ? (string)$_POST['update_policy'] : 'manual',
);
if ($posted_home_id !== $home_id) {
$is_error = true;
$message = 'Invalid server context for workshop action.';
}
elseif (!scm_validate_csrf_token($csrf_token)) {
$is_error = true;
$message = 'Invalid CSRF token for workshop action.';
}
else {
scm_workshop_handle_action($db, $home_info, $user_id, $action, $entered_ids, (array)$selected_ids, $message, $is_error, $posted_addon_id > 0 ? $posted_addon_id : $addon_id, $options);
}
}
$catalog_app_id = ($server_xml !== false) ? scm_extract_workshop_app_id($server_xml) : '';
$steam_app_id = ($server_xml !== false) ? scm_extract_workshop_steam_app_id($server_xml) : '';
$install_strategy = ($server_xml !== false) ? scm_detect_workshop_install_strategy($home_info, $server_xml) : '';
$rows = scm_get_workshop_rows($db, $home_id);
$catalog_rows = scm_get_workshop_catalog_rows($db, $catalog_app_id, $catalog_sort, 50, $catalog_query, $catalog_tag);
$game_search_rows = scm_get_workshop_enabled_games($catalog_query, $catalog_tag);
$csrf_token = scm_get_csrf_token();
$base_query = 'm=addonsmanager&p=workshop_content&home_id=' . (int)$home_id .
'&mod_id=' . (int)$mod_id . '&ip=' . urlencode($ip) . '&port=' . urlencode($port) .
'&addon_id=' . (int)$addon_id .
'&workshop_search=' . urlencode($catalog_query) . '&workshop_tag=' . urlencode($catalog_tag);
echo "<h2>Workshop Mods: " . scm_h($home_info['home_name']) . "</h2>";
if ($addon_template !== null) {
echo "<p class='info'>Content template: <strong>" . scm_h($addon_template['name']) . "</strong>";
if (!empty($addon_template['description'])) {
echo " " . scm_h($addon_template['description']);
}
echo "</p>";
}
echo "<p class='info'>Enter a Steam Workshop URL or numeric item ID. GSP stores only the numeric Workshop ID. App IDs, install paths, mod folder strategy, key-copy behavior, and launch parameter format come from this game's XML.</p>";
if ($message !== '') {
if ($is_error) {
print_failure($message);
} else {
print_success($message);
}
}
?>
<table class='center'>
<tr><td align='right'><strong>Server Name:</strong></td><td align='left'><?php echo scm_h($home_info['home_name']); ?></td></tr>
<tr><td align='right'><strong>Game Name:</strong></td><td align='left'><?php echo scm_h($home_info['game_name']); ?></td></tr>
<tr><td align='right'><strong>Workshop App ID:</strong></td><td align='left'><?php echo scm_h($catalog_app_id); ?></td></tr>
<tr><td align='right'><strong>Steam App ID:</strong></td><td align='left'><?php echo scm_h($steam_app_id); ?></td></tr>
<tr><td align='right'><strong>Install Strategy:</strong></td><td align='left'><?php echo scm_h($install_strategy); ?></td></tr>
</table>
<h3>Search Workshop</h3>
<form method='get' action=''>
<input type='hidden' name='m' value='addonsmanager' />
<input type='hidden' name='p' value='workshop_content' />
<input type='hidden' name='home_id' value='<?php echo (int)$home_id; ?>' />
<input type='hidden' name='mod_id' value='<?php echo (int)$mod_id; ?>' />
<input type='hidden' name='ip' value='<?php echo scm_h($ip); ?>' />
<input type='hidden' name='port' value='<?php echo scm_h($port); ?>' />
<input type='hidden' name='addon_id' value='<?php echo (int)$addon_id; ?>' />
<table class='center'>
<tr>
<td align='right'><strong>Keyword / ID / URL</strong></td>
<td align='left'><input type='text' name='workshop_search' size='42' value='<?php echo scm_h($catalog_query); ?>' placeholder='ACE, CBA, Zombies, Maps, or Workshop ID' /></td>
<td align='right'><strong>Tag</strong></td>
<td align='left'><input type='text' name='workshop_tag' size='24' value='<?php echo scm_h($catalog_tag); ?>' placeholder='Weapons, Missions, Maps' /></td>
<td><button type='submit'>Search</button></td>
</tr>
<?php if ($catalog_app_id !== '' && ($catalog_query !== '' || $catalog_tag !== '')): ?>
<tr>
<td></td>
<td colspan='4' class='info'>
<a target='_blank' rel='noopener' href='https://steamcommunity.com/workshop/browse/?appid=<?php echo scm_h($catalog_app_id); ?>&amp;searchtext=<?php echo scm_h(urlencode(trim($catalog_query . ' ' . $catalog_tag))); ?>'>Open matching Steam Workshop search</a>
</td>
</tr>
<?php endif; ?>
</table>
</form>
<?php if ($catalog_query !== '' || $catalog_tag !== ''): ?>
<h3>Workshop Search Results</h3>
<p class='info'>
GSP searched the local catalog for this selected game's Workshop App ID.
If nothing appears below, use the Steam Workshop search link above, then paste the Workshop URL or numeric ID into the install box.
</p>
<?php endif; ?>
<form method='post' action=''>
<input type='hidden' name='m' value='addonsmanager' />
<input type='hidden' name='p' value='workshop_content' />
<input type='hidden' name='home_id' value='<?php echo (int)$home_id; ?>' />
<input type='hidden' name='mod_id' value='<?php echo (int)$mod_id; ?>' />
<input type='hidden' name='ip' value='<?php echo scm_h($ip); ?>' />
<input type='hidden' name='port' value='<?php echo scm_h($port); ?>' />
<input type='hidden' name='addon_id' value='<?php echo (int)$addon_id; ?>' />
<input type='hidden' name='workshop_csrf' value='<?php echo scm_h($csrf_token); ?>' />
<table class='center'>
<tr>
<td align='right'><strong>Workshop URLs / IDs</strong></td>
<td align='left'>
<textarea name='workshop_ids' rows='4' cols='72' placeholder='https://steamcommunity.com/sharedfiles/filedetails/?id=450814997&#10;463939057'><?php echo scm_h($entered_ids); ?></textarea>
<br><small style="color:#666;">Enter one or more Steam Workshop URLs or numeric IDs, one per line, comma-separated, or space-separated.<br>Example for Arma 3 CBA_A3: <code>https://steamcommunity.com/sharedfiles/filedetails/?id=450814997</code></small>
</td>
<td align='left' style='vertical-align:top;padding-top:4px;'>
<button type='submit' name='workshop_action' value='install_new'>Install / Queue</button>
</td>
</tr>
</table>
<br>
<table class='center'>
<tr>
<th></th>
<th>Workshop ID</th>
<th>Title</th>
<th>Enabled</th>
<th>Order</th>
<th>Update Policy</th>
<th>State</th>
<th>Install Path</th>
<th>Last Installed</th>
<th>Last Updated</th>
<th>Last Error</th>
</tr>
<?php if (empty($rows)): ?>
<tr><td colspan='11' class='info'>No Workshop items saved for this server yet.</td></tr>
<?php else: ?>
<?php foreach ((array)$rows as $row): ?>
<tr>
<td><input type='checkbox' name='selected_ids[]' value='<?php echo scm_h($row['workshop_item_id']); ?>'></td>
<td><?php echo scm_h($row['workshop_item_id']); ?></td>
<td><?php echo scm_h($row['title']); ?></td>
<td><?php echo !empty($row['enabled']) ? 'Yes' : 'No'; ?></td>
<td><?php echo scm_h(isset($row['load_order']) ? $row['load_order'] : ''); ?></td>
<td><?php echo scm_h(isset($row['update_policy']) ? $row['update_policy'] : 'manual'); ?></td>
<td><?php echo scm_h($row['install_state']); ?></td>
<td><small><?php echo scm_h(isset($row['install_path']) ? $row['install_path'] : ''); ?></small></td>
<td><?php echo scm_h($row['last_installed_at']); ?></td>
<td><?php echo scm_h($row['last_updated_at']); ?></td>
<td><?php echo scm_h($row['last_error']); ?></td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</table>
<br>
<table class='center'>
<tr>
<td>
<select name='update_policy'>
<?php foreach (scm_get_workshop_update_policies() as $policy_key => $policy_label): ?>
<option value='<?php echo scm_h($policy_key); ?>'><?php echo scm_h($policy_label); ?></option>
<?php endforeach; ?>
</select>
</td>
<td><button type='submit' name='workshop_action' value='save_update_policy'>Save Policy</button></td>
<td><button type='submit' name='workshop_action' value='update_selected'>Update Selected</button></td>
<td><button type='submit' name='workshop_action' value='download_selected'>Download Selected</button></td>
<td><button type='submit' name='workshop_action' value='remove_selected'>Remove Selected</button></td>
<td><button type='submit' name='workshop_action' value='enable_selected'>Enable Selected</button></td>
<td><button type='submit' name='workshop_action' value='disable_selected'>Disable Selected</button></td>
<td><button type='submit' name='workshop_action' value='update_all'>Update All</button></td>
</tr>
</table>
</form>
<h3>Known Workshop Items</h3>
<p class='info'>These are Workshop items previously installed through Server Content Manager. The catalog grows automatically from real installs. Metadata is optional; direct ID or URL install remains available even when Steam metadata has not been fetched yet.</p>
<table class='center'>
<tr>
<th>Workshop ID</th>
<th><a href='?<?php echo scm_h($base_query); ?>&amp;catalog_sort=name'>Name</a></th>
<th>Author</th>
<th>Thumbnail</th>
<th><a href='?<?php echo scm_h($base_query); ?>&amp;catalog_sort=install_count'>Install Count</a></th>
<th><a href='?<?php echo scm_h($base_query); ?>&amp;catalog_sort=published_date'>Published</a></th>
<th><a href='?<?php echo scm_h($base_query); ?>&amp;catalog_sort=last_updated'>Last Updated</a></th>
<th><a href='?<?php echo scm_h($base_query); ?>&amp;catalog_sort=last_installed'>Last Installed</a></th>
<th><a href='?<?php echo scm_h($base_query); ?>&amp;catalog_sort=workshop_id'>Sort ID</a></th>
<th>Action</th>
</tr>
<?php if (empty($catalog_rows)): ?>
<tr><td colspan='10' class='info'>No known Workshop items have been installed for this app yet.</td></tr>
<?php else: ?>
<?php foreach ((array)$catalog_rows as $catalog_row): ?>
<tr>
<td><?php echo scm_h($catalog_row['workshop_id']); ?></td>
<td><?php echo scm_h($catalog_row['title']); ?></td>
<td><?php echo scm_h(isset($catalog_row['author']) ? $catalog_row['author'] : ''); ?></td>
<td>
<?php if (!empty($catalog_row['thumbnail_url'])): ?>
<img src='<?php echo scm_h($catalog_row['thumbnail_url']); ?>' alt='' style='max-width:72px;max-height:48px;' />
<?php endif; ?>
</td>
<td><?php echo scm_h($catalog_row['install_count']); ?></td>
<td><?php echo scm_h($catalog_row['published_date']); ?></td>
<td><?php echo scm_h($catalog_row['last_updated']); ?></td>
<td><?php echo scm_h($catalog_row['last_installed']); ?></td>
<td><?php echo scm_h($catalog_row['workshop_id']); ?></td>
<td>
<form method='post' action='' style='margin:0;'>
<input type='hidden' name='m' value='addonsmanager' />
<input type='hidden' name='p' value='workshop_content' />
<input type='hidden' name='home_id' value='<?php echo (int)$home_id; ?>' />
<input type='hidden' name='mod_id' value='<?php echo (int)$mod_id; ?>' />
<input type='hidden' name='ip' value='<?php echo scm_h($ip); ?>' />
<input type='hidden' name='port' value='<?php echo scm_h($port); ?>' />
<input type='hidden' name='addon_id' value='<?php echo (int)$addon_id; ?>' />
<input type='hidden' name='workshop_csrf' value='<?php echo scm_h($csrf_token); ?>' />
<input type='hidden' name='workshop_ids' value='<?php echo scm_h($catalog_row['workshop_id']); ?>' />
<button type='submit' name='workshop_action' value='install_new'>Install</button>
</form>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</table>
<form method='get' action=''>
<input type='hidden' name='m' value='addonsmanager' />
<input type='hidden' name='p' value='user_addons' />
<input type='hidden' name='home_id' value='<?php echo (int)$home_id; ?>' />
<input type='hidden' name='mod_id' value='<?php echo (int)$mod_id; ?>' />
<input type='hidden' name='ip' value='<?php echo scm_h($ip); ?>' />
<input type='hidden' name='port' value='<?php echo scm_h($port); ?>' />
<input type='submit' value='Back to Server Content' />
</form>
<?php
}
echo '<h2>Steam Workshop</h2>';
print_failure('Steam Workshop has been removed from Server Content. Use the Steam Workshop module instead.');
echo '<p>The working legacy Steam Workshop module handles Workshop installs. Server Content will continue to manage normal downloadable content/addons only.</p>';
?>