From 7e183d77a0b286962bb05245bdb0b900e7f0ee6e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 19 May 2026 19:13:13 +0000 Subject: [PATCH] feat: add type-aware server content workflows and workshop metadata support Agent-Logs-Url: https://github.com/GameServerPanel/GSP/sessions/211fb593-b55a-42ad-b657-a3a4ca4764ff Co-authored-by: iaretechnician <2749183+iaretechnician@users.noreply.github.com> --- Panel/js/modules/addonsmanager.js | 43 +++- Panel/lang/English/modules/addonsmanager.php | 19 ++ .../addonsmanager/addons_installer.php | 197 +++++++++++++++++- .../modules/addonsmanager/addons_manager.php | 182 +++++++++++----- Panel/modules/addonsmanager/module.php | 35 +++- .../addonsmanager/server_content_helpers.php | 150 ++++++++++++- .../modules/addonsmanager/workshop_action.php | 6 +- .../steam_workshop/includes/functions.php | 58 ++++++ Panel/ogp_api.php | 57 ++++- 9 files changed, 676 insertions(+), 71 deletions(-) diff --git a/Panel/js/modules/addonsmanager.js b/Panel/js/modules/addonsmanager.js index 8c596e9b..d81f63a2 100644 --- a/Panel/js/modules/addonsmanager.js +++ b/Panel/js/modules/addonsmanager.js @@ -1,5 +1,42 @@ $(function() { - // Tooltips are annoying - // Use title attribute instead - // $( 'input,textarea' ).tooltip(); + var methodToRows = { + download_zip: ['#scm-row-url', '#scm-row-path', '#scm-row-post-script'], + download_file: ['#scm-row-url', '#scm-row-path', '#scm-row-post-script'], + steam_workshop: ['#scm-row-workshop-id', '#scm-row-workshop-app-id', '#scm-row-target-path-template', '#scm-row-optional-folder-name', '#scm-row-launch-param-additions', '#scm-row-config-edit-rule', '#scm-row-post-script'], + post_script: ['#scm-row-post-script'], + config_edit: ['#scm-row-path', '#scm-row-config-edit-rule', '#scm-row-post-script'], + create_folder: ['#scm-row-path'] + }; + var allRows = [ + '#scm-row-url', + '#scm-row-path', + '#scm-row-workshop-id', + '#scm-row-workshop-app-id', + '#scm-row-target-path-template', + '#scm-row-optional-folder-name', + '#scm-row-post-script', + '#scm-row-config-edit-rule', + '#scm-row-launch-param-additions' + ]; + var $method = $('#scm-install-method'); + var $help = $('#scm-install-method-help'); + + 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(); + } + 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'); + } + + $method.on('change', applyContentTypeUi); + applyContentTypeUi(); }); diff --git a/Panel/lang/English/modules/addonsmanager.php b/Panel/lang/English/modules/addonsmanager.php index 13462a9d..063c01d7 100644 --- a/Panel/lang/English/modules/addonsmanager.php +++ b/Panel/lang/English/modules/addonsmanager.php @@ -46,6 +46,7 @@ define('LANG_plugin', "Plugins / Mods"); define('LANG_mappack', "Map Packs"); define('LANG_config', "Config Packs"); // Additional category labels (for future content types already defined in server_content_categories.php) +if (!defined('LANG_version')) define('LANG_version', "Version"); define('LANG_server_content_version', "Server Versions"); define('LANG_modpack', "Modpacks"); define('LANG_workshop', "Workshop Content"); @@ -63,9 +64,15 @@ define('LANG_addons_db', "Server Content Database"); define('LANG_addon_has_been_created', "The server content item \"%s\" has been created."); define('LANG_remove_addon', "Remove"); define('LANG_fill_the_url_address_to_a_compressed_file', "Please enter a URL for the compressed file to download."); +define('LANG_fill_the_download_url', "Please enter a download URL."); +define('LANG_fill_the_workshop_id', "Please enter a Workshop ID."); +define('LANG_fill_the_target_install_path', "Please select a target install path."); +define('LANG_fill_the_script_action_body', "Please enter a script/action body."); +define('LANG_fill_the_config_edit_rule', "Please enter a config edit rule."); define('LANG_fill_the_addon_name', "Please enter a name for the server content item."); define('LANG_select_an_addon_type', "Please select a content type."); define('LANG_select_a_game_type', "Please select a game type."); +define('LANG_select_a_content_type', "Please select a content type."); define('LANG_edit_addon', "Edit"); define('LANG_invalid_addon', "Invalid server content item or access denied."); define('LANG_invalid_addon_type', "Invalid content type selected."); @@ -79,4 +86,16 @@ define('LANG_show_to_group', "Show to group"); define('LANG_all_groups', "All groups"); define('LANG_show_addons_for_selected_group', "Show content for selected group"); define('LANG_group', "Group"); +define('LANG_content_type', "Content Type"); +define('LANG_workshop_id', "Workshop ID"); +define('LANG_target_path_template', "Target Path"); +define('LANG_optional_folder_name', "Optional Folder Name"); +define('LANG_config_edit_rule', "Config Edit Rule"); +define('LANG_launch_param_additions', "Launch Parameter Additions"); +define('LANG_content_type_help_download_zip', "Downloads and extracts an archive into the target path."); +define('LANG_content_type_help_download_file', "Downloads a single file without extraction."); +define('LANG_content_type_help_steam_workshop', "Downloads/updates a Workshop item and applies it to the server."); +define('LANG_content_type_help_post_script', "Runs a script/action only, with no URL required."); +define('LANG_content_type_help_config_edit', "Applies config edit rules to a target path, with no URL required."); +define('LANG_content_type_help_create_folder', "Creates target folders/paths only, with no URL required."); ?> diff --git a/Panel/modules/addonsmanager/addons_installer.php b/Panel/modules/addonsmanager/addons_installer.php index 49f9e137..6ea509ce 100644 --- a/Panel/modules/addonsmanager/addons_installer.php +++ b/Panel/modules/addonsmanager/addons_installer.php @@ -112,6 +112,10 @@ require_once("protocol/lgsl/lgsl_protocol.php"); // Central category map — all valid addon_type values and their labels. require_once(dirname(__FILE__) . '/server_content_categories.php'); require_once(dirname(__FILE__) . '/server_content_helpers.php'); +require_once(dirname(__FILE__) . '/workshop_action.php'); +if (file_exists(dirname(__FILE__) . '/../steam_workshop/includes/functions.php')) { + require_once(dirname(__FILE__) . '/../steam_workshop/includes/functions.php'); +} function exec_ogp_module() { @@ -161,7 +165,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 FROM OGP_DB_PREFIXaddons WHERE addon_id=".$addon_id.$query_groups); + $addons_rows = $db->resultQuery("SELECT url, path, post_script, addon_type, install_method, content_version, requires_stop, restart_after_install, workshop_item_id, workshop_app_id, target_path_template, optional_folder_name, config_edit_rule, launch_param_additions, name FROM OGP_DB_PREFIXaddons WHERE addon_id=".$addon_id.$query_groups); if (!is_array($addons_rows)) { $addons_rows = []; } @@ -174,10 +178,24 @@ function exec_ogp_module() { $remote = new OGPRemoteLibrary($home_info['agent_ip'],$home_info['agent_port'],$home_info['encryption_key'],$home_info['timeout']); - $addon_info = $addons_rows[0]; - $install_method = isset($addon_info['install_method']) ? $addon_info['install_method'] : 'download_zip'; + $addon_info = $addons_rows[0]; + $install_method = scm_get_install_method_default(isset($addon_info['install_method']) ? $addon_info['install_method'] : 'download_zip'); $content_version = isset($addon_info['content_version']) ? $addon_info['content_version'] : ''; $requires_stop = !empty($addon_info['requires_stop']) ? 1 : 0; + $post_script = ''; + $validation_payload = array( + 'url' => isset($addon_info['url']) ? $addon_info['url'] : '', + 'path' => isset($addon_info['path']) ? $addon_info['path'] : '', + 'workshop_item_id' => isset($addon_info['workshop_item_id']) ? $addon_info['workshop_item_id'] : '', + 'target_path_template' => isset($addon_info['target_path_template']) ? $addon_info['target_path_template'] : '', + 'post_script' => isset($addon_info['post_script']) ? $addon_info['post_script'] : '', + 'config_edit_rule' => isset($addon_info['config_edit_rule']) ? $addon_info['config_edit_rule'] : '', + ); + $validation_message = ''; + if ($state == "start" && !scm_validate_install_method_payload($install_method, $validation_payload, $validation_message)) { + print_failure($validation_message); + return; + } // ── requires_stop guard ─────────────────────────────────────────────── // If the content item requires the server to be stopped first, check @@ -261,7 +279,155 @@ function exec_ogp_module() { $cache_mode ); $_SESSION['scm_history_id_' . $home_id . '_' . $addon_id] = $history_id; - $pid = $remote->start_file_download( $addon_info['url'], $home_info['home_path']."/".$addon_info['path'], $filename, "uncompress", $post_script); + scm_log_content_install_action(array( + 'addon_id' => (int)$addon_id, + 'addon_name' => isset($addon_info['name']) ? $addon_info['name'] : '', + 'content_type' => $install_method, + 'home_id' => (int)$home_id, + 'home_cfg_id' => (int)$home_info['home_cfg_id'], + 'workshop_id' => isset($addon_info['workshop_item_id']) ? (string)$addon_info['workshop_item_id'] : '', + 'target_path' => isset($addon_info['target_path_template']) ? (string)$addon_info['target_path_template'] : (string)$addon_info['path'], + 'action' => 'started', + )); + if ($install_method === 'steam_workshop') { + scm_ensure_workshop_schema($db); + $workshop_item_id = trim((string)$addon_info['workshop_item_id']); + $target_path_template = trim((string)$addon_info['target_path_template']); + $resolved = function_exists('steam_workshop_install_item_to_home') + ? steam_workshop_install_item_to_home($db, $home_info, $workshop_item_id, $target_path_template, array( + 'optional_folder_name' => trim((string)$addon_info['optional_folder_name']), + 'workshop_app_id' => trim((string)$addon_info['workshop_app_id']), + )) + : array('ok' => false); + if (empty($resolved['ok'])) { + $fallback_profile = function_exists('sw_get_profile_for_home') ? sw_get_profile_for_home($db, (int)$home_id) : false; + $fallback_workshop_app_id = trim((string)$addon_info['workshop_app_id']); + if ($fallback_workshop_app_id === '' && is_array($fallback_profile) && !empty($fallback_profile['workshop_app_id'])) { + $fallback_workshop_app_id = (string)$fallback_profile['workshop_app_id']; + } + if ($fallback_workshop_app_id === '') { + $fallback_workshop_app_id = scm_extract_workshop_app_id($server_xml); + } + $placeholder_map = scm_build_placeholder_map($home_info, array('exe_location' => (string)$server_xml->exe_location), array( + 'WORKSHOP_ID' => $workshop_item_id, + 'WORKSHOP_APP_ID' => $fallback_workshop_app_id, + 'STEAM_APP_ID' => (is_array($fallback_profile) && !empty($fallback_profile['steam_app_id'])) ? (string)$fallback_profile['steam_app_id'] : '', + )); + $resolved = array( + 'workshop_app_id' => $fallback_workshop_app_id, + 'steam_app_id' => (is_array($fallback_profile) && !empty($fallback_profile['steam_app_id'])) ? (string)$fallback_profile['steam_app_id'] : '', + 'target_path_resolved' => scm_apply_placeholders($target_path_template, $placeholder_map), + ); + } + $workshop_app_id = isset($resolved['workshop_app_id']) ? (string)$resolved['workshop_app_id'] : ''; + $steam_app_id = isset($resolved['steam_app_id']) ? (string)$resolved['steam_app_id'] : ''; + $target_path_resolved = isset($resolved['target_path_resolved']) ? (string)$resolved['target_path_resolved'] : ''; + $extra_manifest = array( + 'addon_id' => (int)$addon_id, + 'target_path_template' => $target_path_template, + 'target_path_resolved' => $target_path_resolved, + 'optional_folder_name' => trim((string)$addon_info['optional_folder_name']), + 'config_edit_rule' => trim((string)$addon_info['config_edit_rule']), + 'launch_param_additions' => trim((string)$addon_info['launch_param_additions']), + 'workshop_app_id' => $workshop_app_id, + 'steam_app_id' => $steam_app_id, + ); + $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, + 'target_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, + 'target_path' => $target_path_resolved, + 'action' => 'failed', + 'error' => $workshop_error, + )); + print_failure($workshop_error); + } + echo "

".get_lang('back')."

"; + 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 "

".get_lang('back')."

"; + 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 "

".get_lang('back')."

"; + 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); @@ -319,10 +485,22 @@ function exec_ogp_module() { 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 ) { - print_success(get_lang_f('wait_while_decompressing', $filename)); + if ($install_method === 'download_zip') + print_success(get_lang_f('wait_while_decompressing', $filename)); + else + print_success(get_lang('install') . " " . $filename . "..."); echo "

".get_lang('refresh')."

"; $view->refresh("?m=addonsmanager&p=addons&state=refresh&home_id=$home_id&mod_id=$mod_id". @@ -344,6 +522,15 @@ function exec_ogp_module() { '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 "

".get_lang('back')."

"; $view->refresh("?m=addonsmanager&p=user_addons&home_id=$home_id". diff --git a/Panel/modules/addonsmanager/addons_manager.php b/Panel/modules/addonsmanager/addons_manager.php index ef9ecde3..1df862ab 100644 --- a/Panel/modules/addonsmanager/addons_manager.php +++ b/Panel/modules/addonsmanager/addons_manager.php @@ -41,32 +41,16 @@ function exec_ogp_module() { $addon_type_labels = get_server_content_categories(); // key => label $install_methods = scm_get_install_methods(); // install_method keys => labels - if (isset($_POST['create_addon']) AND isset($_POST['name']) AND $_POST['url']=="") + if (isset($_POST['create_addon'])) { - print_failure(get_lang("fill_the_url_address_to_a_compressed_file")); - } - elseif(isset($_POST['create_addon']) AND isset($_POST['url']) AND $_POST['name']=="") - { - print_failure(get_lang("fill_the_addon_name")); - } - elseif(isset($_POST['create_addon']) AND isset($_POST['name']) and isset($_POST['url']) and empty($_POST['addon_type']) ) - { - print_failure(get_lang("select_an_addon_type")); - } - elseif(isset($_POST['create_addon']) AND isset($_POST['name']) and isset($_POST['url']) and isset($_POST['addon_type']) and empty($_POST['home_cfg_id']) ) - { - print_failure(get_lang("select_a_game_type")); - } - elseif (isset($_POST['create_addon']) AND isset($_POST['name']) AND isset($_POST['url']) AND isset($_POST['addon_type']) and isset($_POST['home_cfg_id']) ) - { $valid_install_methods = array_keys($install_methods); - $fields['name'] = $_POST['name']; - $fields['url'] = $_POST['url']; - $fields['path'] = $_POST['path']; - $fields['addon_type'] = $_POST['addon_type']; - $fields['home_cfg_id'] = $_POST['home_cfg_id']; - $fields['post_script'] = $_POST['post_script']; - $fields['group_id'] = $_POST['group_id']; + $fields['name'] = isset($_POST['name']) ? trim((string)$_POST['name']) : ''; + $fields['url'] = isset($_POST['url']) ? trim((string)$_POST['url']) : ''; + $fields['path'] = isset($_POST['path']) ? trim((string)$_POST['path']) : ''; + $fields['addon_type'] = isset($_POST['addon_type']) ? trim((string)$_POST['addon_type']) : ''; + $fields['home_cfg_id'] = isset($_POST['home_cfg_id']) ? (int)$_POST['home_cfg_id'] : 0; + $fields['post_script'] = isset($_POST['post_script']) ? trim((string)$_POST['post_script']) : ''; + $fields['group_id'] = isset($_POST['group_id']) ? (int)$_POST['group_id'] : 0; $fields['install_method'] = in_array($_POST['install_method'], $valid_install_methods) ? $_POST['install_method'] : 'download_zip'; $fields['content_version'] = isset($_POST['content_version']) ? $_POST['content_version'] : ''; $fields['requires_stop'] = !empty($_POST['requires_stop']) ? 1 : 0; @@ -74,11 +58,46 @@ function exec_ogp_module() { $fields['restart_after_install'] = !empty($_POST['restart_after_install']) ? 1 : 0; $fields['is_cacheable'] = !empty($_POST['is_cacheable']) ? 1 : 0; $fields['description'] = isset($_POST['description']) ? $_POST['description'] : ''; - if( is_numeric($db->resultInsertId( 'addons', $fields )) ) + $fields['workshop_item_id'] = isset($_POST['workshop_item_id']) ? trim((string)$_POST['workshop_item_id']) : ''; + $fields['workshop_app_id'] = isset($_POST['workshop_app_id']) ? trim((string)$_POST['workshop_app_id']) : ''; + $fields['target_path_template']= isset($_POST['target_path_template']) ? trim((string)$_POST['target_path_template']) : ''; + $fields['optional_folder_name']= isset($_POST['optional_folder_name']) ? trim((string)$_POST['optional_folder_name']) : ''; + $fields['config_edit_rule'] = isset($_POST['config_edit_rule']) ? trim((string)$_POST['config_edit_rule']) : ''; + $fields['launch_param_additions'] = isset($_POST['launch_param_additions']) ? trim((string)$_POST['launch_param_additions']) : ''; + + if ($fields['name'] === '') { - print_success(get_lang_f("addon_has_been_created",$_POST['name'])); - if (isset($_POST['addon_id']) && (int)$_POST['addon_id'] > 0 && isset($_POST['edit'])) - $db->query("DELETE FROM OGP_DB_PREFIXaddons WHERE addon_id=" . (int)$_POST['addon_id']); + print_failure(get_lang("fill_the_addon_name")); + } + elseif (empty($fields['addon_type']) || !in_array($fields['addon_type'], $addon_types)) + { + print_failure(get_lang("select_an_addon_type")); + } + elseif (empty($fields['home_cfg_id'])) + { + print_failure(get_lang("select_a_game_type")); + } + else + { + $validation_payload = array( + 'url' => $fields['url'], + 'path' => $fields['path'], + 'workshop_item_id' => $fields['workshop_item_id'], + 'target_path_template' => $fields['target_path_template'], + 'post_script' => $fields['post_script'], + 'config_edit_rule' => $fields['config_edit_rule'], + ); + $validation_message = ''; + if (!scm_validate_install_method_payload($fields['install_method'], $validation_payload, $validation_message)) + { + print_failure($validation_message); + } + elseif (is_numeric($db->resultInsertId('addons', $fields))) + { + print_success(get_lang_f("addon_has_been_created", $fields['name'])); + if (isset($_POST['addon_id']) && (int)$_POST['addon_id'] > 0 && isset($_POST['edit'])) + $db->query("DELETE FROM OGP_DB_PREFIXaddons WHERE addon_id=" . (int)$_POST['addon_id']); + } } } @@ -97,6 +116,12 @@ function exec_ogp_module() { $restart_after_install = isset($_POST['restart_after_install']) ? (int)$_POST['restart_after_install'] : 0; $is_cacheable = isset($_POST['is_cacheable']) ? (int)$_POST['is_cacheable'] : 0; $description = isset($_POST['description']) ? $_POST['description'] : ""; + $workshop_item_id = isset($_POST['workshop_item_id']) ? $_POST['workshop_item_id'] : ""; + $workshop_app_id = isset($_POST['workshop_app_id']) ? $_POST['workshop_app_id'] : ""; + $target_path_template = isset($_POST['target_path_template']) ? $_POST['target_path_template'] : ""; + $optional_folder_name = isset($_POST['optional_folder_name']) ? $_POST['optional_folder_name'] : ""; + $config_edit_rule = isset($_POST['config_edit_rule']) ? $_POST['config_edit_rule'] : ""; + $launch_param_additions = isset($_POST['launch_param_additions']) ? $_POST['launch_param_additions'] : ""; if (isset($_POST['addon_id']) && (int)$_POST['addon_id'] > 0 && isset($_POST['edit'])) { @@ -119,6 +144,12 @@ function exec_ogp_module() { $restart_after_install = isset($addon_info['restart_after_install']) ? (int)$addon_info['restart_after_install'] : 0; $is_cacheable = isset($addon_info['is_cacheable']) ? (int)$addon_info['is_cacheable'] : 0; $description = isset($addon_info['description']) ? $addon_info['description'] : ""; + $workshop_item_id = isset($addon_info['workshop_item_id']) ? $addon_info['workshop_item_id'] : ""; + $workshop_app_id = isset($addon_info['workshop_app_id']) ? $addon_info['workshop_app_id'] : ""; + $target_path_template = isset($addon_info['target_path_template']) ? $addon_info['target_path_template'] : ""; + $optional_folder_name = isset($addon_info['optional_folder_name']) ? $addon_info['optional_folder_name'] : ""; + $config_edit_rule = isset($addon_info['config_edit_rule']) ? $addon_info['config_edit_rule'] : ""; + $launch_param_additions = isset($addon_info['launch_param_additions']) ? $addon_info['launch_param_additions'] : ""; } ?>
@@ -131,7 +162,25 @@ function exec_ogp_module() { - + + + + + + +
+ + + @@ -141,17 +190,50 @@ function exec_ogp_module() { - + - + - + -
+ + + + + + + + + Workshop App ID + + + + + + + + + + + + Supported placeholders: {HOME_ID}, {SERVER_ROOT}, {GAME_ROOT}, {WORKSHOP_ID}, {WORKSHOP_APP_ID}, {STEAM_APP_ID} + + + + + + + + + + + + + Post-Install Script / Action

%home_path%
%home_name%
@@ -166,6 +248,22 @@ function exec_ogp_module() { + + + + + + + + + + + + + + + + @@ -241,23 +339,6 @@ function exec_ogp_module() { - - - - Install Method - - - - The mechanism used to deliver this content to the server. - - Content Version @@ -313,7 +394,6 @@ function exec_ogp_module() { - 'addons_manager', 'name' => 'Server Content Manager', 'group' => 'admin' ) @@ -165,4 +165,35 @@ $install_queries[3] = array( return true; }, ); + +// ── db_version 5 : content-type specific metadata for Workshop/config/folder actions ── +$install_queries[4] = array( + function ($db) { + $prefix = OGP_DB_PREFIX; + $new_columns = array( + 'workshop_item_id' => "VARCHAR(64) NULL AFTER `description`", + 'workshop_app_id' => "VARCHAR(32) NULL AFTER `workshop_item_id`", + 'target_path_template' => "VARCHAR(255) NULL AFTER `workshop_app_id`", + 'optional_folder_name' => "VARCHAR(255) NULL AFTER `target_path_template`", + 'config_edit_rule' => "TEXT NULL AFTER `optional_folder_name`", + 'launch_param_additions' => "VARCHAR(255) NULL AFTER `config_edit_rule`", + ); + foreach ($new_columns as $col => $definition) { + $escaped_col = $db->realEscapeSingle($col); + $escaped_table = $db->realEscapeSingle($prefix . 'addons'); + $check = $db->resultQuery( + "SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = '{$escaped_table}' + AND COLUMN_NAME = '{$escaped_col}'" + ); + if (empty($check)) { + if (!$db->query("ALTER TABLE `{$prefix}addons` ADD COLUMN `{$col}` {$definition}")) { + return false; + } + } + } + return true; + }, +); ?> diff --git a/Panel/modules/addonsmanager/server_content_helpers.php b/Panel/modules/addonsmanager/server_content_helpers.php index efebbb0c..2f40f4aa 100644 --- a/Panel/modules/addonsmanager/server_content_helpers.php +++ b/Panel/modules/addonsmanager/server_content_helpers.php @@ -255,15 +255,146 @@ function scm_get_cache_mode($db) function scm_get_install_methods() { return array( - 'download_zip' => 'Download & extract archive (.zip / .tar.gz)', - 'download_file' => 'Download single file (no extraction)', - 'post_script' => 'Run post-script only (no download)', - 'steam_workshop' => 'Steam Workshop (via agent SteamCMD)', - 'minecraft_jar' => 'Minecraft server jar / version switcher', - 'profile_copy' => 'Copy stored profile directory', + 'download_zip' => 'Compressed file download', + 'download_file' => 'Direct file download', + 'steam_workshop' => 'Steam Workshop item', + 'post_script' => 'Script/action only', + 'config_edit' => 'Config edit only', + 'create_folder' => 'Folder/create path only', ); } +function scm_get_install_method_help_text() +{ + return array( + 'download_zip' => 'Downloads and extracts an archive into the target path.', + 'download_file' => 'Downloads a single file to the target path without extraction.', + 'steam_workshop' => 'Downloads/updates a Steam Workshop item and applies it to the server path.', + 'post_script' => 'Runs only the post-install script/action body (no download).', + 'config_edit' => 'Applies config edit rules to a target config file/path.', + 'create_folder' => 'Creates the target directory path only.', + ); +} + +function scm_get_install_method_required_fields() +{ + return array( + 'download_zip' => array('url', 'path'), + 'download_file' => array('url', 'path'), + 'steam_workshop' => array('workshop_item_id', 'target_path_template'), + 'post_script' => array('post_script'), + 'config_edit' => array('path', 'config_edit_rule'), + 'create_folder' => array('path'), + ); +} + +function scm_get_install_method_validation_errors() +{ + return array( + 'url' => 'Please enter a download URL.', + 'workshop_item_id' => 'Please enter a Workshop ID.', + 'target_path_template' => 'Please select a target install path.', + 'post_script' => 'Please enter a script/action body.', + 'config_edit_rule' => 'Please enter a config edit rule.', + 'path' => 'Please select a target install path.', + ); +} + +function scm_get_install_method_default($value = '') +{ + $methods = scm_get_install_methods(); + $value = trim((string)$value); + return isset($methods[$value]) ? $value : 'download_zip'; +} + +function scm_validate_install_method_payload($install_method, array $payload, &$message = '') +{ + $install_method = scm_get_install_method_default($install_method); + $required = scm_get_install_method_required_fields(); + $errors = scm_get_install_method_validation_errors(); + if (!isset($required[$install_method])) { + $message = 'Invalid install/content type selected.'; + return false; + } + foreach ($required[$install_method] as $field) { + $value = isset($payload[$field]) ? trim((string)$payload[$field]) : ''; + if ($value === '') { + $message = isset($errors[$field]) ? $errors[$field] : 'Missing required field.'; + return false; + } + } + if ($install_method === 'steam_workshop') { + $wid = isset($payload['workshop_item_id']) ? trim((string)$payload['workshop_item_id']) : ''; + if ($wid === '' || !preg_match('/^[0-9]+$/', $wid)) { + $message = 'Please enter a Workshop ID.'; + return false; + } + } + $message = ''; + return true; +} + +function scm_build_placeholder_map(array $home_info, array $server_context = array(), array $overrides = array()) +{ + $home_id = (int)(isset($home_info['home_id']) ? $home_info['home_id'] : 0); + $server_root = rtrim(clean_path((string)(isset($home_info['home_path']) ? $home_info['home_path'] : '')), '/'); + $game_root = $server_root; + if (!empty($server_context['exe_location'])) { + $exe_location = clean_path((string)$server_context['exe_location']); + $exe_dir = dirname($exe_location); + if ($exe_dir !== '.' && $exe_dir !== '/') { + $game_root = clean_path($server_root . '/' . ltrim($exe_dir, '/')); + } + } + $map = array( + '{HOME_ID}' => (string)$home_id, + '{SERVER_ROOT}' => $server_root, + '{GAME_ROOT}' => $game_root, + '{WORKSHOP_ID}' => '', + '{WORKSHOP_APP_ID}' => '', + '{STEAM_APP_ID}' => '', + ); + foreach ($overrides as $key => $value) { + $token = '{' . strtoupper(trim((string)$key, '{}')) . '}'; + $map[$token] = (string)$value; + } + return $map; +} + +function scm_apply_placeholders($template, array $placeholder_map) +{ + $template = (string)$template; + if ($template === '') { + return ''; + } + return str_replace(array_keys($placeholder_map), array_values($placeholder_map), $template); +} + +function scm_content_logs_dir() +{ + return dirname(__FILE__) . '/logs'; +} + +function scm_content_log_file() +{ + return scm_content_logs_dir() . '/content_install.log'; +} + +function scm_log_content_install_action(array $context) +{ + $dir = scm_content_logs_dir(); + if (!is_dir($dir)) { + @mkdir($dir, 0775, true); + } + $context['logged_at'] = date('Y-m-d H:i:s'); + $line = json_encode($context); + if ($line === false) { + $line = '{"logged_at":"' . date('Y-m-d H:i:s') . '","error":"json_encode_failed"}'; + } + @error_log($line . PHP_EOL, 3, scm_content_log_file()); + return true; +} + /** * Idempotently ensures the Phase 2 schema is present. * Called from pages that use manifest / history data so that existing @@ -290,6 +421,12 @@ function scm_ensure_phase2_schema($db) 'restart_after_install' => "TINYINT(1) NOT NULL DEFAULT 0", 'is_cacheable' => "TINYINT(1) NOT NULL DEFAULT 0", 'description' => "TEXT NULL", + 'workshop_item_id' => "VARCHAR(64) NULL", + 'workshop_app_id' => "VARCHAR(32) NULL", + 'target_path_template' => "VARCHAR(255) NULL", + 'optional_folder_name' => "VARCHAR(255) NULL", + 'config_edit_rule' => "TEXT NULL", + 'launch_param_additions'=> "VARCHAR(255) NULL", ); foreach ($new_columns as $col => $definition) { $escaped_col = $db->realEscapeSingle($col); @@ -489,4 +626,3 @@ function scm_upsert_manifest($db, $home_id, $addon_id, array $fields = array()) updated_at = NOW()" ); } - diff --git a/Panel/modules/addonsmanager/workshop_action.php b/Panel/modules/addonsmanager/workshop_action.php index 5a865957..1e4c4c1c 100644 --- a/Panel/modules/addonsmanager/workshop_action.php +++ b/Panel/modules/addonsmanager/workshop_action.php @@ -67,7 +67,7 @@ function scm_workshop_filter_existing_ids($db, $home_id, array $item_ids) return array_values($allowed); } -function scm_workshop_write_manifest_and_run($db, array $home_info, $server_xml, $action, array $item_ids, &$error = '') +function scm_workshop_write_manifest_and_run($db, array $home_info, $server_xml, $action, array $item_ids, &$error = '', array $extra_manifest = array()) { $error = ''; if (empty($item_ids)) { @@ -102,6 +102,9 @@ function scm_workshop_write_manifest_and_run($db, array $home_info, $server_xml, 'workshop_app_id' => scm_extract_workshop_app_id($server_xml), 'items' => array_values($item_ids), ); + if (!empty($extra_manifest)) { + $manifest['extra'] = $extra_manifest; + } $manifest_json = json_encode($manifest); if ($manifest_json === false) { $error = 'Failed to encode workshop manifest JSON.'; @@ -274,4 +277,3 @@ function scm_workshop_handle_action($db, array $home_info, $user_id, $action, $r $message = 'Invalid workshop action.'; return false; } - diff --git a/Panel/modules/steam_workshop/includes/functions.php b/Panel/modules/steam_workshop/includes/functions.php index 2cd04e54..9cbb8d97 100644 --- a/Panel/modules/steam_workshop/includes/functions.php +++ b/Panel/modules/steam_workshop/includes/functions.php @@ -577,3 +577,61 @@ function sw_apply_detected_profile_defaults($db, array $profile, array $detected ); return $updated; } + +function steam_workshop_resolve_paths($db, array $home_info, $workshop_id, $target_path_template = '', $optional_folder_name = '', $workshop_app_id_override = '') +{ + $profile = sw_get_profile_for_home($db, (int)$home_info['home_id']); + if (!$profile || !is_array($profile)) { + return array('ok' => false, 'error' => 'No enabled Steam Workshop profile for this server home.'); + } + $workshop_id = trim((string)$workshop_id); + if ($workshop_id === '' || !preg_match('/^[0-9]+$/', $workshop_id)) { + return array('ok' => false, 'error' => 'Invalid Workshop ID.'); + } + $workshop_app_id = trim((string)$workshop_app_id_override); + if ($workshop_app_id === '') { + $workshop_app_id = (string)$profile['workshop_app_id']; + } + $folder_name = trim((string)$optional_folder_name); + if ($folder_name === '') { + $folder_name = '@' . $workshop_id; + } + $vars = array( + 'HOME_ID' => (int)$home_info['home_id'], + 'SERVER_ROOT' => rtrim((string)$home_info['home_path'], '/'), + 'GAME_ROOT' => rtrim((string)$home_info['home_path'], '/'), + 'WORKSHOP_ID' => $workshop_id, + 'WORKSHOP_APP_ID' => $workshop_app_id, + 'STEAM_APP_ID' => (string)$profile['steam_app_id'], + 'FOLDER_NAME' => $folder_name, + 'MOD_FOLDER' => $folder_name, + ); + $target_template = trim((string)$target_path_template); + if ($target_template === '') { + $target_template = !empty($profile['install_path_template']) ? (string)$profile['install_path_template'] : '{SERVER_ROOT}/{MOD_FOLDER}'; + } + $resolved_target = sw_apply_template($target_template, $vars); + return array( + 'ok' => true, + 'profile' => $profile, + 'workshop_id' => $workshop_id, + 'workshop_app_id' => $workshop_app_id, + 'steam_app_id' => (string)$profile['steam_app_id'], + 'folder_name' => $folder_name, + 'target_path_template' => $target_template, + 'target_path_resolved' => $resolved_target, + 'vars' => $vars, + ); +} + +function steam_workshop_download_item($db, array $home_info, $workshop_id, $target_path_template = '', array $options = array()) +{ + $optional_folder_name = isset($options['optional_folder_name']) ? $options['optional_folder_name'] : ''; + $workshop_app_id = isset($options['workshop_app_id']) ? $options['workshop_app_id'] : ''; + return steam_workshop_resolve_paths($db, $home_info, $workshop_id, $target_path_template, $optional_folder_name, $workshop_app_id); +} + +function steam_workshop_install_item_to_home($db, array $home_info, $workshop_id, $target_path_template = '', array $options = array()) +{ + return steam_workshop_download_item($db, $home_info, $workshop_id, $target_path_template, $options); +} diff --git a/Panel/ogp_api.php b/Panel/ogp_api.php index 37ca95b3..6d27d5cb 100644 --- a/Panel/ogp_api.php +++ b/Panel/ogp_api.php @@ -1357,6 +1357,10 @@ function api_litefm() function api_addonsmanager() { global $request, $db, $user_info; + require_once(MODULES.'addonsmanager/server_content_helpers.php'); + require_once(MODULES.'addonsmanager/workshop_action.php'); + if (file_exists(MODULES.'steam_workshop/includes/functions.php')) + require_once(MODULES.'steam_workshop/includes/functions.php'); if($db->isModuleInstalled('addonsmanager') === FALSE) return array("status" => '349', "message" => "This function is not available because the module is not installed."); @@ -1427,9 +1431,22 @@ function api_addonsmanager() $addon_info = $addons_rows[0]; + $install_method = scm_get_install_method_default(isset($addon_info['install_method']) ? $addon_info['install_method'] : 'download_zip'); + $validation_payload = array( + 'url' => isset($addon_info['url']) ? $addon_info['url'] : '', + 'path' => isset($addon_info['path']) ? $addon_info['path'] : '', + 'workshop_item_id' => isset($addon_info['workshop_item_id']) ? $addon_info['workshop_item_id'] : '', + 'target_path_template' => isset($addon_info['target_path_template']) ? $addon_info['target_path_template'] : '', + 'post_script' => isset($addon_info['post_script']) ? $addon_info['post_script'] : '', + 'config_edit_rule' => isset($addon_info['config_edit_rule']) ? $addon_info['config_edit_rule'] : '', + ); + $validation_message = ''; + if (!scm_validate_install_method_payload($install_method, $validation_payload, $validation_message)) + return array("status" => '422', "message" => $validation_message); $url = $addon_info['url']; $filename = basename($url); + $post_script = ''; if($addon_info['post_script'] != "") { $addon_info['post_script'] = strip_real_escape_string($addon_info['post_script']); @@ -1461,7 +1478,45 @@ function api_addonsmanager() $post_script = preg_replace( "/\%incremental\%/i", $home_info['incremental'], $post_script); } - $pid = $remote->start_file_download($addon_info['url'], $home_info['home_path']."/".$addon_info['path'], $filename, "uncompress", $post_script); + if ($install_method === 'steam_workshop') { + scm_ensure_workshop_schema($db); + $workshop_item_id = trim((string)$addon_info['workshop_item_id']); + $workshop_error = ''; + $extra_manifest = array( + 'addon_id' => (int)$addon_id, + 'target_path_template' => isset($addon_info['target_path_template']) ? (string)$addon_info['target_path_template'] : '', + 'optional_folder_name' => isset($addon_info['optional_folder_name']) ? (string)$addon_info['optional_folder_name'] : '', + 'config_edit_rule' => isset($addon_info['config_edit_rule']) ? (string)$addon_info['config_edit_rule'] : '', + 'launch_param_additions' => isset($addon_info['launch_param_additions']) ? (string)$addon_info['launch_param_additions'] : '', + ); + $ok = scm_workshop_write_manifest_and_run($db, $home_info, $server_xml, 'install', array($workshop_item_id), $workshop_error, $extra_manifest); + if ($ok) + return array("status" => "200", "message" => "Workshop content installed successfully."); + return array("status" => "342", "message" => "Workshop install failed: ".$workshop_error); + } + if ($install_method === 'post_script') { + $output = $remote->exec("cd " . escapeshellarg($home_info['home_path']) . " && /bin/bash -lc " . escapeshellarg((string)$post_script) . " ; echo __GSP_SCRIPT_EXIT:$?"); + if (is_string($output) && preg_match('/__GSP_SCRIPT_EXIT:(\d+)/', $output, $m) && (int)$m[1] === 0) + return array("status" => "200", "message" => "Script/action completed."); + return array("status" => "342", "message" => "Script/action failed."); + } + if ($install_method === 'config_edit' || $install_method === 'create_folder') { + $path = trim((string)$addon_info['path']); + if ($path === '') + return array("status" => "422", "message" => "Please select a target install path."); + $target = (strpos($path, '/') === 0) ? $path : clean_path(rtrim($home_info['home_path'], '/') . '/' . ltrim($path, '/')); + if ($install_method === 'create_folder') + $out = $remote->exec("mkdir -p " . escapeshellarg($target) . " && echo __GSP_FOLDER_OK"); + else { + $rule = trim((string)$addon_info['config_edit_rule']); + $out = $remote->exec("mkdir -p " . escapeshellarg(dirname($target)) . " && touch " . escapeshellarg($target) . " && printf %s " . escapeshellarg($rule . PHP_EOL) . " >> " . escapeshellarg($target) . " && echo __GSP_CONFIG_OK"); + } + if (is_string($out)) + return array("status" => "200", "message" => "Content action completed."); + return array("status" => "342", "message" => "Content action failed."); + } + $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); if($pid > 0) { $status = "200";