server content fix

This commit is contained in:
Frank Harris 2026-06-18 12:11:47 -05:00
parent 8a56ddc83c
commit 67022a3846
9 changed files with 589 additions and 740 deletions

View file

@ -7,42 +7,22 @@ $(function() {
return output;
}
var methodToRows = {
download_zip: ['#scm-row-url', '#scm-row-path'],
steam_workshop: ['#scm-row-workshop-xml-info'],
post_script: ['#scm-row-post-script'],
config_edit: ['#scm-row-path', '#scm-row-config-edit-rule'],
server_app: ['.scm-row-server-app']
};
var allRows = [
'#scm-row-url',
'#scm-row-path',
'#scm-row-workshop-xml-info',
'#scm-row-post-script',
'#scm-row-config-edit-rule',
'.scm-row-server-app'
];
var $method = $('#scm-install-method');
var $help = $('#scm-install-method-help');
var $category = $('#scm-category-input');
var $hookEnabled = $('#scm-hook-enabled');
function applyContentTypeUi() {
if ($method.length === 0) return;
var value = $method.val();
var shown = methodToRows[value] || [];
for (var i = 0; i < allRows.length; i++) {
$(allRows[i]).hide();
function applyCategoryUi() {
var category = String($category.val() || '').toLowerCase();
var isServerApp = category === 'server-side application';
var showHookRows = isServerApp || $hookEnabled.is(':checked');
$('.scm-row-server-app').toggle(showHookRows);
if (isServerApp) {
$hookEnabled.prop('checked', true);
}
for (var j = 0; j < shown.length; j++) {
$(shown[j]).show();
}
var selectedOption = $method.find('option:selected');
var helpText = selectedOption.data('help') || '';
$help.text(helpText);
$('#scm-path-label').text(value === 'config_edit' ? 'Config Target Path' : 'Target Path / Extract Path (optional)');
}
$method.on('change', applyContentTypeUi);
applyContentTypeUi();
$category.on('input change', applyCategoryUi);
$hookEnabled.on('change', applyCategoryUi);
applyCategoryUi();
var $userSelect = $('#scm-user-addon-select');
var $userWorkshopRows = $('.scm-user-workshop-row');

View file

@ -6,18 +6,17 @@
*
* 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.
* This file runs the unified Server Content installer workflow for a selected
* category item.
*
* 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.
* 1. User selects a category from user_addons.php.
* 2. The page lists available content items in that category.
* 3. On install, the Panel prepares helper directories under _gsp_content.
* 4. The content item's install script is resolved from admin-defined script
* text or generated from helper fields such as URL/target/extract path.
* 5. The script runs on the assigned agent inside the server home.
* 6. Optional startup-hook metadata is written to _gsp_content/hooks/*.json.
*
* POST-INSTALL SCRIPT REPLACEMENT VARIABLES:
* %home_path% absolute path of the game server home directory
@ -30,17 +29,14 @@
* %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.
* - Users CANNOT supply arbitrary scripts; only the admin-defined or
* Panel-generated installer 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
@ -54,14 +50,6 @@
* 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.
@ -80,14 +68,6 @@
* 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.
* ─────────────────────────────────────────────────────────────────────────────
*/
@ -113,18 +93,9 @@ 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');
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'];
@ -159,9 +130,6 @@ function exec_ogp_module() {
$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'] : "";
@ -171,7 +139,7 @@ function exec_ogp_module() {
{
$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, hook_name, hook_enabled, hook_platform, hook_working_dir, hook_start_command, hook_stop_command, hook_start_timing, hook_stop_with_server, hook_watch, hook_critical, hook_kill_game_if_app_exits, hook_restart_app_if_exits, hook_pid_name, hook_app_name, hook_description FROM OGP_DB_PREFIXaddons WHERE addon_id=".$addon_id.$query_groups);
$addons_rows = $db->resultQuery("SELECT url, path, extract_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, hook_name, hook_enabled, hook_platform, hook_working_dir, hook_start_command, hook_stop_command, hook_start_timing, hook_stop_with_server, hook_watch, hook_critical, hook_kill_game_if_app_exits, hook_restart_app_if_exits, hook_pid_name, hook_app_name, hook_description FROM OGP_DB_PREFIXaddons WHERE addon_id=".$addon_id.$query_groups);
if (!is_array($addons_rows)) {
$addons_rows = [];
}
@ -194,13 +162,14 @@ function exec_ogp_module() {
}
$addon_info = $addons_rows[0];
$install_method = scm_get_install_method_default(isset($addon_info['install_method']) ? $addon_info['install_method'] : 'download_zip');
$install_method = scm_get_install_method_default(isset($addon_info['install_method']) ? $addon_info['install_method'] : 'post_script');
$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);
$install_payload = scm_collect_install_payload($addon_info, $_REQUEST, array());
$install_payload['addon_type'] = scm_normalize_category(isset($addon_info['addon_type']) ? $addon_info['addon_type'] : '', $install_method);
if (scm_category_enables_startup_hook($install_payload['addon_type'])) {
$install_payload['hook_enabled'] = 1;
}
$post_script = '';
$validation_message = '';
if ($state == "start" && !scm_validate_install_method_payload($install_method, $install_payload, $validation_message)) {
@ -220,16 +189,15 @@ function exec_ogp_module() {
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'] != "")
$resolved_script = scm_resolve_installer_script($install_payload);
if($resolved_script != "")
{
$addon_info['post_script'] = strip_real_escape_string($addon_info['post_script']);
$resolved_script = strip_real_escape_string($resolved_script);
$check_passed = FALSE;
$address_at_post = $ip.":".$port;
$ip_ports = $db->getHomeIpPorts($home_info['home_id']);
@ -264,7 +232,7 @@ function exec_ogp_module() {
$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_path\%/i", $home_info['home_path'], $resolved_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);
@ -277,418 +245,141 @@ function exec_ogp_module() {
#### 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'],
$install_payload['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,
'content_type' => $install_payload['addon_type'],
'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,
'target_path' => (string)$install_payload['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 === 'server_app') {
$hook_message = '';
$hook_ok = scm_install_server_app_hook($remote, $home_info, $addon_info, $hook_message);
if ($hook_ok) {
scm_record_install_done($db, (int)$history_id, 'installed', 0, 'hook_manifest=' . $hook_message);
$hook_ok = true;
if ($script_ok && !empty($install_payload['hook_enabled'])) {
$hook_ok = scm_install_server_app_hook($remote, $home_info, array_merge($addon_info, $install_payload), $hook_message);
}
if ($script_ok && $hook_ok) {
scm_record_install_done($db, (int)$history_id, 'installed', 0, trim((string)$script_output) . ($hook_message !== '' ? "\nhook_manifest=" . $hook_message : ''));
scm_upsert_manifest($db, $home_id, $addon_id, array(
'install_method' => $install_method,
'content_version' => $content_version,
'install_state' => 'installed',
'source_url' => '',
'source_url' => $install_payload['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,
'content_type' => $install_payload['addon_type'],
'home_id' => (int)$home_id,
'home_cfg_id' => (int)$home_info['home_cfg_id'],
'target_path' => $hook_message,
'target_path' => (string)$install_payload['path'],
'hook_manifest' => $hook_message,
'action' => 'succeeded',
));
print_success('Server-side application hook installed.');
print_success(get_lang('addon_installed_successfully'));
} else {
scm_record_install_done($db, (int)$history_id, 'failed', 1, $hook_message);
$error_output = $script_ok ? $hook_message : trim((string)$script_output);
scm_record_install_done($db, (int)$history_id, 'failed', 1, $error_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,
'content_type' => $install_payload['addon_type'],
'home_id' => (int)$home_id,
'home_cfg_id' => (int)$home_info['home_cfg_id'],
'target_path' => (string)$install_payload['path'],
'action' => 'failed',
'error' => $hook_message,
'error' => $error_output,
));
print_failure($hook_message);
print_failure($script_ok ? $hook_message : 'Install script failed.');
if ($script_output && !$script_ok) {
echo "<pre class='log'>".htmlspecialchars((string)$script_output, ENT_QUOTES, 'UTF-8')."</pre>";
}
}
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;
}
// 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;
$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));
$addon_type_lang = $addon_type;
$addons = $db->resultQuery(
"SELECT addon_id, name, install_method, workshop_item_id, workshop_app_id, target_path_template, optional_folder_name
"SELECT addon_id, name, addon_type, content_version, description, requires_stop, restart_after_install, hook_enabled
FROM OGP_DB_PREFIXaddons
WHERE addon_type='".$addon_type."' AND home_cfg_id=" . $home_cfg_id . $query_groups . "
WHERE addon_type='".$db->realEscapeSingle($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'>
<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'><strong>Category</strong></td><td align='left'><?php echo htmlspecialchars($addon_type_lang, ENT_QUOTES, 'UTF-8'); ?></td></tr>
<tr><td align='right'><strong>Available items</strong></td><td align='left'><?php echo count($addons); ?></td></tr>
</table>
<br>
<table class='center'>
<tr>
<th>Name</th>
<th>Version</th>
<th>Description</th>
<th>Requirements</th>
<th>Install</th>
</tr>
<?php foreach ((array)$addons as $addon) { ?>
<tr>
<td><?php echo htmlspecialchars($addon['name'], ENT_QUOTES, 'UTF-8'); ?></td>
<td><?php echo htmlspecialchars(isset($addon['content_version']) && $addon['content_version'] !== '' ? $addon['content_version'] : '-', ENT_QUOTES, 'UTF-8'); ?></td>
<td><?php echo htmlspecialchars(isset($addon['description']) && $addon['description'] !== '' ? $addon['description'] : 'No description provided.', ENT_QUOTES, 'UTF-8'); ?></td>
<td>
<?php
$requirements = array();
if (!empty($addon['requires_stop'])) { $requirements[] = 'Stop first'; }
if (!empty($addon['restart_after_install'])) { $requirements[] = 'Restart after install'; }
if (!empty($addon['hook_enabled'])) { $requirements[] = 'Startup hook'; }
echo htmlspecialchars(!empty($requirements) ? implode(', ', $requirements) : 'Standard install', ENT_QUOTES, 'UTF-8');
?>
</td>
<td>
<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='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 htmlspecialchars($ip, ENT_QUOTES, 'UTF-8'); ?>' />
<input type='hidden' name='port' value='<?php echo htmlspecialchars($port, ENT_QUOTES, 'UTF-8'); ?>' />
<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>
<input type='hidden' name='addon_id' value='<?php echo (int)$addon['addon_id']; ?>' />
<input type='submit' name='update' value='<?php print_lang('install'); ?>' />
</form>
</td>
</tr>
<?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>
<tr>
<td colspan='5' align='right'>
<form method="get">
<input type="hidden" name="m" value="addonsmanager" />
<input type="hidden" name="p" value="user_addons" />
@ -698,7 +389,8 @@ function exec_ogp_module() {
<input type="hidden" name="port" value="<?php echo $port; ?>" />
<input type="submit" value="<?php print_lang('back'); ?>" />
</form>
</td></tr>
</td>
</tr>
</table>
<?php
}

View file

@ -8,15 +8,10 @@
* ─────────────────────────────────────────────────────────────────────────────
* 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).
* Server Content now uses a single scripted-installer model. Admins define a
* category for sorting/filtering, an install script or helper fields that can
* generate a basic script, and optional startup-hook metadata for companion
* applications such as BEC.
*
* DB table: OGP_DB_PREFIXaddons (unchanged for backward compatibility).
* See SERVER_CONTENT_ROADMAP.md for the full migration plan.
@ -30,36 +25,25 @@ 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).
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
$addon_type_labels = scm_get_category_options_from_db($db);
$install_method = 'post_script';
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['extract_path'] = isset($_POST['extract_path']) ? trim((string)$_POST['extract_path']) : '';
$fields['addon_type'] = scm_normalize_category(isset($_POST['addon_type']) ? $_POST['addon_type'] : scm_get_default_category());
$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['install_method'] = 'post_script';
$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;
@ -73,7 +57,7 @@ function exec_ogp_module() {
$fields['config_edit_rule'] = isset($_POST['config_edit_rule']) ? trim((string)$_POST['config_edit_rule']) : '';
$fields['launch_param_additions'] = '';
$fields['hook_name'] = isset($_POST['hook_name']) ? trim((string)$_POST['hook_name']) : '';
$fields['hook_enabled'] = !empty($_POST['hook_enabled']) ? 1 : 0;
$fields['hook_enabled'] = (!empty($_POST['hook_enabled']) || scm_category_enables_startup_hook($fields['addon_type'])) ? 1 : 0;
$fields['hook_platform'] = scm_normalize_hook_platform(isset($_POST['hook_platform']) ? $_POST['hook_platform'] : 'both');
$fields['hook_working_dir'] = isset($_POST['hook_working_dir']) ? trim((string)$_POST['hook_working_dir']) : '';
$fields['hook_start_command'] = isset($_POST['hook_start_command']) ? trim((string)$_POST['hook_start_command']) : '';
@ -87,13 +71,13 @@ function exec_ogp_module() {
$fields['hook_pid_name'] = isset($_POST['hook_pid_name']) ? trim((string)$_POST['hook_pid_name']) : '';
$fields['hook_app_name'] = isset($_POST['hook_app_name']) ? trim((string)$_POST['hook_app_name']) : '';
$fields['hook_description'] = isset($_POST['hook_description']) ? trim((string)$_POST['hook_description']) : '';
$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'] = '';
}
$fields['post_script'] = scm_resolve_installer_script(array(
'url' => $fields['url'],
'path' => $fields['path'],
'extract_path' => $fields['extract_path'],
'post_script' => $fields['post_script'],
'config_edit_rule' => $fields['config_edit_rule'],
));
if ($fields['name'] === '')
{
@ -103,15 +87,13 @@ function exec_ogp_module() {
{
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(
'addon_type' => $fields['addon_type'],
'url' => $fields['url'],
'path' => $fields['path'],
'extract_path' => $fields['extract_path'],
'workshop_item_id' => $fields['workshop_item_id'],
'workshop_app_id' => $fields['workshop_app_id'],
'target_path_template' => $fields['target_path_template'],
@ -151,11 +133,12 @@ function exec_ogp_module() {
$name = isset($_POST['name']) ? $_POST['name'] : "";
$url = isset($_POST['url']) ? $_POST['url'] : "";
$path = isset($_POST['path']) ? $_POST['path'] : "";
$extract_path = isset($_POST['extract_path']) ? $_POST['extract_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'] : "";
$addon_type = isset($_POST['addon_type']) ? scm_normalize_category($_POST['addon_type']) : scm_get_default_category();
$group_id = isset($_POST['group_id']) ? $_POST['group_id'] : "";
$install_method = isset($_POST['install_method']) ? $_POST['install_method'] : "download_zip";
$install_method = 'post_script';
$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;
@ -164,7 +147,7 @@ function exec_ogp_module() {
$description = isset($_POST['description']) ? $_POST['description'] : "";
$config_edit_rule = isset($_POST['config_edit_rule']) ? $_POST['config_edit_rule'] : "";
$hook_name = isset($_POST['hook_name']) ? $_POST['hook_name'] : "";
$hook_enabled = isset($_POST['hook_enabled']) ? (int)$_POST['hook_enabled'] : 1;
$hook_enabled = isset($_POST['hook_enabled']) ? (int)$_POST['hook_enabled'] : (scm_category_enables_startup_hook($addon_type) ? 1 : 0);
$hook_platform = isset($_POST['hook_platform']) ? $_POST['hook_platform'] : "both";
$hook_working_dir = isset($_POST['hook_working_dir']) ? $_POST['hook_working_dir'] : "";
$hook_start_command = isset($_POST['hook_start_command']) ? $_POST['hook_start_command'] : "";
@ -189,11 +172,12 @@ function exec_ogp_module() {
$name = isset($addon_info['name']) ? $addon_info['name'] : "";
$url = isset($addon_info['url']) ? $addon_info['url'] : "";
$path = isset($addon_info['path']) ? $addon_info['path'] : "";
$extract_path = isset($addon_info['extract_path']) ? $addon_info['extract_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);
$addon_type = scm_normalize_category(isset($addon_info['addon_type']) ? $addon_info['addon_type'] : "", isset($addon_info['install_method']) ? $addon_info['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";
$install_method = 'post_script';
$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;
@ -202,7 +186,7 @@ function exec_ogp_module() {
$description = isset($addon_info['description']) ? $addon_info['description'] : "";
$config_edit_rule = isset($addon_info['config_edit_rule']) ? $addon_info['config_edit_rule'] : "";
$hook_name = isset($addon_info['hook_name']) ? $addon_info['hook_name'] : "";
$hook_enabled = isset($addon_info['hook_enabled']) ? (int)$addon_info['hook_enabled'] : 1;
$hook_enabled = isset($addon_info['hook_enabled']) ? (int)$addon_info['hook_enabled'] : (scm_category_enables_startup_hook($addon_type) ? 1 : 0);
$hook_platform = isset($addon_info['hook_platform']) ? $addon_info['hook_platform'] : "both";
$hook_working_dir = isset($addon_info['hook_working_dir']) ? $addon_info['hook_working_dir'] : "";
$hook_start_command = isset($addon_info['hook_start_command']) ? $addon_info['hook_start_command'] : "";
@ -228,53 +212,50 @@ function exec_ogp_module() {
<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">
<tr id="scm-row-category">
<td align="right">
<b><?php print_lang('content_type'); ?></b>
<b>Category</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>
<input type="hidden" name="install_method" value="post_script" />
<input type="text" value="<?php echo htmlspecialchars($addon_type, ENT_QUOTES, 'UTF-8'); ?>" name="addon_type" id="scm-category-input" size="50" list="scm-category-options" placeholder="Server Content" />
<datalist id="scm-category-options">
<?php foreach ((array)$addon_type_labels as $label) { ?>
<option value="<?php echo htmlspecialchars($label, ENT_QUOTES, 'UTF-8'); ?>"></option>
<?php } ?>
</datalist>
<div id="scm-install-method-help" style="color:#666;margin-top:4px;">All server content installs through the scripted installer workflow. Category is used only for sorting and filtering.</div>
</td>
</tr>
<tr id="scm-row-url">
<td align="right">
<b><?php print_lang('url'); ?></b>
<b>Download URL</b>
</td>
<td align="left">
<input type="text" value="<?php echo $url; ?>" name="url" size="85" title="<?php print_lang('url_info'); ?>" />
<input type="text" value="<?php echo htmlspecialchars($url, ENT_QUOTES, 'UTF-8'); ?>" name="url" size="85" title="<?php print_lang('url_info'); ?>" />
<small style="color:#666;">Optional helper field for the installer script.</small>
</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>
<b id="scm-path-label">Target path</b>
</td>
<td align="left">
<input type="text" value="<?php echo $path; ?>" name="path" size="85" title="<?php print_lang('path_info'); ?>" />
<input type="text" value="<?php echo htmlspecialchars($path, ENT_QUOTES, 'UTF-8'); ?>" name="path" size="85" title="<?php print_lang('path_info'); ?>" />
<small style="color:#666;">Optional helper field for install scripts and generated download/extract steps.</small>
</td>
</tr>
<tr id="scm-row-workshop-xml-info">
<tr id="scm-row-extract-path">
<td align="right">
<b>Steam Workshop</b>
<b>Extract path</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>
<input type="text" value="<?php echo htmlspecialchars($extract_path, ENT_QUOTES, 'UTF-8'); ?>" name="extract_path" size="85" placeholder="Optional extraction folder" />
</td>
</tr>
<tr id="scm-row-post-script">
<td align="right">
<b>Post-Install Script / Action</b><br>
<b>Install Script</b><br>
<u><?php print_lang('replacements'); ?></u><br>
%home_path%<br>
%home_name%<br>
@ -286,25 +267,32 @@ function exec_ogp_module() {
%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>
<textarea name="post_script" style="width:99%;height:175px;" title="<?php print_lang('post-script_info'); ?>" placeholder="Primary installer script. If left blank, GSP will generate a basic script from the helper fields when possible."><?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>
<b>Config helper content</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>
<textarea name="config_edit_rule" style="width:99%;height:90px;" placeholder="Optional helper content appended to the target path when no full script is provided."><?php echo htmlspecialchars($config_edit_rule, ENT_QUOTES, 'UTF-8'); ?></textarea>
</td>
</tr>
<tr id="scm-row-enable-hook">
<td align="right"><b>Startup hook</b></td>
<td align="left">
<label>
<input type="checkbox" name="hook_enabled" id="scm-hook-enabled" value="1" <?php echo $hook_enabled ? 'checked' : ''; ?> />
Enable startup hook metadata
</label>
<small style="color:#666;">Use for companion apps that should start and stop with the game server.</small>
</td>
</tr>
<tr class="scm-row-server-app">
<td align="right"><b>Application name</b></td>
<td align="left">
<input type="text" name="hook_name" value="<?php echo htmlspecialchars($hook_name, ENT_QUOTES, 'UTF-8'); ?>" size="60" placeholder="BEC" />
<label style="margin-left:10px;">
<input type="checkbox" name="hook_enabled" value="1" <?php echo $hook_enabled ? 'checked' : ''; ?> />
Enabled
</label>
<span style="margin-left:10px;color:#666;">Hook manifest name</span>
</td>
</tr>
<tr class="scm-row-server-app">
@ -551,24 +539,12 @@ function exec_ogp_module() {
?>
</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>
<input type="text" name="addon_type" value="<?php echo isset($_GET['addon_type']) ? htmlspecialchars(scm_normalize_category($_GET['addon_type']), ENT_QUOTES, 'UTF-8') : ''; ?>" list="scm-filter-categories" placeholder="Server Content" />
<datalist id="scm-filter-categories">
<?php foreach ((array)$addon_type_labels as $label) { ?>
<option value="<?php echo htmlspecialchars($label, ENT_QUOTES, 'UTF-8'); ?>"></option>
<?php } ?>
</datalist>
<b><?php print_lang('group'); ?></b>
<select name='group_id'>
<option value="0"><?php print_lang('all_groups'); ?></option>
@ -603,7 +579,7 @@ function exec_ogp_module() {
}
$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'] : "";
$addon_type = !empty($_GET['addon_type']) ? scm_normalize_category($_GET['addon_type']) : "";
$group_id = isset($_GET['group_id']) && is_numeric($_GET['group_id']) ? (int)$_GET['group_id'] : 0;
if ( isset($_GET['show']) )

View file

@ -28,13 +28,15 @@
* 7 add Phase 1 Workshop runtime tracking columns to
* server_content_workshop (install_path, install_strategy, enabled,
* load_order)
* 8 normalize all legacy install methods to post_script, add extract_path,
* keep addon_type as a category only, and add startup-hook metadata
*
*/
// Module general information
$module_title = "Server Content Manager";
$module_version = "2.5";
$db_version = 7;
$module_version = "2.6";
$db_version = 8;
$module_required = TRUE;
$module_menus = array(
array( 'subpage' => 'addons_manager', 'name' => 'Server Content Manager', 'group' => 'admin' )
@ -290,4 +292,64 @@ $install_queries[6] = array(
return true;
},
);
// ── db_version 8 : unify scripted installer model + category migration ─────
$install_queries[7] = array(
function ($db) {
$prefix = OGP_DB_PREFIX;
$addon_table = $db->realEscapeSingle($prefix . 'addons');
$new_columns = array(
'extract_path' => "VARCHAR(255) NULL AFTER `path`",
'hook_name' => "VARCHAR(128) NULL AFTER `blocked_workshop_ids`",
'hook_enabled' => "TINYINT(1) NOT NULL DEFAULT 0 AFTER `hook_name`",
'hook_platform' => "VARCHAR(16) NOT NULL DEFAULT 'both' AFTER `hook_enabled`",
'hook_working_dir' => "VARCHAR(255) NULL AFTER `hook_platform`",
'hook_start_command' => "TEXT NULL AFTER `hook_working_dir`",
'hook_stop_command' => "TEXT NULL AFTER `hook_start_command`",
'hook_start_timing' => "VARCHAR(32) NOT NULL DEFAULT 'before_server' AFTER `hook_stop_command`",
'hook_stop_with_server' => "TINYINT(1) NOT NULL DEFAULT 1 AFTER `hook_start_timing`",
'hook_watch' => "TINYINT(1) NOT NULL DEFAULT 1 AFTER `hook_stop_with_server`",
'hook_critical' => "TINYINT(1) NOT NULL DEFAULT 0 AFTER `hook_watch`",
'hook_kill_game_if_app_exits' => "TINYINT(1) NOT NULL DEFAULT 0 AFTER `hook_critical`",
'hook_restart_app_if_exits' => "TINYINT(1) NOT NULL DEFAULT 1 AFTER `hook_kill_game_if_app_exits`",
'hook_pid_name' => "VARCHAR(128) NULL AFTER `hook_restart_app_if_exits`",
'hook_app_name' => "VARCHAR(128) NULL AFTER `hook_pid_name`",
'hook_description' => "TEXT NULL AFTER `hook_app_name`",
);
foreach ($new_columns as $col => $definition) {
$check = $db->resultQuery(
"SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = '{$addon_table}'
AND COLUMN_NAME = '" . $db->realEscapeSingle($col) . "'"
);
if (empty($check)) {
if (!$db->query("ALTER TABLE `{$prefix}addons` ADD COLUMN `{$col}` {$definition}")) {
return false;
}
}
}
$db->query("ALTER TABLE `{$prefix}addons` ALTER `install_method` SET DEFAULT 'post_script'");
$db->query("UPDATE `{$prefix}addons` SET install_method='post_script' WHERE install_method IS NULL OR install_method='' OR install_method IN ('download_zip','download_file','config_edit','create_folder','server_app')");
$migrations = array(
'file_download' => 'Mod',
'plugin' => 'Mod',
'mappack' => 'Map',
'config' => 'Config',
'config_edit' => 'Config',
'scripted_installer' => 'Server Content',
'post_script' => 'Server Content',
'server_app' => 'Server-side Application',
);
foreach ($migrations as $legacy => $category) {
$db->query(
"UPDATE `{$prefix}addons`
SET addon_type='" . $db->realEscapeSingle($category) . "'
WHERE LOWER(TRIM(addon_type))='" . $db->realEscapeSingle($legacy) . "'"
);
}
return true;
},
);
?>

View file

@ -1,102 +1,105 @@
<?php
/*
*
* GSP - Game Server Panel (a heavily customized fork of OGP maintained by Runlevel Systems)
* GSP - Server Content categories
*
* Server Content Category Map
* ─────────────────────────────────────────────────────────────────────────────
* This file is the single source of truth for all Server Content types.
* It maps internal addon_type DB values to human-readable display labels.
* addon_type is now treated as a user/admin-facing category for sorting and
* filtering. It no longer determines how installation is executed.
*
* BACKWARD COMPATIBILITY:
* The three original types (plugin, mappack, config) are preserved exactly
* as they exist in the addons table. Do not rename or remove them.
*
* ADDING NEW TYPES:
* 1. Add the key/label pair below.
* 2. Ensure the DB column addon_type is VARCHAR(32) (migration db_version 2
* in module.php handles this automatically on the next module update).
* 3. No other code changes are required for the type to appear in the admin
* "Create Server Content Item" form.
*
* PLANNED INSTALL METHODS (see SERVER_CONTENT_ROADMAP.md for details):
* download_zip download a .zip/.tar.gz and extract into the server path
* download_file download a single file into the server path
* post_script run only a post-install bash script (no download)
* minecraft_jar download a Minecraft server jar and update startup config
* profile_copy copy a profile directory tree into the server home
* Backward compatibility:
* - legacy addon_type values are normalized to the current category labels
* - old install_method values are still recognized elsewhere and routed into
* the scripted installer workflow
*
*/
/**
* Returns the full Server Content category map.
*
* Keys : addon_type values stored in OGP_DB_PREFIXaddons.addon_type
* Values : Human-readable display label shown in admin and user UI
*
* @return array<string,string>
*/
function get_server_content_categories()
{
return array(
'file_download' => 'Downloadable Mod',
'config_edit' => 'Configuration Package',
'scripted_installer' => 'Scripted Installer',
'server_app' => 'Server-side Application',
'Server Content' => 'Server Content',
'Mod' => 'Mod',
'Map' => 'Map',
'Config' => 'Config',
'Bot' => 'Bot',
'Admin Tool' => 'Admin Tool',
'DayZ Mod' => 'DayZ Mod',
'Minecraft Version' => 'Minecraft Version',
'Steam Workshop Collection' => 'Steam Workshop Collection',
'Server-side Application' => 'Server-side Application',
);
}
/**
* Returns only the original three types that existed before db_version 2.
* Use this when you need to restrict to legacy values, e.g. for
* installs that have not yet run the VARCHAR(32) migration.
*
* @return array<string,string>
*/
function get_legacy_addon_types()
{
return array(
'plugin' => 'Plugins / Mods',
'mappack' => 'Map Packs',
'config' => 'Config Packs',
'plugin' => 'Mod',
'mappack' => 'Map',
'config' => 'Config',
);
}
/**
* Returns an ordered list of addon_type keys only (no labels).
* Useful as a whitelist for input validation.
*
* @return string[]
*/
function get_server_content_type_keys()
{
return array_keys(get_server_content_categories());
}
function scm_category_migration_map()
{
return array(
'file_download' => 'Mod',
'downloadable mod' => 'Mod',
'plugin' => 'Mod',
'plugins / mods' => 'Mod',
'mod' => 'Mod',
'mappack' => 'Map',
'map packs' => 'Map',
'map' => 'Map',
'config' => 'Config',
'config pack' => 'Config',
'config packs' => 'Config',
'config_edit' => 'Config',
'configuration package' => 'Config',
'scripted_installer' => 'Server Content',
'scripted installer' => 'Server Content',
'post_script' => 'Server Content',
'server content' => 'Server Content',
'server_app' => 'Server-side Application',
'server-side application' => 'Server-side Application',
);
}
function scm_normalize_category($category, $install_method = '')
{
$category = trim((string)$category);
$categories = get_server_content_categories();
if ($category === '') {
return $install_method === 'server_app' ? 'Server-side Application' : 'Server Content';
}
if (isset($categories[$category])) {
return $category;
}
$key = strtolower($category);
$map = scm_category_migration_map();
if (isset($map[$key])) {
return $map[$key];
}
if (trim((string)$install_method) === 'server_app') {
return 'Server-side Application';
}
return $category;
}
function scm_get_addon_type_from_install_method($install_method)
{
$install_method = trim((string)$install_method);
$map = array(
'download_zip' => 'file_download',
'config_edit' => 'config_edit',
'post_script' => 'scripted_installer',
'server_app' => 'server_app',
);
return isset($map[$install_method]) ? $map[$install_method] : 'file_download';
if ($install_method === 'server_app') {
return 'Server-side Application';
}
return 'Server Content';
}
function scm_normalize_addon_type($addon_type, $install_method = '')
{
$addon_type = trim((string)$addon_type);
$categories = get_server_content_categories();
if (isset($categories[$addon_type])) {
return $addon_type;
}
if ($addon_type === 'script') {
return 'scripted_installer';
}
if ($addon_type === 'config') {
return 'config_edit';
}
return scm_get_addon_type_from_install_method($install_method);
return scm_normalize_category($addon_type, $install_method);
}

View file

@ -693,55 +693,38 @@ function scm_get_cache_mode($db)
function scm_get_install_methods()
{
return array(
'download_zip' => 'Downloadable Mod',
'config_edit' => 'Configuration Package',
'post_script' => 'Scripted Installer',
'server_app' => 'Server-side Application',
);
}
function scm_get_install_method_help_text()
{
return array(
'download_zip' => 'Download and extract a ZIP, RAR, or archive file.',
'config_edit' => 'Install configuration files, profiles, or templates.',
'post_script' => 'Run a custom scripted installation process.',
'server_app' => 'Install a server-side application hook managed by the agent lifecycle.',
'post_script' => 'Run a scripted installation process. Helper fields can be used to generate or support the install script.',
);
}
function scm_get_install_method_required_fields()
{
return array(
'download_zip' => array('url'),
'steam_workshop' => array(), // No required fields; users provide Workshop IDs on their server page
'post_script' => array('post_script'),
'config_edit' => array('path', 'config_edit_rule'),
'server_app' => array('hook_name', 'hook_platform', 'hook_working_dir', 'hook_start_command'),
'post_script' => array(),
);
}
function scm_get_install_method_validation_errors()
{
return array(
'download_zip' => 'Please enter a download URL.',
'config_edit' => 'Please enter the config target and edit action.',
'post_script' => 'Please enter the installer script/action.',
'server_app' => 'Please enter the application name, platform, working directory, and start command.',
'post_script' => 'Please enter an install script or enough helper fields to generate one.',
);
}
function scm_get_install_method_default($value = '')
{
$value = trim((string)$value);
if ($value === 'download_file') {
$value = 'download_zip';
if ($value === 'steam_workshop') {
return 'steam_workshop';
}
if ($value === 'create_folder') {
$value = 'config_edit';
}
$methods = scm_get_install_methods();
return isset($methods[$value]) ? $value : 'download_zip';
return 'post_script';
}
function scm_get_install_payload_keys()
@ -749,6 +732,7 @@ function scm_get_install_payload_keys()
return array(
'url',
'path',
'extract_path',
'workshop_item_id',
'workshop_app_id',
'target_path_template',
@ -774,6 +758,45 @@ function scm_get_install_payload_keys()
);
}
function scm_get_default_category()
{
return 'Server Content';
}
function scm_category_enables_startup_hook($category)
{
return scm_normalize_category($category) === 'Server-side Application';
}
function scm_category_sort_weight($category)
{
$category = scm_normalize_category($category);
return $category === scm_get_default_category() ? 0 : 1;
}
function scm_get_category_options_from_db($db)
{
$categories = get_server_content_categories();
$rows = $db->resultQuery("SELECT DISTINCT addon_type FROM `".OGP_DB_PREFIX."addons` WHERE addon_type IS NOT NULL AND addon_type <> '' ORDER BY addon_type ASC");
if (is_array($rows)) {
foreach ($rows as $row) {
$category = scm_normalize_category(isset($row['addon_type']) ? $row['addon_type'] : '');
if ($category !== '') {
$categories[$category] = $category;
}
}
}
uksort($categories, function ($a, $b) {
$wa = scm_category_sort_weight($a);
$wb = scm_category_sort_weight($b);
if ($wa !== $wb) {
return $wa - $wb;
}
return strcasecmp($a, $b);
});
return $categories;
}
function scm_collect_install_payload(array $defaults = array(), array $request = array(), array $override_keys = array())
{
$payload = array();
@ -818,6 +841,49 @@ function scm_validate_download_content(array $payload, &$message = '')
return true;
}
function scm_resolve_installer_script(array $payload)
{
$script = isset($payload['post_script']) ? trim((string)$payload['post_script']) : '';
if ($script !== '') {
return $script;
}
$steps = array();
$url = trim((string)(isset($payload['url']) ? $payload['url'] : ''));
$target_path = trim((string)(isset($payload['path']) ? $payload['path'] : ''));
$extract_path = trim((string)(isset($payload['extract_path']) ? $payload['extract_path'] : ''));
$config_rule = trim((string)(isset($payload['config_edit_rule']) ? $payload['config_edit_rule'] : ''));
if ($url !== '') {
$quoted_url = escapeshellarg($url);
$quoted_target = escapeshellarg($target_path !== '' ? $target_path : '.');
$quoted_extract = escapeshellarg($extract_path !== '' ? $extract_path : ($target_path !== '' ? $target_path : '.'));
$basename = basename(parse_url($url, PHP_URL_PATH) ?: $url);
$basename = $basename !== '' ? $basename : 'server_content_package';
$quoted_filename = escapeshellarg($basename);
$steps[] = 'SERVER_HOME="${OGP_HOME_DIR:-$(pwd)}"';
$steps[] = 'RUNTIME_DIR="$SERVER_HOME/_gsp_content/runtime"';
$steps[] = 'TMP_ARCHIVE="$RUNTIME_DIR/' . addslashes($basename) . '"';
$steps[] = 'mkdir -p "$RUNTIME_DIR"';
$steps[] = 'mkdir -p ' . $quoted_target;
$steps[] = 'mkdir -p ' . $quoted_extract;
$steps[] = 'if command -v curl >/dev/null 2>&1; then curl -L --fail -o "$TMP_ARCHIVE" ' . $quoted_url . '; elif command -v wget >/dev/null 2>&1; then wget -O "$TMP_ARCHIVE" ' . $quoted_url . '; else echo "Missing curl or wget for server content install."; exit 1; fi';
$steps[] = 'case "$TMP_ARCHIVE" in';
$steps[] = ' *.zip) unzip -o "$TMP_ARCHIVE" -d ' . $quoted_extract . ' ;;';
$steps[] = ' *.tar.gz|*.tgz) tar -xzf "$TMP_ARCHIVE" -C ' . $quoted_extract . ' ;;';
$steps[] = ' *.tar.bz2|*.tbz2) tar -xjf "$TMP_ARCHIVE" -C ' . $quoted_extract . ' ;;';
$steps[] = ' *.tar.xz|*.txz) tar -xJf "$TMP_ARCHIVE" -C ' . $quoted_extract . ' ;;';
$steps[] = ' *) cp -f "$TMP_ARCHIVE" ' . $quoted_target . ' ;;';
$steps[] = 'esac';
}
if ($config_rule !== '' && $target_path !== '') {
$steps[] = 'mkdir -p "$(dirname ' . escapeshellarg($target_path) . ')"';
$steps[] = 'touch ' . escapeshellarg($target_path);
$steps[] = 'cat <<\'GSP_CONFIG_RULE\' >> ' . escapeshellarg($target_path);
$steps[] = $config_rule;
$steps[] = 'GSP_CONFIG_RULE';
}
return trim(implode("\n", $steps));
}
function scm_validate_workshop_content(array $payload, &$message = '')
{
$message = '';
@ -842,9 +908,9 @@ function scm_validate_workshop_user_ids($raw_ids, &$message = '')
function scm_validate_scripted_installer(array $payload, &$message = '')
{
$script = isset($payload['post_script']) ? trim((string)$payload['post_script']) : '';
$script = scm_resolve_installer_script($payload);
if ($script === '') {
$message = 'Please enter the installer script/action.';
$message = 'Please enter an install script or enough helper fields to generate one.';
return false;
}
$message = '';
@ -853,14 +919,7 @@ function scm_validate_scripted_installer(array $payload, &$message = '')
function scm_validate_configuration_package(array $payload, &$message = '')
{
$path = isset($payload['path']) ? trim((string)$payload['path']) : '';
$rule = isset($payload['config_edit_rule']) ? trim((string)$payload['config_edit_rule']) : '';
if ($path === '' || $rule === '') {
$message = 'Please enter the config target and edit action.';
return false;
}
$message = '';
return true;
return scm_validate_scripted_installer($payload, $message);
}
function scm_normalize_hook_platform($platform)
@ -906,6 +965,9 @@ function scm_validate_server_app_content(array $payload, &$message = '')
$message = 'Hook platform must be Windows, Linux, or Both.';
return false;
}
if (!scm_validate_scripted_installer($payload, $message)) {
return false;
}
$message = '';
return true;
}
@ -948,23 +1010,13 @@ function scm_validate_install_method_payload($install_method, array $payload, &$
return false;
}
if ($install_method === 'download_zip') {
return scm_validate_download_content($payload, $message);
}
if ($install_method === 'steam_workshop') {
return scm_validate_workshop_content($payload, $message);
}
if ($install_method === 'post_script') {
return scm_validate_scripted_installer($payload, $message);
}
if ($install_method === 'config_edit') {
return scm_validate_configuration_package($payload, $message);
}
if ($install_method === 'server_app') {
if (!empty($payload['hook_enabled']) || scm_category_enables_startup_hook(isset($payload['addon_type']) ? $payload['addon_type'] : '')) {
return scm_validate_server_app_content($payload, $message);
}
$message = '';
return true;
return scm_validate_scripted_installer($payload, $message);
}
function scm_build_workshop_runtime_context($db, array $home_info, $server_xml, array $payload, &$message = '')
@ -1179,7 +1231,7 @@ function scm_ensure_phase2_schema($db)
// ── Extend addons table ───────────────────────────────────────────────────
$new_columns = array(
'install_method' => "VARCHAR(32) NOT NULL DEFAULT 'download_zip'",
'install_method' => "VARCHAR(32) NOT NULL DEFAULT 'post_script'",
'content_version' => "VARCHAR(64) NULL",
'requires_stop' => "TINYINT(1) NOT NULL DEFAULT 1",
'backup_before_install' => "TINYINT(1) NOT NULL DEFAULT 1",
@ -1188,6 +1240,7 @@ function scm_ensure_phase2_schema($db)
'description' => "TEXT NULL",
'workshop_item_id' => "VARCHAR(64) NULL",
'workshop_app_id' => "VARCHAR(32) NULL",
'extract_path' => "VARCHAR(255) NULL",
'target_path_template' => "VARCHAR(255) NULL",
'optional_folder_name' => "VARCHAR(255) NULL",
'config_edit_rule' => "TEXT NULL",
@ -1197,7 +1250,7 @@ function scm_ensure_phase2_schema($db)
'required_workshop_ids' => "TEXT NULL",
'blocked_workshop_ids' => "TEXT NULL",
'hook_name' => "VARCHAR(128) NULL",
'hook_enabled' => "TINYINT(1) NOT NULL DEFAULT 1",
'hook_enabled' => "TINYINT(1) NOT NULL DEFAULT 0",
'hook_platform' => "VARCHAR(16) NOT NULL DEFAULT 'both'",
'hook_working_dir' => "VARCHAR(255) NULL",
'hook_start_command' => "TEXT NULL",
@ -1226,6 +1279,15 @@ function scm_ensure_phase2_schema($db)
}
}
$db->query("UPDATE `{$prefix}addons` SET install_method='post_script' WHERE install_method IS NULL OR install_method='' OR install_method IN ('download_zip','download_file','config_edit','create_folder','server_app')");
foreach (scm_category_migration_map() as $legacy => $category) {
$db->query(
"UPDATE `{$prefix}addons`
SET addon_type='" . $db->realEscapeSingle($category) . "'
WHERE LOWER(TRIM(addon_type))='" . $db->realEscapeSingle($legacy) . "'"
);
}
// ── Per-server manifest ───────────────────────────────────────────────────
$db->query(
"CREATE TABLE IF NOT EXISTS `{$prefix}server_content_manifest` (
@ -1310,14 +1372,14 @@ function scm_get_manifest_rows($db, $home_id)
* @param string $cache_mode_used
* @return int history row ID, or 0 on failure
*/
function scm_record_install_start($db, $home_id, $addon_id, $user_id, $source_url = '', $content_version = '', $install_method = 'download_zip', $cache_mode_used = 'disabled')
function scm_record_install_start($db, $home_id, $addon_id, $user_id, $source_url = '', $content_version = '', $install_method = 'post_script', $cache_mode_used = 'disabled')
{
$home_id = (int)$home_id;
$addon_id = (int)$addon_id;
$user_id = (int)$user_id;
$source_url = $db->realEscapeSingle((string)$source_url);
$content_version = $db->realEscapeSingle((string)$content_version);
$install_method = $db->realEscapeSingle((string)$install_method);
$install_method = $db->realEscapeSingle((string)scm_get_install_method_default($install_method));
$cache_mode_used = $db->realEscapeSingle((string)$cache_mode_used);
if (!scm_ensure_phase2_schema($db)) {
@ -1385,7 +1447,7 @@ function scm_upsert_manifest($db, $home_id, $addon_id, array $fields = array())
if ($home_id <= 0 || $addon_id <= 0 || !scm_ensure_phase2_schema($db)) {
return false;
}
$install_method = $db->realEscapeSingle((string)(isset($fields['install_method']) ? $fields['install_method'] : 'download_zip'));
$install_method = $db->realEscapeSingle((string)scm_get_install_method_default(isset($fields['install_method']) ? $fields['install_method'] : 'post_script'));
$content_version = $db->realEscapeSingle((string)(isset($fields['content_version']) ? $fields['content_version'] : ''));
$install_state = $db->realEscapeSingle((string)(isset($fields['install_state']) ? $fields['install_state'] : 'installed'));
$source_url = $db->realEscapeSingle((string)(isset($fields['source_url']) ? $fields['source_url'] : ''));
@ -1418,6 +1480,10 @@ function scm_remote_shell_quote($value)
function scm_install_server_app_hook($remote, array $home_info, array $addon_info, &$message = '')
{
if (empty($addon_info['hook_enabled'])) {
$message = '';
return true;
}
$home_path = rtrim((string)(isset($home_info['home_path']) ? $home_info['home_path'] : ''), '/\\');
if ($home_path === '') {
$message = 'Server home path is missing.';

View file

@ -15,8 +15,9 @@
*
*/
// Central category map — load so we can iterate all types dynamically.
// Category and helper definitions for sorting/filtering server content.
require_once(dirname(__FILE__) . '/server_content_categories.php');
require_once(dirname(__FILE__) . '/server_content_helpers.php');
function exec_ogp_module() {
global $db;
@ -49,29 +50,43 @@ function exec_ogp_module() {
"<table class='center' >\n".
"<tr>\n";
// Iterate all registered content types. Each type that has at least
// one item for this game generates a link to the installer page.
// New types added to server_content_categories.php automatically
// appear here without any further code changes.
$categories = get_server_content_categories(); // key => label
$categories = scm_get_category_options_from_db($db);
$category_counts = array();
$rows = $db->resultQuery(
"SELECT addon_type, COUNT(*) AS qty
FROM OGP_DB_PREFIXaddons
WHERE home_cfg_id=" . (int)$home_cfg_id . $query_groups . "
GROUP BY addon_type
ORDER BY addon_type ASC"
);
if (is_array($rows)) {
foreach ($rows as $row) {
$category = scm_normalize_category(isset($row['addon_type']) ? $row['addon_type'] : '');
$category_counts[$category] = isset($category_counts[$category]) ? ($category_counts[$category] + (int)$row['qty']) : (int)$row['qty'];
$categories[$category] = $category;
}
}
uksort($categories, function ($a, $b) {
$wa = scm_category_sort_weight($a);
$wb = scm_category_sort_weight($b);
if ($wa !== $wb) {
return $wa - $wb;
}
return strcasecmp($a, $b);
});
$printed_any_cell = false;
foreach ((array)$categories as $type_key => $type_label)
{
$items = $db->resultQuery(
"SELECT DISTINCT addon_id, name, game_name " .
"FROM OGP_DB_PREFIXaddons " .
"NATURAL JOIN OGP_DB_PREFIXconfig_homes " .
"WHERE addon_type='" . $db->realEscapeSingle($type_key) . "' " .
"AND home_cfg_id=" . (int)$home_cfg_id . $query_groups
);
$items_qty = is_array($items) ? count((array)$items) : 0;
if ($items && $items_qty >= 1)
{
if ($printed_any_cell)
$items_qty = isset($category_counts[$type_key]) ? (int)$category_counts[$type_key] : 0;
if ($items_qty < 1) {
continue;
}
if ($printed_any_cell) {
echo "</td><td>\n";
else
} else {
echo "<td>\n";
}
$printed_any_cell = true;
echo "<a href='?m=addonsmanager&amp;p=addons" .
"&amp;home_id=" . (int)$home_id .
@ -82,7 +97,6 @@ function exec_ogp_module() {
htmlspecialchars($type_label) . " (" . $items_qty . ")" .
"</a>\n";
}
}
if ($printed_any_cell)
echo "</td>\n";

View file

@ -14,16 +14,20 @@ _gsp_content/
runtime/
```
## Content Type
## Category And Hook Metadata
The Server Content Manager includes the `Server-side Application` content type.
When this type is installed, the Panel writes a JSON hook manifest to:
All server content uses the scripted installer workflow.
`Server-side Application` is a category used for sorting/filtering, plus a
startup-hook metadata preset. Any content item with startup-hook metadata
enabled writes a JSON hook manifest to:
```text
_gsp_content/hooks/<app>.json
```
The agents read these manifests during server startup.
The agents read these manifests during server startup. The actual files for the
content item may be installed anywhere the game requires them.
## Hook Manifest
@ -86,5 +90,25 @@ second, then continue normal game process and screen/session cleanup.
Windows `_alsoRun.bat` support remains for compatibility, but it is deprecated.
New companion applications should be installed as `Server-side Application`
content so both Linux and Windows agents can manage them through the same hook
content with hook metadata so both Linux and Windows agents can manage them through the same hook
contract.
## Examples
Ordinary mod install:
- Category: `Mod`
- Install script handles download/extract/move
- No startup hook metadata
BEC install:
- Category: `Server-side Application`
- Install script places BEC files in the required folder
- Startup hook metadata writes `_gsp_content/hooks/bec.json`
Config install:
- Category: `Config`
- Install script writes or patches config files
- No special install mechanism beyond the scripted installer

View file

@ -26,22 +26,31 @@ Known tables used by the module:
- `server_content_manifest`
- `server_content_install_history`
## What It Already Does
## Current Model
The module can already represent several content types, including:
All server content installs through the scripted installer workflow.
- downloads/extracted packages
- post-script driven installs
- config packs
- server-side applications with lifecycle hooks
- future profile-type content
`addon_type` is now treated as a category used for sorting and filtering only.
Examples:
- `Server Content`
- `Mod`
- `Map`
- `Config`
- `Bot`
- `Admin Tool`
- `DayZ Mod`
- `Minecraft Version`
- `Steam Workshop Collection`
- `Server-side Application`
Steam Workshop is no longer a user-facing Server Content category. Workshop access belongs to the dedicated `steam_workshop` module.
## Server-Side Applications
`Server-side Application` content writes an agent-readable hook manifest under
the target game home:
`Server-side Application` is a category plus optional startup-hook metadata.
Any content item with startup-hook metadata enabled writes an agent-readable
hook manifest under the target game home:
```text
_gsp_content/hooks/<app>.json
@ -50,17 +59,31 @@ _gsp_content/hooks/<app>.json
The agents generate runtime watchdog scripts in `_gsp_content/generated/` and
track side-application PIDs in `_gsp_content/runtime/server_content.pids`.
Use this type for companion applications such as BEC, Big Brother Bot, Discord
bridges, RCON tools, and log watchers. The application files themselves may
still be installed wherever the game requires them.
Use this category and hook metadata for companion applications such as BEC, Big
Brother Bot, Discord bridges, RCON tools, and log watchers. The application
files themselves may still be installed wherever the game requires them.
Detailed lifecycle documentation:
- `docs/features/SERVER_CONTENT_APPLICATION_HOOKS.md`
## Current Limitations
## Installer Fields
- Cache and cleanup policy need a clearer product design.
Every content item may use:
- install script
- optional download URL
- optional target path
- optional extract path
- version
- description
- stop server before install
- backup target path before install
- restart server after install
- cacheable flag
The install script is the source of truth. Helper fields can be used to
generate a basic scripted installer when a full script is not supplied.
## Where To Start Reading
@ -81,6 +104,15 @@ This module is the right place for:
- server content manifests
- install history
## Migration
Older category values are migrated or normalized as follows:
- `Downloadable Mod` / `file_download` / `plugin` -> `Mod`
- `Configuration Package` / `config_edit` / `config` -> `Config`
- `Scripted Installer` / `scripted_installer` -> `Server Content`
- `Server-side Application` / `server_app` -> `Server-side Application`
## Validation
Relevant smoke tests: