server content fix
This commit is contained in:
parent
8a56ddc83c
commit
67022a3846
9 changed files with 589 additions and 740 deletions
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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&p=user_addons&home_id=$home_id&mod_id=$mod_id&ip=$ip&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&p=user_addons&home_id=$home_id&mod_id=$mod_id&ip=$ip&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&p=user_addons&home_id=$home_id&mod_id=$mod_id&ip=$ip&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&p=user_addons&home_id=$home_id&mod_id=$mod_id&ip=$ip&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&p=user_addons&home_id=$home_id&mod_id=$mod_id&ip=$ip&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&p=addons&state=refresh&home_id=$home_id&mod_id=$mod_id".
|
||||
"&ip=$ip&port=$port&addon_id=$addon_id&pid=$pid\">".get_lang('refresh')."</a></p>";
|
||||
$view->refresh("?m=addonsmanager&p=addons&state=refresh&home_id=$home_id&mod_id=$mod_id".
|
||||
"&ip=$ip&port=$port&addon_id=$addon_id&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&p=user_addons&home_id=$home_id".
|
||||
"&mod_id=$mod_id&ip=$ip&port=$port\">".get_lang('back')."</a></p>";
|
||||
$view->refresh("?m=addonsmanager&p=user_addons&home_id=$home_id".
|
||||
"&mod_id=$mod_id&ip=$ip&port=$port",10);
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
echo "<p><a href=\"?m=addonsmanager&p=addons&state=refresh&home_id=$home_id&mod_id=$mod_id".
|
||||
"&ip=$ip&port=$port&addon_id=$addon_id&pid=$pid\">".get_lang('refresh')."</a></p>";
|
||||
$view->refresh("?m=addonsmanager&p=addons&state=refresh&home_id=$home_id&mod_id=$mod_id".
|
||||
"&ip=$ip&port=$port&addon_id=$addon_id&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'])." ".$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'> </td></tr>
|
||||
<td align='left'>
|
||||
|
||||
</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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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']) )
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
},
|
||||
);
|
||||
?>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.';
|
||||
|
|
|
|||
|
|
@ -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&p=addons" .
|
||||
"&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";
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue