diff --git a/Panel/js/modules/addonsmanager.js b/Panel/js/modules/addonsmanager.js index dce93ed6..67b06e4d 100644 --- a/Panel/js/modules/addonsmanager.js +++ b/Panel/js/modules/addonsmanager.js @@ -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'); diff --git a/Panel/modules/addonsmanager/addons_installer.php b/Panel/modules/addonsmanager/addons_installer.php index 67b94521..e21cb0c8 100644 --- a/Panel/modules/addonsmanager/addons_installer.php +++ b/Panel/modules/addonsmanager/addons_installer.php @@ -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=. - * 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,428 +245,152 @@ 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, - 'home_id' => (int)$home_id, - 'home_cfg_id' => (int)$home_info['home_cfg_id'], - 'workshop_id' => isset($install_payload['workshop_item_id']) ? (string)$install_payload['workshop_item_id'] : '', - 'target_path' => $log_target_path, - 'action' => 'started', - )); - if ($install_method === 'steam_workshop') { - scm_ensure_workshop_schema($db); - $workshop_runtime = scm_build_workshop_runtime_context($db, $home_info, $server_xml, $install_payload, $validation_message); - if ($workshop_runtime === false) { - print_failure($validation_message); - echo "

".get_lang('back')."

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

".get_lang('back')."

"; - return; + scm_log_content_install_action(array( + 'addon_id' => (int)$addon_id, + 'addon_name' => isset($addon_info['name']) ? $addon_info['name'] : '', + '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' => 'started', + )); + $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; + $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 ($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 === '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); - 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' => $hook_message, - 'action' => 'succeeded', - )); - print_success('Server-side application hook installed.'); - } else { - scm_record_install_done($db, (int)$history_id, 'failed', 1, $hook_message); - 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' => $hook_message, - )); - print_failure($hook_message); - } - 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); - - $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 "

" . htmlentities($home_info['home_name']) . "

"; - echo '
-

'.get_lang('install')." ".$filename." ${mbytes}MB/${totalmbytes}MB

-
- "; - $bar = ''; - for( $i = 1; $i <= $pct; $i++ ) - { - $bar .= ''; - } - echo "
$bar $pct%
-
-
"; - - 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 "
".$script_log."
"; - } - 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 "

".get_lang('refresh')."

"; - $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]); - } + 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, + 'install_method' => $install_method, 'content_version' => $content_version, - 'install_state' => 'installed', - 'source_url' => $addon_info['url'], - 'installed_by' => $user_id, + 'install_state' => 'installed', + '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' => isset($addon_info['path']) ? (string)$addon_info['path'] : '', + 'target_path' => (string)$install_payload['path'], + 'hook_manifest' => $hook_message, 'action' => 'succeeded', )); - echo "

".get_lang('back')."

"; - $view->refresh("?m=addonsmanager&p=user_addons&home_id=$home_id". - "&mod_id=$mod_id&ip=$ip&port=$port",10); - return; + print_success(get_lang('addon_installed_successfully')); + } else { + $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_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' => $error_output, + )); + print_failure($script_ok ? $hook_message : 'Install script failed.'); + if ($script_output && !$script_ok) { + echo "
".htmlspecialchars((string)$script_output, ENT_QUOTES, 'UTF-8')."
"; + } } + echo "

".get_lang('back')."

"; + return; } - else - { - echo "

".get_lang('refresh')."

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

Redirecting to Workshop Content manager…
"; - echo "Click here if not redirected.

"; - return; - } ?> 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)); - } ?>

- - - - - - - - - - - - - - - - > - > - - +
:
:
:
: - -
- -
Install a Steam Workshop mod using Workshop ID. URL is not required.
-
Target Path Preview - -
Workshop App ID, install path, and launch parameter format are defined in the game XML.
-
 
-   -
- - -
- - - - - - - -
-
+ + + + + +
:
:
:
Category
Available items
+
+ + + + + + + + + + + + + + + + + + + +
NameVersionDescriptionRequirementsInstall
+ + +
+ + + + + ' /> + ' /> + + ' /> + ' /> +
+
+
+ + + + + + + +
+
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() { - + - + Category - -
+ + + + + + + +
All server content installs through the scripted installer workflow. Category is used only for sorting and filtering.
- + Download URL - + + Optional helper field for the installer script. - - + Target path - + + Optional helper field for install scripts and generated download/extract steps. - + - Steam Workshop + Extract path -
Steam Workshop behavior is configured by the selected game's XML workshop_support block. Users install Workshop items from their server management page.
+ - Post-Install Script / Action
+ Install Script

%home_path%
%home_name%
@@ -286,25 +267,32 @@ function exec_ogp_module() { %incremental%
- + - + Config helper content - + + + + + Startup hook + + + Use for companion apps that should start and stop with the game server. Application name - + Hook manifest name @@ -551,24 +539,12 @@ function exec_ogp_module() { ?> - + + + + + +