diff --git a/Panel/modules/addonsmanager/addons_installer.php b/Panel/modules/addonsmanager/addons_installer.php index b114d154..34e3bdb7 100644 --- a/Panel/modules/addonsmanager/addons_installer.php +++ b/Panel/modules/addonsmanager/addons_installer.php @@ -113,13 +113,18 @@ require_once("protocol/lgsl/lgsl_protocol.php"); // Central category map — all valid addon_type values and their labels. require_once(dirname(__FILE__) . '/server_content_categories.php'); require_once(dirname(__FILE__) . '/server_content_helpers.php'); -require_once(dirname(__FILE__) . '/workshop_action.php'); if (file_exists(dirname(__FILE__) . '/../steam_workshop/includes/functions.php')) { require_once(dirname(__FILE__) . '/../steam_workshop/includes/functions.php'); } function exec_ogp_module() { + if (isset($install_method) && $install_method === 'steam_workshop') { + print_failure('Steam Workshop has been removed from Server Content. Use the Steam Workshop module instead.'); + return; + } + + global $db,$view; $home_id = $_REQUEST['home_id']; $mod_id = $_REQUEST['mod_id']; @@ -549,7 +554,7 @@ function exec_ogp_module() { return; } - // Workshop items are managed through the dedicated workshop_content page + // Steam Workshop is disabled in Server Content. Use the legacy steam_workshop module. // where users enter their own Workshop IDs. Redirect there immediately. if ($addon_type === 'workshop_item') { $first_addon_id = 0; diff --git a/Panel/modules/addonsmanager/addons_installer.php.bak.20260609-111046 b/Panel/modules/addonsmanager/addons_installer.php.bak.20260609-111046 new file mode 100644 index 00000000..b114d154 --- /dev/null +++ b/Panel/modules/addonsmanager/addons_installer.php.bak.20260609-111046 @@ -0,0 +1,654 @@ + +. + * 2. User picks a specific content item from a dropdown. + * 3. On form submit, state=start is set and start_file_download() is called + * on the remote agent with the configured URL and target path. + * 4. The agent downloads and extracts the archive. + * 5. If a post_script is defined it is run on the agent after extraction. + * 6. The page auto-refreshes (state=refresh) to show download/script progress. + * + * POST-INSTALL SCRIPT REPLACEMENT VARIABLES: + * %home_path% – absolute path of the game server home directory + * %home_name% – display name of the game server home + * %control_password% – RCON / control password for this server instance + * %max_players% – maximum player count configured for this mod slot + * %ip% – IP address bound to this server instance + * %port% – game port bound to this server instance + * %query_port% – query/status port (derived from game XML rules) + * %incremental% – internal incremental run counter for this mod/home + * + * SECURITY NOTES: + * - Users CANNOT supply arbitrary scripts; only the admin-defined post_script + * is executed. Users only pick from the approved list. + * - Paths are passed to the agent which is responsible for enforcing that + * all paths stay inside the assigned home directory. + * - TODO (next phase): add explicit server-side path validation before + * sending the command to the agent to block ../ traversal at the panel. + * + * ─── FUTURE WORK (TODO – next phase) ──────────────────────────────────────── + * The items below are intentionally NOT implemented here yet. They are + * documented so the next contributor knows exactly where to add them. + * + * TODO: requires_stop flag + * If the content item sets requires_stop=1, stop the server before + * initiating the download. Poll is_server_running() and abort if it + * cannot be stopped within a timeout. + * + * TODO: backup_before_install flag + * If backup_before_install=1, call the agent's backup function or + * compress the target path into a timestamped .tar.gz before extraction. + * + * TODO: restart_after_install flag + * If restart_after_install=1, trigger a server start after a successful + * install (i.e. after post_script completes with exit code 0). + * + * TODO: install_method field + * Current method is always 'download_zip'. Future methods: + * 'download_file' – single-file download, no extraction + * 'post_script' – run only the post_script, no download + * 'steam_workshop' – pass workshop item IDs to the agent's workshop helper + * 'minecraft_jar' – download a Minecraft server jar + update start script + * 'profile_copy' – copy a profile directory tree into the server home + * + * TODO: content_version field + * Store the installed version tag so the UI can display "installed: 1.21.1" + * and detect whether an update is available. + * + * TODO: safe script templates + * Provide a set of admin-approved script templates so admins do not have to + * write raw bash from scratch. Templates are stored in the DB and referenced + * by content items. + * + * TODO: install history / logging + * Write a row to a new install_history table (or log file) each time a + * content item is installed: + * home_id, addon_id, installed_by (user_id), installed_at, result, log_output + * + * TODO: user-friendly status output + * Replace the raw progress-bar with a card-style status block showing: + * content item name, version, download progress, script output, final status. + * + * TODO: Steam Workshop integration + * When install_method='steam_workshop', pass the workshop item ID list to + * the agent. See SERVER_CONTENT_ROADMAP.md – Part 6 for the full design. + * + * TODO: Minecraft jar / version switching + * When install_method='minecraft_jar', download the jar from Mojang/Paper/ + * Purpur/Fabric API, place it at the configured server path, and patch the + * startup command line. See SERVER_CONTENT_ROADMAP.md – Part 7. + * ───────────────────────────────────────────────────────────────────────────── + */ + +function do_progress($kbytes,$totalsize) +{ + $mbytes = round($kbytes / 1024, 2); + + if($kbytes > 0) + { + $pct = round(( $kbytes / $totalsize ) * 100, 2); + } + else + { + $pct = "-"; + } + #echo "Percent is $pct"; + return "$totalsize;$mbytes;$pct"; +} + +require_once("includes/lib_remote.php"); +require_once("modules/config_games/server_config_parser.php"); +require_once("protocol/lgsl/lgsl_protocol.php"); +// Central category map — all valid addon_type values and their labels. +require_once(dirname(__FILE__) . '/server_content_categories.php'); +require_once(dirname(__FILE__) . '/server_content_helpers.php'); +require_once(dirname(__FILE__) . '/workshop_action.php'); +if (file_exists(dirname(__FILE__) . '/../steam_workshop/includes/functions.php')) { + require_once(dirname(__FILE__) . '/../steam_workshop/includes/functions.php'); +} + +function exec_ogp_module() { + + global $db,$view; + $home_id = $_REQUEST['home_id']; + $mod_id = $_REQUEST['mod_id']; + $ip = $_REQUEST['ip']; + $port = $_REQUEST['port']; + $user_id = $_SESSION['user_id']; + + $isAdmin = $db->isAdmin( $_SESSION['user_id'] ); + $query_groups = ""; + if($isAdmin) + $home_info = $db->getGameHome($home_id); + else + { + $home_info = $db->getUserGameHome($user_id,$home_id); + $groups = $db->getUsersGroups($_SESSION['user_id']); + if (!is_array($groups)) { + $groups = []; + } + $query_groups .= " AND ("; + foreach ((array)$groups as $group) + $query_groups .= "group_id=".$group['group_id']." OR "; + $query_groups .= "group_id=0 OR group_id IS NULL)"; + } + + if ( $home_info === FALSE ) + { + print_failure(get_lang('no_rights')); + echo create_back_button("addonsmanager","user_addons"); + return; + } + + $home_cfg_id = $home_info['home_cfg_id']; + $server_xml = read_server_config(SERVER_CONFIG_LOCATION."/".$home_info['home_cfg_file']); + + // Use the full category map so newly added types are accepted without + // editing this file. The original three types are always present. + $addon_types = get_server_content_type_keys(); + $addon_type = isset($_REQUEST['addon_type']) ? scm_normalize_addon_type($_REQUEST['addon_type']) : ""; + + $state = isset($_REQUEST['state']) ? $_REQUEST['state'] : ""; + $pid = isset($_REQUEST['pid']) ? $_REQUEST['pid'] : -1; + + if ( $state != "" ) + { + $addon_id = (int)$_REQUEST['addon_id']; + + $addons_rows = $db->resultQuery("SELECT url, path, post_script, addon_type, install_method, content_version, requires_stop, restart_after_install, workshop_item_id, workshop_app_id, target_path_template, optional_folder_name, config_edit_rule, launch_param_additions, name FROM OGP_DB_PREFIXaddons WHERE addon_id=".$addon_id.$query_groups); + if (!is_array($addons_rows)) { + $addons_rows = []; + } + + if (!$addons_rows) { + print_failure(get_lang('invalid_addon')); + $view->refresh('?m=addonsmanager&p=user_addons&home_id='. $home_id .'&mod_id='. $mod_id .'&ip='. $ip .'&port='.$port); + return; + } + + $remote = new OGPRemoteLibrary($home_info['agent_ip'],$home_info['agent_port'],$home_info['encryption_key'],$home_info['timeout']); + + $addon_info = $addons_rows[0]; + $install_method = scm_get_install_method_default(isset($addon_info['install_method']) ? $addon_info['install_method'] : 'download_zip'); + $content_version = isset($addon_info['content_version']) ? $addon_info['content_version'] : ''; + $requires_stop = !empty($addon_info['requires_stop']) ? 1 : 0; + $user_override_keys = ($install_method === 'steam_workshop') + ? array('workshop_item_id') + : array(); + $install_payload = scm_collect_install_payload($addon_info, $_REQUEST, $user_override_keys); + $post_script = ''; + $validation_message = ''; + if ($state == "start" && !scm_validate_install_method_payload($install_method, $install_payload, $validation_message)) { + print_failure($validation_message); + return; + } + + // ── requires_stop guard ─────────────────────────────────────────────── + // If the content item requires the server to be stopped first, check + // whether the server is currently running and block the install if so. + // (Phase 2 blocks install; automatic stop/start is Phase 3.) + if ( $state == "start" && $requires_stop ) { + $is_running = $remote->is_screen_running( $home_info['home_name'], $home_info['home_id'] ); + if ( $is_running === 1 ) { + print_failure('This content item requires the server to be stopped before installing. Please stop the server and try again.'); + echo "

".get_lang('back')."

"; + return; + } + } + $url = $install_payload['url']; + $filename = basename($url); + #### Replace template variables in the post-install script with + #### live server data before sending to the agent. + #### Each variable is replaced case-insensitively. + #### SECURITY: only admin-defined variables are substituted; users + #### cannot inject additional commands through these fields. + if($addon_info['post_script'] != "") + { + $addon_info['post_script'] = strip_real_escape_string($addon_info['post_script']); + $check_passed = FALSE; + $address_at_post = $ip.":".$port; + $ip_ports = $db->getHomeIpPorts($home_info['home_id']); + if (!is_array($ip_ports)) { + $ip_ports = []; + } + foreach ((array)$ip_ports as $ip_port); + { + $address_owned = $ip_port['ip'].":".$ip_port['port']; + if($address_owned == $address_at_post) + { + $check_passed = TRUE; + $ip = $ip_port['ip']; + $port = $ip_port['port']; + } + } + if($check_passed) + { + $home_info['ip'] = $ip; + $home_info['port'] = $port; + + if( isset($server_xml->gameq_query_name) ) + { + require_once("modules/gamemanager/home_handling_functions.php"); + $home_info['query_port'] = get_query_port($server_xml, $home_info['port']); + } + elseif( isset($server_xml->lgsl_query_name) ) + { + $get_q_and_s = lgsl_port_conversion((string)$server_xml->lgsl_query_name, $home_info['port'], "", ""); + $home_info['query_port'] = $get_q_and_s['1']; + } + + $home_info["incremental"] = $db->incrementalNumByHomeId( $home_info['home_id'], $home_info['mods'][$mod_id]['mod_cfg_id'], $home_info['remote_server_id'] ); + + $post_script = preg_replace( "/\%home_path\%/i", $home_info['home_path'], $addon_info['post_script']); + $post_script = preg_replace( "/\%home_name\%/i", $home_info['home_name'], $post_script); + $post_script = preg_replace( "/\%control_password\%/i", $home_info['control_password'], $post_script); + $post_script = preg_replace( "/\%max_players\%/i", $home_info['mods'][$mod_id]['max_players'], $post_script); + $post_script = preg_replace( "/\%ip\%/i", $home_info['ip'], $post_script); + $post_script = preg_replace( "/\%port\%/i", $home_info['port'], $post_script); + $post_script = preg_replace( "/\%query_port\%/i", $home_info['query_port'], $post_script); + $post_script = preg_replace( "/\%incremental\%/i", $home_info['incremental'], $post_script); + } + } + + #### end of replacements + if ( $state == "start" AND $addon_id != "" ) { + // Record install attempt in history before triggering download. + $cache_mode = scm_get_cache_mode($db); + $history_id = scm_record_install_start( + $db, + $home_id, + $addon_id, + $user_id, + $addon_info['url'], + $content_version, + $install_method, + $cache_mode + ); + $_SESSION['scm_history_id_' . $home_id . '_' . $addon_id] = $history_id; + $log_target_path = (string)$install_payload['path']; + if ($install_method === 'steam_workshop') { + $log_target_path = scm_extract_workshop_install_path($server_xml); + if ($log_target_path === '') { + $log_target_path = scm_get_default_workshop_target_template(scm_detect_workshop_install_strategy($home_info, $server_xml)); + } + } + scm_log_content_install_action(array( + 'addon_id' => (int)$addon_id, + 'addon_name' => isset($addon_info['name']) ? $addon_info['name'] : '', + 'content_type' => $install_method, + 'home_id' => (int)$home_id, + 'home_cfg_id' => (int)$home_info['home_cfg_id'], + 'workshop_id' => isset($install_payload['workshop_item_id']) ? (string)$install_payload['workshop_item_id'] : '', + 'target_path' => $log_target_path, + 'action' => 'started', + )); + if ($install_method === 'steam_workshop') { + scm_ensure_workshop_schema($db); + $workshop_runtime = scm_build_workshop_runtime_context($db, $home_info, $server_xml, $install_payload, $validation_message); + if ($workshop_runtime === false) { + print_failure($validation_message); + echo "

".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; + } + 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); + + $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]); + } + 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 "

".get_lang('back')."

"; + $view->refresh("?m=addonsmanager&p=user_addons&home_id=$home_id". + "&mod_id=$mod_id&ip=$ip&port=$port",10); + 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; + } + + // Workshop items are managed through the dedicated workshop_content page + // where users enter their own Workshop IDs. Redirect there immediately. + if ($addon_type === 'workshop_item') { + $first_addon_id = 0; + $wk_addons = $db->resultQuery( + "SELECT addon_id FROM OGP_DB_PREFIXaddons + WHERE addon_type='workshop_item' AND home_cfg_id=" . (int)$home_cfg_id . $query_groups . " + ORDER BY name ASC LIMIT 1" + ); + if (is_array($wk_addons) && !empty($wk_addons[0]['addon_id'])) { + $first_addon_id = (int)$wk_addons[0]['addon_id']; + } + $redirect = "?m=addonsmanager&p=workshop_content&home_id=" . (int)$home_id . + "&mod_id=" . (int)$mod_id . "&ip=" . urlencode($ip) . "&port=" . urlencode($port) . + ($first_addon_id > 0 ? "&addon_id=" . $first_addon_id : ''); + $view->refresh($redirect); + echo "

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 + FROM OGP_DB_PREFIXaddons + WHERE addon_type='".$addon_type."' AND home_cfg_id=" . $home_cfg_id . $query_groups . " + ORDER BY name ASC" + ); + if (!is_array($addons)) { + $addons = []; + } + $selected_addon = isset($addons[0]) ? $addons[0] : array(); + $default_install_method = isset($selected_addon['install_method']) ? scm_get_install_method_default($selected_addon['install_method']) : ''; + $is_workshop_default = ($default_install_method === 'steam_workshop'); + $default_workshop_app_id = scm_extract_workshop_app_id($server_xml); + $default_target_template = scm_extract_workshop_install_path($server_xml); + if ($default_target_template === '') { + $default_target_template = scm_get_default_workshop_target_template(scm_detect_workshop_install_strategy($home_info, $server_xml)); + } + ?> +

+ + + + + + + + + + + + + + + + > + > + + +
:
:
:
: + +
+ +
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.
+
 
+   +
+ + +
+ + + + + + + +
+
+ diff --git a/Panel/modules/addonsmanager/addons_manager.php b/Panel/modules/addonsmanager/addons_manager.php index 8fb52f30..114df221 100644 --- a/Panel/modules/addonsmanager/addons_manager.php +++ b/Panel/modules/addonsmanager/addons_manager.php @@ -30,6 +30,12 @@ require_once("modules/config_games/server_config_parser.php"); function exec_ogp_module() { + if (isset($install_method) && $install_method === 'steam_workshop') { + print_failure('Steam Workshop has been removed from Server Content. Use the Steam Workshop module instead.'); + return; + } + + global $db; // Ensure Phase 2 schema is present (idempotent). diff --git a/Panel/modules/addonsmanager/addons_manager.php.bak.20260609-111046 b/Panel/modules/addonsmanager/addons_manager.php.bak.20260609-111046 new file mode 100644 index 00000000..8fb52f30 --- /dev/null +++ b/Panel/modules/addonsmanager/addons_manager.php.bak.20260609-111046 @@ -0,0 +1,532 @@ + + label + $install_methods = scm_get_install_methods(); // install_method keys => labels + + if (isset($_POST['create_addon'])) + { + $valid_install_methods = array_keys($install_methods); + $fields['name'] = isset($_POST['name']) ? trim((string)$_POST['name']) : ''; + $fields['url'] = isset($_POST['url']) ? trim((string)$_POST['url']) : ''; + $fields['path'] = isset($_POST['path']) ? trim((string)$_POST['path']) : ''; + $fields['addon_type'] = ''; + $fields['home_cfg_id'] = isset($_POST['home_cfg_id']) ? (int)$_POST['home_cfg_id'] : 0; + $fields['post_script'] = isset($_POST['post_script']) ? trim((string)$_POST['post_script']) : ''; + $fields['group_id'] = isset($_POST['group_id']) ? (int)$_POST['group_id'] : 0; + $posted_install_method = isset($_POST['install_method']) ? $_POST['install_method'] : ''; + $fields['install_method'] = in_array($posted_install_method, $valid_install_methods) ? $posted_install_method : 'download_zip'; + $fields['content_version'] = isset($_POST['content_version']) ? $_POST['content_version'] : ''; + $fields['requires_stop'] = !empty($_POST['requires_stop']) ? 1 : 0; + $fields['backup_before_install'] = !empty($_POST['backup_before_install']) ? 1 : 0; + $fields['restart_after_install'] = !empty($_POST['restart_after_install']) ? 1 : 0; + $fields['is_cacheable'] = !empty($_POST['is_cacheable']) ? 1 : 0; + $fields['description'] = isset($_POST['description']) ? $_POST['description'] : ''; + $fields['workshop_item_id'] = ''; + $fields['workshop_app_id'] = ''; + $fields['target_path_template']= ''; + $fields['optional_folder_name']= ''; + $fields['config_edit_rule'] = isset($_POST['config_edit_rule']) ? trim((string)$_POST['config_edit_rule']) : ''; + $fields['launch_param_additions'] = ''; + $fields['addon_type'] = scm_get_addon_type_from_install_method($fields['install_method']); + if ($fields['install_method'] === 'steam_workshop') { + $fields['url'] = ''; + $fields['path'] = ''; + $fields['post_script'] = ''; + $fields['config_edit_rule'] = ''; + } + + if ($fields['name'] === '') + { + print_failure(get_lang("fill_the_addon_name")); + } + elseif (empty($fields['home_cfg_id'])) + { + print_failure(get_lang("select_a_game_type")); + } + elseif ($fields['install_method'] === 'steam_workshop' && (!($game_cfg = $db->getGameCfg($fields['home_cfg_id'])) || !($server_xml = read_server_config(SERVER_CONFIG_LOCATION . "/" . $game_cfg['home_cfg_file'])) || !scm_workshop_is_supported($server_xml))) + { + print_failure('Steam Workshop support must be configured in the selected game XML before creating a Workshop content entry.'); + } + else + { + $validation_payload = array( + 'url' => $fields['url'], + 'path' => $fields['path'], + 'workshop_item_id' => $fields['workshop_item_id'], + 'workshop_app_id' => $fields['workshop_app_id'], + 'target_path_template' => $fields['target_path_template'], + 'post_script' => $fields['post_script'], + 'config_edit_rule' => $fields['config_edit_rule'], + ); + $validation_message = ''; + if (!scm_validate_install_method_payload($fields['install_method'], $validation_payload, $validation_message)) + { + print_failure($validation_message); + } + elseif (is_numeric($db->resultInsertId('addons', $fields))) + { + print_success(get_lang_f("addon_has_been_created", $fields['name'])); + if (isset($_POST['addon_id']) && (int)$_POST['addon_id'] > 0 && isset($_POST['edit'])) + $db->query("DELETE FROM OGP_DB_PREFIXaddons WHERE addon_id=" . (int)$_POST['addon_id']); + } + } + } + + echo "

".get_lang('addons_manager')."

"; + $name = isset($_POST['name']) ? $_POST['name'] : ""; + $url = isset($_POST['url']) ? $_POST['url'] : ""; + $path = isset($_POST['path']) ? $_POST['path'] : ""; + $post_script = isset($_POST['post_script']) ? $_POST['post_script'] : ""; + $home_cfg_id = isset($_POST['home_cfg_id']) ? $_POST['home_cfg_id'] : ""; + $addon_type = isset($_POST['addon_type']) ? $_POST['addon_type'] : ""; + $group_id = isset($_POST['group_id']) ? $_POST['group_id'] : ""; + $install_method = isset($_POST['install_method']) ? $_POST['install_method'] : "download_zip"; + $content_version = isset($_POST['content_version']) ? $_POST['content_version'] : ""; + $requires_stop = isset($_POST['requires_stop']) ? (int)$_POST['requires_stop'] : 1; + $backup_before_install = isset($_POST['backup_before_install']) ? (int)$_POST['backup_before_install'] : 1; + $restart_after_install = isset($_POST['restart_after_install']) ? (int)$_POST['restart_after_install'] : 0; + $is_cacheable = isset($_POST['is_cacheable']) ? (int)$_POST['is_cacheable'] : 0; + $description = isset($_POST['description']) ? $_POST['description'] : ""; + $config_edit_rule = isset($_POST['config_edit_rule']) ? $_POST['config_edit_rule'] : ""; + + if (isset($_POST['addon_id']) && (int)$_POST['addon_id'] > 0 && isset($_POST['edit'])) + { + $addons_rows = $db->resultQuery("SELECT * FROM OGP_DB_PREFIXaddons WHERE addon_id=".(int)$_POST['addon_id']); + if (!is_array($addons_rows)) { + $addons_rows = []; + } + $addon_info = $addons_rows[0]; + $name = isset($addon_info['name']) ? $addon_info['name'] : ""; + $url = isset($addon_info['url']) ? $addon_info['url'] : ""; + $path = isset($addon_info['path']) ? $addon_info['path'] : ""; + $post_script = isset($addon_info['post_script']) ? $addon_info['post_script'] : ""; + $home_cfg_id = isset($addon_info['home_cfg_id']) ? $addon_info['home_cfg_id'] : ""; + $addon_type = scm_normalize_addon_type(isset($addon_info['addon_type']) ? $addon_info['addon_type'] : "", $install_method); + $group_id = isset($addon_info['group_id']) ? $addon_info['group_id'] : ""; + $install_method = isset($addon_info['install_method']) ? $addon_info['install_method'] : "download_zip"; + $content_version = isset($addon_info['content_version']) ? $addon_info['content_version'] : ""; + $requires_stop = isset($addon_info['requires_stop']) ? (int)$addon_info['requires_stop'] : 1; + $backup_before_install = isset($addon_info['backup_before_install']) ? (int)$addon_info['backup_before_install'] : 1; + $restart_after_install = isset($addon_info['restart_after_install']) ? (int)$addon_info['restart_after_install'] : 0; + $is_cacheable = isset($addon_info['is_cacheable']) ? (int)$addon_info['is_cacheable'] : 0; + $description = isset($addon_info['description']) ? $addon_info['description'] : ""; + $config_edit_rule = isset($addon_info['config_edit_rule']) ? $addon_info['config_edit_rule'] : ""; + } + ?> +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + +
+
+ + + +
+ + + +
+ Steam Workshop + +
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
+
+ %home_path%
+ %home_name%
+ %control_password%
+ %max_players%
+ %ip%
+ %port%
+ %query_port%
+ %incremental%
+
+ +
+ + + +
+ + + +
+ + + +
+ Content Version + + + Optional version tag shown in the installed-content list. +
+ Description + + +
+ Behaviour Options + + +    + +    + +
+ Content Reuse + + + + Only check this for public, non-sensitive content (maps, mods, jars). + Never check for configs, saves, credentials, or user-edited files. + Caching only activates when the Server Content Cache Mode panel + setting (in Panel Settings) is set to something other than Disabled. + +
+ '; + echo ''; + ?> + + + + +
+
+
+

+ + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ 0 && isset($_POST['remove'])) + { + if (!$db->query("DELETE FROM OGP_DB_PREFIXaddons WHERE addon_id=" . (int)$_POST['addon_id'])) + print_lang('can_not_remove_addon'); + } + + $home_cfg_id = !empty($_GET['home_cfg_id']) && (int)$_GET['home_cfg_id'] > 0 ? (int)$_GET['home_cfg_id'] : 0; + $addon_type = !empty($_GET['addon_type']) && in_array($_GET['addon_type'], $addon_types) ? $_GET['addon_type'] : ""; + $group_id = isset($_GET['group_id']) && is_numeric($_GET['group_id']) ? (int)$_GET['group_id'] : 0; + + if ( isset($_GET['show']) ) + { + $result = $db->resultQuery("SELECT DISTINCT addon_id, name, game_name, url, path, group_id FROM OGP_DB_PREFIXaddons NATURAL JOIN OGP_DB_PREFIXconfig_homes WHERE addon_type='".$addon_type."' AND home_cfg_id=".$home_cfg_id); + } + elseif ( isset($_GET['show_all']) ) + { + $result = $db->resultQuery("SELECT DISTINCT addon_id, name, game_name, url, path, group_id FROM OGP_DB_PREFIXaddons NATURAL JOIN OGP_DB_PREFIXconfig_homes"); + } + elseif ( isset($_GET['show_type'])) + { + $result = $db->resultQuery("SELECT DISTINCT addon_id, name, game_name, url, path, group_id FROM OGP_DB_PREFIXaddons NATURAL JOIN OGP_DB_PREFIXconfig_homes WHERE addon_type='".$addon_type."'"); + } + elseif ( isset($_GET['show_game'])) + { + $result = $db->resultQuery("SELECT DISTINCT addon_id, name, game_name, url, path, group_id FROM OGP_DB_PREFIXaddons NATURAL JOIN OGP_DB_PREFIXconfig_homes WHERE home_cfg_id=".$home_cfg_id); + } + elseif ( isset($_GET['show_group'])) + { + $group_id = $group_id == '0' ? $group_id." OR group_id IS NULL" : $group_id; + $result = $db->resultQuery("SELECT DISTINCT addon_id, name, game_name, url, path, group_id FROM OGP_DB_PREFIXaddons NATURAL JOIN OGP_DB_PREFIXconfig_homes WHERE group_id=".$group_id); + } + if (isset($result) && !is_array($result)) { + $result = []; + } + ?> + + 0) + { + foreach ((array)$result as $row) + { + ?> + + + + + + + + + +
+ + + + + + + + + +
+ diff --git a/Panel/modules/addonsmanager/module.php.bak.20260609-111046 b/Panel/modules/addonsmanager/module.php.bak.20260609-111046 new file mode 100644 index 00000000..9b2161e0 --- /dev/null +++ b/Panel/modules/addonsmanager/module.php.bak.20260609-111046 @@ -0,0 +1,293 @@ + 'addons_manager', 'name' => 'Server Content Manager', 'group' => 'admin' ) +); + +// ── db_version 1 : initial install ─────────────────────────────────────────── +$install_queries = array(); +$install_queries[0] = array( + "CREATE TABLE IF NOT EXISTS `".OGP_DB_PREFIX."addons` ( + `addon_id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + `name` VARCHAR(80) NOT NULL, + `url` VARCHAR(200) NOT NULL, + `path` VARCHAR(80) NOT NULL, + `addon_type` VARCHAR(7) NOT NULL, + `home_cfg_id` VARCHAR(7) NOT NULL, + `post_script` longtext NOT NULL, + `group_id` int(11) NULL + ) ENGINE=MyISAM;" +); + +// ── db_version 2 : expand addon_type to VARCHAR(32) ────────────────────────── +// Required so extended content types such as 'workshop' (8 chars) can be stored. +// MODIFY is safe on existing installs; existing 'plugin'/'mappack'/'config' +// values are preserved without alteration. +$install_queries[1] = array( + "ALTER TABLE `".OGP_DB_PREFIX."addons` + MODIFY `addon_type` VARCHAR(32) NOT NULL;" +); + +// ── db_version 3 : workshop item selections per server home ─────────────────── +$install_queries[2] = array( + "CREATE TABLE IF NOT EXISTS `".OGP_DB_PREFIX."server_content_workshop` ( + `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + `home_id` INT NOT NULL, + `home_cfg_id` INT NOT NULL, + `remote_server_id` INT NULL, + `workshop_app_id` VARCHAR(32) NULL, + `workshop_item_id` VARCHAR(64) NOT NULL, + `title` VARCHAR(255) NULL, + `install_state` VARCHAR(32) NOT NULL DEFAULT 'selected', + `last_installed_at` DATETIME NULL, + `last_updated_at` DATETIME NULL, + `last_error` TEXT NULL, + `created_by` INT NULL, + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` DATETIME NULL, + UNIQUE KEY `uniq_home_workshop_item` (`home_id`, `workshop_item_id`), + KEY `idx_home_id` (`home_id`), + KEY `idx_home_cfg_id` (`home_cfg_id`), + KEY `idx_install_state` (`install_state`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;" +); + +// ── db_version 4 : Phase 2 – install_method, per-server manifest, install history ── +// +// Uses a PHP callable so each ALTER is applied only when the column does not +// already exist (safe for repeated runs, compatible with all MySQL versions). +// +$install_queries[3] = array( + function ($db) { + $prefix = OGP_DB_PREFIX; + + // ── Extend the addons table with Phase 2 columns ────────────────────── + $new_columns = array( + 'install_method' => "VARCHAR(32) NOT NULL DEFAULT 'download_zip' AFTER `group_id`", + 'content_version' => "VARCHAR(64) NULL AFTER `install_method`", + 'requires_stop' => "TINYINT(1) NOT NULL DEFAULT 1 AFTER `content_version`", + 'backup_before_install' => "TINYINT(1) NOT NULL DEFAULT 1 AFTER `requires_stop`", + 'restart_after_install' => "TINYINT(1) NOT NULL DEFAULT 0 AFTER `backup_before_install`", + 'is_cacheable' => "TINYINT(1) NOT NULL DEFAULT 0 AFTER `restart_after_install`", + 'description' => "TEXT NULL AFTER `is_cacheable`", + ); + + foreach ($new_columns as $col => $definition) { + $escaped_col = $db->realEscapeSingle($col); + $escaped_table = $db->realEscapeSingle($prefix . 'addons'); + $check = $db->resultQuery( + "SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = '{$escaped_table}' + AND COLUMN_NAME = '{$escaped_col}'" + ); + if (empty($check)) { + if (!$db->query("ALTER TABLE `{$prefix}addons` ADD COLUMN `{$col}` {$definition}")) { + return false; + } + } + } + + // ── Per-server installed-content manifest ───────────────────────────── + if (!$db->query( + "CREATE TABLE IF NOT EXISTS `{$prefix}server_content_manifest` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + `home_id` INT NOT NULL, + `addon_id` INT NOT NULL, + `install_method` VARCHAR(32) NOT NULL DEFAULT 'download_zip', + `content_version` VARCHAR(64) NULL, + `install_state` VARCHAR(32) NOT NULL DEFAULT 'installed', + `checksum_sha256` VARCHAR(64) NULL, + `source_url` VARCHAR(255) NULL, + `installed_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `installed_by` INT NULL, + `updated_at` DATETIME NULL, + `notes` TEXT NULL, + UNIQUE KEY `uniq_home_addon` (`home_id`, `addon_id`), + KEY `idx_home_id` (`home_id`), + KEY `idx_addon_id` (`addon_id`), + KEY `idx_install_state` (`install_state`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" + )) { + return false; + } + + // ── Install history (one row per install attempt) ───────────────────── + if (!$db->query( + "CREATE TABLE IF NOT EXISTS `{$prefix}server_content_install_history` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + `home_id` INT NOT NULL, + `addon_id` INT NOT NULL, + `installed_by` INT NULL, + `started_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `completed_at` DATETIME NULL, + `install_state` VARCHAR(32) NOT NULL DEFAULT 'started', + `install_method` VARCHAR(32) NULL, + `content_version` VARCHAR(64) NULL, + `source_url` VARCHAR(255) NULL, + `cache_mode_used` VARCHAR(32) NULL, + `result_code` INT NULL, + `log_output` MEDIUMTEXT NULL, + KEY `idx_home_id` (`home_id`), + KEY `idx_addon_id` (`addon_id`), + KEY `idx_started_at` (`started_at`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" + )) { + return false; + } + + return true; + }, +); + +// ── db_version 5 : content-type specific metadata for Workshop/config/folder actions ── +$install_queries[4] = array( + function ($db) { + $prefix = OGP_DB_PREFIX; + $new_columns = array( + 'workshop_item_id' => "VARCHAR(64) NULL AFTER `description`", + 'workshop_app_id' => "VARCHAR(32) NULL AFTER `workshop_item_id`", + 'target_path_template' => "VARCHAR(255) NULL AFTER `workshop_app_id`", + 'optional_folder_name' => "VARCHAR(255) NULL AFTER `target_path_template`", + 'config_edit_rule' => "TEXT NULL AFTER `optional_folder_name`", + 'launch_param_additions' => "VARCHAR(255) NULL AFTER `config_edit_rule`", + ); + foreach ($new_columns as $col => $definition) { + $escaped_col = $db->realEscapeSingle($col); + $escaped_table = $db->realEscapeSingle($prefix . 'addons'); + $check = $db->resultQuery( + "SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = '{$escaped_table}' + AND COLUMN_NAME = '{$escaped_col}'" + ); + if (empty($check)) { + if (!$db->query("ALTER TABLE `{$prefix}addons` ADD COLUMN `{$col}` {$definition}")) { + return false; + } + } + } + return true; + }, +); +// ── db_version 6 : admin template policy columns + content_id on workshop rows ── +// +// allow_user_workshop_ids – whether users may enter their own IDs (default 1) +// max_workshop_ids – optional cap on how many IDs a user may install +// required_workshop_ids – JSON list of IDs that must always be installed +// blocked_workshop_ids – JSON list of IDs that must not be installed +// content_id on server_content_workshop – links a user install row back to +// the admin content template so the correct workshop_app_id is used. +// +$install_queries[5] = array( + function ($db) { + $prefix = OGP_DB_PREFIX; + + // ── New policy columns on the addons (content template) table ───────── + $addon_columns = array( + 'allow_user_workshop_ids' => "TINYINT(1) NOT NULL DEFAULT 1 AFTER `blocked_workshop_ids`", + 'max_workshop_ids' => "INT NULL AFTER `allow_user_workshop_ids`", + 'required_workshop_ids' => "TEXT NULL AFTER `max_workshop_ids`", + 'blocked_workshop_ids' => "TEXT NULL AFTER `launch_param_additions`", + ); + foreach ($addon_columns as $col => $definition) { + $escaped_col = $db->realEscapeSingle($col); + $escaped_table = $db->realEscapeSingle($prefix . 'addons'); + $check = $db->resultQuery( + "SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = '{$escaped_table}' + AND COLUMN_NAME = '{$escaped_col}'" + ); + if (empty($check)) { + if (!$db->query("ALTER TABLE `{$prefix}addons` ADD COLUMN `{$col}` {$definition}")) { + return false; + } + } + } + + // ── content_id on server_content_workshop ───────────────────────────── + $wk_table = $db->realEscapeSingle($prefix . 'server_content_workshop'); + $col_check = $db->resultQuery( + "SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = '{$wk_table}' + AND COLUMN_NAME = 'content_id'" + ); + if (empty($col_check)) { + if (!$db->query( + "ALTER TABLE `{$prefix}server_content_workshop` + ADD COLUMN `content_id` INT NULL AFTER `id`, + ADD KEY `idx_content_id` (`content_id`)" + )) { + return false; + } + } + + return true; + }, +); +// ── db_version 7 : Workshop Phase 1 runtime tracking columns ──────────────── +$install_queries[6] = array( + function ($db) { + $prefix = OGP_DB_PREFIX; + $table = $db->realEscapeSingle($prefix . 'server_content_workshop'); + $columns = array( + 'install_path' => "VARCHAR(512) NULL AFTER `title`", + 'install_strategy' => "VARCHAR(64) NULL AFTER `install_path`", + 'enabled' => "TINYINT(1) NOT NULL DEFAULT 1 AFTER `install_strategy`", + 'load_order' => "INT NOT NULL DEFAULT 0 AFTER `enabled`", + ); + foreach ($columns as $col => $definition) { + $check = $db->resultQuery( + "SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = '{$table}' + AND COLUMN_NAME = '" . $db->realEscapeSingle($col) . "'" + ); + if (empty($check)) { + if (!$db->query("ALTER TABLE `{$prefix}server_content_workshop` ADD COLUMN `{$col}` {$definition}")) { + return false; + } + } + } + return true; + }, +); +?> diff --git a/Panel/modules/addonsmanager/navigation.xml b/Panel/modules/addonsmanager/navigation.xml index da0b4cc5..d0a30067 100644 --- a/Panel/modules/addonsmanager/navigation.xml +++ b/Panel/modules/addonsmanager/navigation.xml @@ -2,5 +2,5 @@ - + diff --git a/Panel/modules/addonsmanager/navigation.xml.bak.20260609-111046 b/Panel/modules/addonsmanager/navigation.xml.bak.20260609-111046 new file mode 100644 index 00000000..da0b4cc5 --- /dev/null +++ b/Panel/modules/addonsmanager/navigation.xml.bak.20260609-111046 @@ -0,0 +1,6 @@ + + + + + + diff --git a/Panel/modules/addonsmanager/scripts/workshop/generic_steam_workshop_linux.sh b/Panel/modules/addonsmanager/scripts/workshop/generic_steam_workshop_linux.sh deleted file mode 100755 index 93259dd7..00000000 --- a/Panel/modules/addonsmanager/scripts/workshop/generic_steam_workshop_linux.sh +++ /dev/null @@ -1,246 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -MANIFEST_PATH="${1:-}" -if [[ -z "$MANIFEST_PATH" ]]; then - echo "Usage: $0 " - exit 1 -fi - -python3 - "$MANIFEST_PATH" <<'PY' -import json -import os -import shutil -import subprocess -import sys -import tempfile -from datetime import datetime - -manifest_path = os.path.abspath(sys.argv[1]) -if not os.path.isfile(manifest_path): - print(f"Manifest not found: {manifest_path}") - sys.exit(1) - -manifest_dir = os.path.dirname(manifest_path) -home_root = os.path.dirname(manifest_dir) -log_file = os.path.join(manifest_dir, 'workshop_install.log') -removed_dir = os.path.join(manifest_dir, 'workshop', 'removed') -os.makedirs(removed_dir, exist_ok=True) - - -def log(message, status=None): - line = f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}]" - if status: - line += f" [{status}]" - line += f" {message}" - print(line) - with open(log_file, 'a', encoding='utf-8') as handle: - handle.write(line + "\n") - - -def fail(message): - log(message, 'Failed') - raise RuntimeError(message) - - -def uniq_numeric_items(raw_items): - seen = [] - for value in raw_items: - text = str(value).strip() - if text.isdigit() and text not in seen: - seen.append(text) - return seen - - -def render_template(template, values): - rendered = str(template or '') - for key, value in values.items(): - rendered = rendered.replace('{' + key + '}', str(value)) - return rendered - - -def ensure_under_home(path_value): - target = os.path.abspath(path_value) - try: - common = os.path.commonpath([home_root, target]) - except ValueError: - common = '' - if common != os.path.abspath(home_root): - fail(f"Refusing to write outside server home: {target}") - return target - - -def safe_folder_name(value, fallback): - text = str(value or '').strip() - if not text or '..' in text or '/' in text or '\\' in text or '\x00' in text: - return fallback - return text - - -def truthy(value): - if isinstance(value, bool): - return value - return str(value).strip().lower() in ('1', 'yes', 'true', 'on') - - -def resolve_steamcmd(explicit_path=''): - candidates = [] - explicit_path = str(explicit_path or '').strip() - if explicit_path: - candidates.append(explicit_path) - env_value = os.environ.get('STEAMCMD_PATH', '').strip() - if env_value: - candidates.append(env_value) - for path_value in ( - '/home/gameserver/steamcmd/steamcmd.sh', - shutil.which('steamcmd'), - shutil.which('steamcmd.exe'), - ): - if path_value: - candidates.append(path_value) - for candidate in candidates: - if candidate and os.path.isfile(candidate): - return candidate - fail('SteamCMD is missing on the agent host.') - - -def sync_copy(src, dst): - if not os.path.isdir(src): - fail(f"Workshop download source was not found: {src}") - os.makedirs(dst, exist_ok=True) - for entry in os.listdir(src): - source_entry = os.path.join(src, entry) - target_entry = os.path.join(dst, entry) - if os.path.isdir(source_entry): - sync_copy(source_entry, target_entry) - else: - os.makedirs(os.path.dirname(target_entry), exist_ok=True) - shutil.copy2(source_entry, target_entry) - - -def copy_bikeys(mod_path, keys_target, workshop_id): - if not os.path.isdir(mod_path): - return 0 - keys_target = ensure_under_home(keys_target) - os.makedirs(keys_target, exist_ok=True) - copied = 0 - for root, dirs, files in os.walk(mod_path): - for filename in files: - if not filename.lower().endswith('.bikey'): - continue - source_file = os.path.join(root, filename) - target_file = os.path.join(keys_target, filename) - shutil.copy2(source_file, target_file) - copied += 1 - log(f"workshop_id={workshop_id} key={target_file}", 'Copying Key') - if copied == 0: - log(f"workshop_id={workshop_id} no .bikey files found; continuing", 'Copying Key') - return copied - - -try: - with open(manifest_path, 'r', encoding='utf-8') as handle: - manifest = json.load(handle) - - extra = manifest.get('extra') or {} - action = str(manifest.get('action', '')).strip() - raw_items = manifest.get('items', []) - if isinstance(raw_items, dict): - raw_items = raw_items.get('workshop_item_ids', []) - items = uniq_numeric_items(raw_items) - if not items: - fail('No Workshop IDs were found in the manifest.') - - workshop_app_id = str(extra.get('workshop_app_id') or manifest.get('workshop_app_id') or '').strip() - steam_app_id = str(extra.get('steam_app_id') or manifest.get('steam_app_id') or '').strip() - server_root = ensure_under_home(extra.get('server_root') or home_root) - steamcmd_path = '' - if action in ('install', 'update', 'check_updates', 'download_only', 'validate_files'): - steamcmd_path = resolve_steamcmd(extra.get('steamcmd_path') or '') - post_install_script = str(extra.get('post_install_script') or '').strip() - item_details = manifest.get('item_details') or extra.get('item_details') or {} - default_install_strategy = str(manifest.get('install_strategy') or extra.get('install_strategy') or 'copy_to_mod_folder').strip() - default_download_dir = extra.get('workshop_download_dir') or os.path.join(server_root, 'steamapps', 'workshop', 'content', workshop_app_id or steam_app_id) - - action_label = 'Queued' if action in ('install', 'update', 'check_updates', 'download_only', 'validate_files') else action - log(f"action={action} manifest={manifest_path} steam_app_id={steam_app_id or 'n/a'} workshop_app_id={workshop_app_id or 'n/a'}", action_label) - - for workshop_id in items: - detail = item_details.get(workshop_id) or item_details.get(str(workshop_id)) or {} - folder_name = safe_folder_name(detail.get('folder_name') or extra.get('optional_folder_name') or '', '@' + workshop_id) - install_strategy = str(detail.get('install_strategy') or default_install_strategy).strip() or 'copy_to_mod_folder' - template_values = { - 'HOME_ID': manifest.get('home_id', ''), - 'SERVER_ROOT': server_root, - 'GAME_ROOT': server_root, - 'WORKSHOP_ID': workshop_id, - 'WORKSHOP_APP_ID': workshop_app_id, - 'STEAM_APP_ID': steam_app_id, - 'FOLDER_NAME': folder_name, - 'MOD_FOLDER': folder_name, - } - target_template = str(detail.get('target_path_template') or extra.get('target_path_template') or '{SERVER_ROOT}/workshop/{MOD_FOLDER}') - target_path = str(detail.get('target_path_resolved') or extra.get('target_path_resolved') or '').strip() - if len(items) != 1 or not target_path: - target_path = render_template(target_template, template_values) - target_path = ensure_under_home(target_path) - keys_target_path = str(detail.get('keys_target_path') or extra.get('keys_target_path') or os.path.join(server_root, 'keys')) - should_copy_keys = truthy(detail.get('copy_keys', extra.get('copy_keys', install_strategy in ('dayz_mod_folder', 'arma_mod_folder')))) - - download_dir = ensure_under_home(render_template(default_download_dir, template_values)) - source_path = os.path.join(download_dir, workshop_id) - - if action in ('install', 'update', 'check_updates', 'download_only', 'validate_files'): - if not workshop_app_id: - fail(f"Workshop App ID is missing for Workshop item {workshop_id}.") - fd, runscript_path = tempfile.mkstemp(prefix=f"steamcmd_workshop_{workshop_id}_", suffix=".txt", dir=manifest_dir, text=True) - with os.fdopen(fd, 'w', encoding='utf-8') as script_handle: - script_handle.write("@ShutdownOnFailedCommand 0\n") - script_handle.write("@NoPromptForPassword 1\n") - script_handle.write(f"force_install_dir {server_root}\n") - script_handle.write("login anonymous\n") - script_handle.write(f"workshop_download_item {workshop_app_id} {workshop_id} validate\n") - script_handle.write("quit\n") - command = [steamcmd_path, '+runscript', runscript_path] - log(f"workshop_id={workshop_id} runscript={runscript_path}", 'Downloading Workshop Item') - result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, cwd=server_root) - if result.stdout: - for line in result.stdout.splitlines(): - log(f"steamcmd[{workshop_id}] {line}") - if result.returncode != 0: - fail(f"SteamCMD failed for Workshop item {workshop_id} with exit code {result.returncode}.") - if not os.path.isdir(source_path): - fail(f"SteamCMD did not create the expected Workshop cache path: {source_path}") - - if action not in ('check_updates', 'download_only', 'validate_files'): - log(f"workshop_id={workshop_id} strategy={install_strategy} install_path={target_path}", 'Extracting/Copying') - sync_copy(source_path, target_path) - log(f"workshop_id={workshop_id} final_folder_path={target_path}", 'Applying Folder Name') - if should_copy_keys: - copy_bikeys(target_path, keys_target_path, workshop_id) - if post_install_script: - log(f"workshop_id={workshop_id} cwd={server_root}", 'Running Post-install Script') - post_result = subprocess.run(['bash', '-lc', post_install_script], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, cwd=server_root) - if post_result.stdout: - for line in post_result.stdout.splitlines(): - log(f"post_install[{workshop_id}] {line}") - if post_result.returncode != 0: - fail(f"Post-install script failed for Workshop item {workshop_id} with exit code {post_result.returncode}.") - elif action == 'download_only': - log(f"workshop_id={workshop_id} cached_path={source_path}", 'Downloaded Only') - elif action == 'validate_files': - log(f"workshop_id={workshop_id} cached_path={source_path}", 'Validated') - log(f"workshop_id={workshop_id} install_path={target_path}", 'Completed') - elif action == 'remove': - if os.path.exists(target_path): - timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') - removed_path = os.path.join(removed_dir, f"{workshop_id}_{timestamp}") - shutil.move(target_path, removed_path) - log(f"workshop_id={workshop_id} removed_path={removed_path}", 'Completed') - else: - log(f"workshop_id={workshop_id} target_path_missing={target_path}", 'Completed') - else: - fail(f"Unknown workshop action: {action}") -except RuntimeError: - sys.exit(1) -PY diff --git a/Panel/modules/addonsmanager/scripts/workshop/generic_steam_workshop_windows_cygwin.sh b/Panel/modules/addonsmanager/scripts/workshop/generic_steam_workshop_windows_cygwin.sh deleted file mode 100755 index 21f7de40..00000000 --- a/Panel/modules/addonsmanager/scripts/workshop/generic_steam_workshop_windows_cygwin.sh +++ /dev/null @@ -1,247 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -MANIFEST_PATH="${1:-}" -if [[ -z "$MANIFEST_PATH" ]]; then - echo "Usage: $0 " - exit 1 -fi - -python3 - "$MANIFEST_PATH" <<'PY' -import json -import os -import shutil -import subprocess -import sys -import tempfile -from datetime import datetime - -manifest_path = os.path.abspath(sys.argv[1]) -if not os.path.isfile(manifest_path): - print(f"Manifest not found: {manifest_path}") - sys.exit(1) - -manifest_dir = os.path.dirname(manifest_path) -home_root = os.path.dirname(manifest_dir) -log_file = os.path.join(manifest_dir, 'workshop_install_windows.log') -removed_dir = os.path.join(manifest_dir, 'workshop', 'removed') -os.makedirs(removed_dir, exist_ok=True) - - -def log(message, status=None): - line = f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}]" - if status: - line += f" [{status}]" - line += f" {message}" - print(line) - with open(log_file, 'a', encoding='utf-8') as handle: - handle.write(line + "\n") - - -def fail(message): - log(message, 'Failed') - raise RuntimeError(message) - - -def uniq_numeric_items(raw_items): - seen = [] - for value in raw_items: - text = str(value).strip() - if text.isdigit() and text not in seen: - seen.append(text) - return seen - - -def render_template(template, values): - rendered = str(template or '') - for key, value in values.items(): - rendered = rendered.replace('{' + key + '}', str(value)) - return rendered - - -def ensure_under_home(path_value): - target = os.path.abspath(path_value) - try: - common = os.path.commonpath([home_root, target]) - except ValueError: - common = '' - if common != os.path.abspath(home_root): - fail(f"Refusing to write outside server home: {target}") - return target - - -def safe_folder_name(value, fallback): - text = str(value or '').strip() - if not text or '..' in text or '/' in text or '\\' in text or '\x00' in text: - return fallback - return text - - -def truthy(value): - if isinstance(value, bool): - return value - return str(value).strip().lower() in ('1', 'yes', 'true', 'on') - - -def resolve_steamcmd(explicit_path=''): - candidates = [] - explicit_path = str(explicit_path or '').strip() - if explicit_path: - candidates.append(explicit_path) - env_value = os.environ.get('STEAMCMD_PATH', '').strip() - if env_value: - candidates.append(env_value) - for path_value in ( - '/home/gameserver/steamcmd/steamcmd.sh', - shutil.which('steamcmd'), - shutil.which('steamcmd.exe'), - shutil.which('steamcmd.sh'), - ): - if path_value: - candidates.append(path_value) - for candidate in candidates: - if candidate and os.path.isfile(candidate): - return candidate - fail('SteamCMD is missing on the agent host.') - - -def sync_copy(src, dst): - if not os.path.isdir(src): - fail(f"Workshop download source was not found: {src}") - os.makedirs(dst, exist_ok=True) - for entry in os.listdir(src): - source_entry = os.path.join(src, entry) - target_entry = os.path.join(dst, entry) - if os.path.isdir(source_entry): - sync_copy(source_entry, target_entry) - else: - os.makedirs(os.path.dirname(target_entry), exist_ok=True) - shutil.copy2(source_entry, target_entry) - - -def copy_bikeys(mod_path, keys_target, workshop_id): - if not os.path.isdir(mod_path): - return 0 - keys_target = ensure_under_home(keys_target) - os.makedirs(keys_target, exist_ok=True) - copied = 0 - for root, dirs, files in os.walk(mod_path): - for filename in files: - if not filename.lower().endswith('.bikey'): - continue - source_file = os.path.join(root, filename) - target_file = os.path.join(keys_target, filename) - shutil.copy2(source_file, target_file) - copied += 1 - log(f"workshop_id={workshop_id} key={target_file}", 'Copying Key') - if copied == 0: - log(f"workshop_id={workshop_id} no .bikey files found; continuing", 'Copying Key') - return copied - - -try: - with open(manifest_path, 'r', encoding='utf-8') as handle: - manifest = json.load(handle) - - extra = manifest.get('extra') or {} - action = str(manifest.get('action', '')).strip() - raw_items = manifest.get('items', []) - if isinstance(raw_items, dict): - raw_items = raw_items.get('workshop_item_ids', []) - items = uniq_numeric_items(raw_items) - if not items: - fail('No Workshop IDs were found in the manifest.') - - workshop_app_id = str(extra.get('workshop_app_id') or manifest.get('workshop_app_id') or '').strip() - steam_app_id = str(extra.get('steam_app_id') or manifest.get('steam_app_id') or '').strip() - server_root = ensure_under_home(extra.get('server_root') or home_root) - steamcmd_path = '' - if action in ('install', 'update', 'check_updates', 'download_only', 'validate_files'): - steamcmd_path = resolve_steamcmd(extra.get('steamcmd_path') or '') - post_install_script = str(extra.get('post_install_script') or '').strip() - item_details = manifest.get('item_details') or extra.get('item_details') or {} - default_install_strategy = str(manifest.get('install_strategy') or extra.get('install_strategy') or 'copy_to_mod_folder').strip() - default_download_dir = extra.get('workshop_download_dir') or os.path.join(server_root, 'steamapps', 'workshop', 'content', workshop_app_id or steam_app_id) - - action_label = 'Queued' if action in ('install', 'update', 'check_updates', 'download_only', 'validate_files') else action - log(f"action={action} manifest={manifest_path} steam_app_id={steam_app_id or 'n/a'} workshop_app_id={workshop_app_id or 'n/a'}", action_label) - - for workshop_id in items: - detail = item_details.get(workshop_id) or item_details.get(str(workshop_id)) or {} - folder_name = safe_folder_name(detail.get('folder_name') or extra.get('optional_folder_name') or '', '@' + workshop_id) - install_strategy = str(detail.get('install_strategy') or default_install_strategy).strip() or 'copy_to_mod_folder' - template_values = { - 'HOME_ID': manifest.get('home_id', ''), - 'SERVER_ROOT': server_root, - 'GAME_ROOT': server_root, - 'WORKSHOP_ID': workshop_id, - 'WORKSHOP_APP_ID': workshop_app_id, - 'STEAM_APP_ID': steam_app_id, - 'FOLDER_NAME': folder_name, - 'MOD_FOLDER': folder_name, - } - target_template = str(detail.get('target_path_template') or extra.get('target_path_template') or '{SERVER_ROOT}/workshop/{MOD_FOLDER}') - target_path = str(detail.get('target_path_resolved') or extra.get('target_path_resolved') or '').strip() - if len(items) != 1 or not target_path: - target_path = render_template(target_template, template_values) - target_path = ensure_under_home(target_path) - keys_target_path = str(detail.get('keys_target_path') or extra.get('keys_target_path') or os.path.join(server_root, 'keys')) - should_copy_keys = truthy(detail.get('copy_keys', extra.get('copy_keys', install_strategy in ('dayz_mod_folder', 'arma_mod_folder')))) - - download_dir = ensure_under_home(render_template(default_download_dir, template_values)) - source_path = os.path.join(download_dir, workshop_id) - - if action in ('install', 'update', 'check_updates', 'download_only', 'validate_files'): - if not workshop_app_id: - fail(f"Workshop App ID is missing for Workshop item {workshop_id}.") - fd, runscript_path = tempfile.mkstemp(prefix=f"steamcmd_workshop_{workshop_id}_", suffix=".txt", dir=manifest_dir, text=True) - with os.fdopen(fd, 'w', encoding='utf-8') as script_handle: - script_handle.write("@ShutdownOnFailedCommand 0\n") - script_handle.write("@NoPromptForPassword 1\n") - script_handle.write(f"force_install_dir {server_root}\n") - script_handle.write("login anonymous\n") - script_handle.write(f"workshop_download_item {workshop_app_id} {workshop_id} validate\n") - script_handle.write("quit\n") - command = [steamcmd_path, '+runscript', runscript_path] - log(f"workshop_id={workshop_id} runscript={runscript_path}", 'Downloading Workshop Item') - result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, cwd=server_root) - if result.stdout: - for line in result.stdout.splitlines(): - log(f"steamcmd[{workshop_id}] {line}") - if result.returncode != 0: - fail(f"SteamCMD failed for Workshop item {workshop_id} with exit code {result.returncode}.") - if not os.path.isdir(source_path): - fail(f"SteamCMD did not create the expected Workshop cache path: {source_path}") - - if action not in ('check_updates', 'download_only', 'validate_files'): - log(f"workshop_id={workshop_id} strategy={install_strategy} install_path={target_path}", 'Extracting/Copying') - sync_copy(source_path, target_path) - log(f"workshop_id={workshop_id} final_folder_path={target_path}", 'Applying Folder Name') - if should_copy_keys: - copy_bikeys(target_path, keys_target_path, workshop_id) - if post_install_script: - log(f"workshop_id={workshop_id} cwd={server_root}", 'Running Post-install Script') - post_result = subprocess.run(['bash', '-lc', post_install_script], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, cwd=server_root) - if post_result.stdout: - for line in post_result.stdout.splitlines(): - log(f"post_install[{workshop_id}] {line}") - if post_result.returncode != 0: - fail(f"Post-install script failed for Workshop item {workshop_id} with exit code {post_result.returncode}.") - elif action == 'download_only': - log(f"workshop_id={workshop_id} cached_path={source_path}", 'Downloaded Only') - elif action == 'validate_files': - log(f"workshop_id={workshop_id} cached_path={source_path}", 'Validated') - log(f"workshop_id={workshop_id} install_path={target_path}", 'Completed') - elif action == 'remove': - if os.path.exists(target_path): - timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') - removed_path = os.path.join(removed_dir, f"{workshop_id}_{timestamp}") - shutil.move(target_path, removed_path) - log(f"workshop_id={workshop_id} removed_path={removed_path}", 'Completed') - else: - log(f"workshop_id={workshop_id} target_path_missing={target_path}", 'Completed') - else: - fail(f"Unknown workshop action: {action}") -except RuntimeError: - sys.exit(1) -PY diff --git a/Panel/modules/addonsmanager/scripts/workshop/panel_generated_steamcmd_job b/Panel/modules/addonsmanager/scripts/workshop/panel_generated_steamcmd_job deleted file mode 100755 index f8791e3a..00000000 --- a/Panel/modules/addonsmanager/scripts/workshop/panel_generated_steamcmd_job +++ /dev/null @@ -1,137 +0,0 @@ -#!/usr/bin/env bash -set -u -MANIFEST="${1:-}" -if [ -z "$MANIFEST" ] || [ ! -f "$MANIFEST" ]; then - echo "ERROR: Workshop manifest missing: $MANIFEST" - exit 2 -fi -MANIFEST_DIR="$(dirname "$MANIFEST")" -LOG="$MANIFEST_DIR/workshop_install.log" -touch "$LOG" -log() { - echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG" -} -json_value() { - local key="$1" - KEY="$key" MANIFEST_FILE="$MANIFEST" perl -0777 -e ' - my $k = $ENV{"KEY"}; - my $f = $ENV{"MANIFEST_FILE"}; - open my $fh, "<", $f or exit 1; - local $/; - my $json = <$fh>; - if ($json =~ /"\Q$k\E"\s*:\s*"([^"]*)"/s) { - my $v = $1; - $v =~ s/\\\//\//g; - print $v; - exit 0; - } - if ($json =~ /"\Q$k\E"\s*:\s*([0-9]+)/s) { - print $1; - exit 0; - } - exit 0; - ' -} - -json_items() { - MANIFEST_FILE="$MANIFEST" perl -0777 -e ' - my $f = $ENV{"MANIFEST_FILE"}; - open my $fh, "<", $f or exit 1; - local $/; - my $json = <$fh>; - - my %seen; - - if ($json =~ /"items"\s*:\s*\[(.*?)\]/s) { - my $x = $1; - while ($x =~ /"([0-9]{3,20})"/g) { - print "$1\n" unless $seen{$1}++; - } - while ($x =~ /(?/dev/null 2>&1; then command -v "$c"; return 0; fi - if [ -f "$c" ]; then echo "$c"; return 0; fi - done - return 1 -} -log "GSP Workshop job starting. action=$ACTION appid=$APPID server_path=$SERVER_PATH" -if [ "$ACTION" = "remove" ]; then - for id in $ITEMS; do - log "Removing Workshop item files for $id if present." - rm -rf "$SERVER_PATH/@$id" "$SERVER_PATH/workshop/@$id" "$SERVER_PATH/steamapps/workshop/content/$APPID/$id" - done - log "Remove job complete." - exit 0 -fi -STEAMCMD="$(find_steamcmd)" || { - log "ERROR: steamcmd was not found. Install SteamCMD on the agent host or set STEAMCMD_PATH." - exit 127 -} -RUNSCRIPT="$MANIFEST_DIR/steamcmd_workshop_$$.txt" -{ - echo "@ShutdownOnFailedCommand 0" - echo "@NoPromptForPassword 1" - echo "login anonymous" - echo "force_install_dir $SERVER_PATH" - for id in $ITEMS; do - echo "workshop_download_item $APPID $id validate" - done - echo "quit" -} > "$RUNSCRIPT" -log "Using SteamCMD: $STEAMCMD" -log "Running SteamCMD runscript: $RUNSCRIPT" -"$STEAMCMD" +runscript "$RUNSCRIPT" 2>&1 | tee -a "$LOG" -rc=${PIPESTATUS[0]} -if [ "$rc" -ne 0 ]; then - log "ERROR: SteamCMD failed with exit $rc" - exit "$rc" -fi -for id in $ITEMS; do - SRC="$SERVER_PATH/steamapps/workshop/content/$APPID/$id" - DST="$SERVER_PATH/@$id" - if [ ! -d "$SRC" ]; then - log "ERROR: downloaded Workshop source not found: $SRC" - exit 5 - fi - log "Installing Workshop item $id to $DST" - rm -rf "$DST" - cp -a "$SRC" "$DST" - if [ -d "$DST/keys" ]; then - mkdir -p "$SERVER_PATH/keys" - find "$DST/keys" -type f -iname '*.bikey' -exec cp -f {} "$SERVER_PATH/keys/" \; - fi -done -log "Workshop job complete." -exit 0 diff --git a/Panel/modules/addonsmanager/server_content_helpers.php b/Panel/modules/addonsmanager/server_content_helpers.php index fed5465c..711c25de 100644 --- a/Panel/modules/addonsmanager/server_content_helpers.php +++ b/Panel/modules/addonsmanager/server_content_helpers.php @@ -694,7 +694,6 @@ function scm_get_install_methods() { return array( 'download_zip' => 'Downloadable Mod', - 'steam_workshop' => 'Steam Workshop Mods', 'config_edit' => 'Configuration Package', 'post_script' => 'Scripted Installer', ); @@ -704,7 +703,6 @@ function scm_get_install_method_help_text() { return array( 'download_zip' => 'Download and extract a ZIP, RAR, or archive file.', - 'steam_workshop' => 'Users install Steam Workshop items from their server page. App IDs, install paths, key-copy rules, and launch parameter format come from the game XML.', 'config_edit' => 'Install configuration files, profiles, or templates.', 'post_script' => 'Run a custom scripted installation process.', ); @@ -724,7 +722,6 @@ function scm_get_install_method_validation_errors() { return array( 'download_zip' => 'Please enter a download URL.', - 'steam_workshop' => 'Workshop behavior is configured in the game XML.', 'config_edit' => 'Please enter the config target and edit action.', 'post_script' => 'Please enter the installer script/action.', ); diff --git a/Panel/modules/addonsmanager/server_content_helpers.php.bak.20260609-111046 b/Panel/modules/addonsmanager/server_content_helpers.php.bak.20260609-111046 new file mode 100644 index 00000000..fed5465c --- /dev/null +++ b/Panel/modules/addonsmanager/server_content_helpers.php.bak.20260609-111046 @@ -0,0 +1,1301 @@ +query("ALTER TABLE `".OGP_DB_PREFIX."addons` MODIFY `addon_type` VARCHAR(32) NOT NULL"); + $ok = (bool)$db->query( + "CREATE TABLE IF NOT EXISTS `".OGP_DB_PREFIX."server_content_workshop` ( + `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + `home_id` INT NOT NULL, + `home_cfg_id` INT NOT NULL, + `remote_server_id` INT NULL, + `workshop_app_id` VARCHAR(32) NULL, + `workshop_item_id` VARCHAR(64) NOT NULL, + `title` VARCHAR(255) NULL, + `install_path` VARCHAR(512) NULL, + `install_strategy` VARCHAR(64) NULL, + `enabled` TINYINT(1) NOT NULL DEFAULT 1, + `load_order` INT NOT NULL DEFAULT 0, + `install_state` VARCHAR(32) NOT NULL DEFAULT 'selected', + `last_installed_at` DATETIME NULL, + `last_updated_at` DATETIME NULL, + `last_error` TEXT NULL, + `created_by` INT NULL, + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` DATETIME NULL, + UNIQUE KEY `uniq_home_workshop_item` (`home_id`, `workshop_item_id`), + KEY `idx_home_id` (`home_id`), + KEY `idx_home_cfg_id` (`home_cfg_id`), + KEY `idx_install_state` (`install_state`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" + ); + + // Idempotently add content_id column (db_version 6). + $wk_table = $db->realEscapeSingle(OGP_DB_PREFIX . 'server_content_workshop'); + $col_check = $db->resultQuery( + "SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = '{$wk_table}' + AND COLUMN_NAME = 'content_id'" + ); + if (empty($col_check)) { + $db->query( + "ALTER TABLE `".OGP_DB_PREFIX."server_content_workshop` + ADD COLUMN `content_id` INT NULL AFTER `id`, + ADD KEY `idx_content_id` (`content_id`)" + ); + } + + $workshop_columns = array( + 'install_path' => "VARCHAR(512) NULL AFTER `title`", + 'install_strategy' => "VARCHAR(64) NULL AFTER `install_path`", + 'enabled' => "TINYINT(1) NOT NULL DEFAULT 1 AFTER `install_strategy`", + 'load_order' => "INT NOT NULL DEFAULT 0 AFTER `enabled`", + 'update_policy' => "VARCHAR(64) NOT NULL DEFAULT 'manual' AFTER `load_order`", + 'pending_action' => "VARCHAR(64) NULL AFTER `update_policy`", + ); + foreach ($workshop_columns as $col => $definition) { + $escaped_col = $db->realEscapeSingle($col); + $col_check = $db->resultQuery( + "SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = '{$wk_table}' + AND COLUMN_NAME = '{$escaped_col}'" + ); + if (empty($col_check)) { + $db->query("ALTER TABLE `".OGP_DB_PREFIX."server_content_workshop` ADD COLUMN `{$col}` {$definition}"); + } + } + + $db->query( + "CREATE TABLE IF NOT EXISTS `".OGP_DB_PREFIX."server_content_workshop_catalog` ( + `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + `workshop_id` VARCHAR(64) NOT NULL, + `app_id` VARCHAR(32) NOT NULL DEFAULT '', + `title` VARCHAR(255) NULL, + `install_count` INT NOT NULL DEFAULT 0, + `first_seen` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `last_installed` DATETIME NULL, + `last_updated` DATETIME NULL, + `published_date` DATETIME NULL, + `tags` TEXT NULL, + `game_key` VARCHAR(128) NULL, + `local_cache_path` VARCHAR(512) NULL, + `author` VARCHAR(255) NULL, + `thumbnail_url` VARCHAR(512) NULL, + UNIQUE KEY `uniq_workshop_app` (`workshop_id`, `app_id`), + KEY `idx_app_id` (`app_id`), + KEY `idx_install_count` (`install_count`), + KEY `idx_last_installed` (`last_installed`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" + ); + $catalog_table = $db->realEscapeSingle(OGP_DB_PREFIX . 'server_content_workshop_catalog'); + foreach (array( + 'author' => "VARCHAR(255) NULL AFTER `title`", + 'thumbnail_url' => "VARCHAR(512) NULL AFTER `author`", + ) as $col => $definition) { + $escaped_col = $db->realEscapeSingle($col); + $col_check = $db->resultQuery( + "SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = '{$catalog_table}' + AND COLUMN_NAME = '{$escaped_col}'" + ); + if (empty($col_check)) { + $db->query("ALTER TABLE `".OGP_DB_PREFIX."server_content_workshop_catalog` ADD COLUMN `{$col}` {$definition}"); + } + } + + return $ok; +} + +function scm_get_home_for_user($db, $home_id, $user_id) +{ + $home_id = (int)$home_id; + $user_id = (int)$user_id; + if ($home_id <= 0 || $user_id <= 0) { + return false; + } + if ($db->isAdmin($user_id)) { + return $db->getGameHome($home_id); + } + return $db->getUserGameHome($user_id, $home_id); +} + +function scm_get_workshop_saved_count($db, $home_id) +{ + $home_id = (int)$home_id; + if ($home_id <= 0 || !scm_ensure_workshop_schema($db)) { + return 0; + } + $rows = $db->resultQuery( + "SELECT COUNT(*) AS cnt FROM `".OGP_DB_PREFIX."server_content_workshop` WHERE home_id=".$home_id." AND install_state<>'removed'" + ); + if (!is_array($rows) || !isset($rows[0]['cnt'])) { + return 0; + } + return (int)$rows[0]['cnt']; +} + +function scm_get_workshop_rows($db, $home_id) +{ + $home_id = (int)$home_id; + if ($home_id <= 0 || !scm_ensure_workshop_schema($db)) { + return array(); + } + $rows = $db->resultQuery( + "SELECT * FROM `".OGP_DB_PREFIX."server_content_workshop` WHERE home_id=".$home_id." ORDER BY load_order ASC, created_at DESC, workshop_item_id ASC" + ); + return is_array($rows) ? $rows : array(); +} + +function scm_get_workshop_update_policies() +{ + return array( + 'manual' => 'Manual update only', + 'scheduled' => 'Update on scheduled task', + 'update_now' => 'Update immediately', + 'update_and_restart' => 'Update immediately and restart', + 'download_only' => 'Download now, install later', + 'install_on_restart' => 'Install during next restart', + ); +} + +function scm_normalize_workshop_update_policy($policy) +{ + $policy = trim((string)$policy); + $policies = scm_get_workshop_update_policies(); + return isset($policies[$policy]) ? $policy : 'manual'; +} + +function scm_workshop_set_enabled($db, $home_id, array $item_ids, $enabled) +{ + if (empty($item_ids)) { + return false; + } + $escaped_ids = array(); + foreach ($item_ids as $item_id) { + $escaped_ids[] = "'" . $db->realEscapeSingle((string)$item_id) . "'"; + } + return (bool)$db->query( + "UPDATE `".OGP_DB_PREFIX."server_content_workshop` + SET enabled=" . ((int)$enabled ? 1 : 0) . ", updated_at=NOW() + WHERE home_id=".(int)$home_id." AND workshop_item_id IN (".implode(",", $escaped_ids).")" + ); +} + +function scm_workshop_set_update_policy($db, $home_id, array $item_ids, $policy) +{ + if (empty($item_ids)) { + return false; + } + $policy = scm_normalize_workshop_update_policy($policy); + $pending = ''; + if ($policy === 'download_only') { + $pending = 'install_on_restart'; + } + elseif ($policy === 'install_on_restart') { + $pending = 'install_on_restart'; + } + $escaped_ids = array(); + foreach ($item_ids as $item_id) { + $escaped_ids[] = "'" . $db->realEscapeSingle((string)$item_id) . "'"; + } + return (bool)$db->query( + "UPDATE `".OGP_DB_PREFIX."server_content_workshop` + SET update_policy='" . $db->realEscapeSingle($policy) . "', + pending_action=" . ($pending === '' ? "NULL" : "'" . $db->realEscapeSingle($pending) . "'") . ", + updated_at=NOW() + WHERE home_id=".(int)$home_id." AND workshop_item_id IN (".implode(",", $escaped_ids).")" + ); +} + +function scm_workshop_catalog_sort_sql($sort) +{ + $sort = trim((string)$sort); + $allowed = array( + 'name' => 'title ASC, workshop_id ASC', + 'install_count' => 'install_count DESC, last_installed DESC, workshop_id ASC', + 'published_date' => 'published_date DESC, title ASC, workshop_id ASC', + 'last_updated' => 'last_updated DESC, title ASC, workshop_id ASC', + 'last_installed' => 'last_installed DESC, title ASC, workshop_id ASC', + 'workshop_id' => 'CAST(workshop_id AS UNSIGNED) ASC, workshop_id ASC', + ); + return isset($allowed[$sort]) ? $allowed[$sort] : $allowed['last_installed']; +} + +function scm_get_workshop_catalog_rows($db, $app_id = '', $sort = 'last_installed', $limit = 50, $query = '', $tag = '') +{ + if (!scm_ensure_workshop_schema($db)) { + return array(); + } + $where_parts = array(); + $app_id = trim((string)$app_id); + if ($app_id !== '' && preg_match('/^[0-9]+$/', $app_id)) { + $where_parts[] = "app_id='" . $db->realEscapeSingle($app_id) . "'"; + } + $query = trim((string)$query); + if ($query !== '') { + $query_id = scm_extract_workshop_item_id($query); + if ($query_id !== '') { + $where_parts[] = "workshop_id='" . $db->realEscapeSingle($query_id) . "'"; + } else { + $like = "%" . $db->realEscapeSingle($query) . "%"; + $where_parts[] = "(title LIKE '{$like}' OR author LIKE '{$like}' OR tags LIKE '{$like}' OR game_key LIKE '{$like}')"; + } + } + $tag = trim((string)$tag); + if ($tag !== '') { + $like = "%" . $db->realEscapeSingle($tag) . "%"; + $where_parts[] = "tags LIKE '{$like}'"; + } + $where = empty($where_parts) ? '' : ('WHERE ' . implode(' AND ', $where_parts)); + $limit = (int)$limit; + if ($limit <= 0 || $limit > 200) { + $limit = 50; + } + $rows = $db->resultQuery( + "SELECT * FROM `".OGP_DB_PREFIX."server_content_workshop_catalog` + {$where} + ORDER BY " . scm_workshop_catalog_sort_sql($sort) . " + LIMIT {$limit}" + ); + return is_array($rows) ? $rows : array(); +} + + +function scm_fetch_steam_workshop_details(array $item_ids) +{ + $ids = array(); + foreach ($item_ids as $id) { + $id = (string)$id; + if (preg_match('/^[0-9]{3,20}$/', $id)) { + $ids[$id] = $id; + } + } + if (empty($ids)) { + return array(); + } + $post = array('itemcount' => count($ids)); + $i = 0; + foreach (array_values($ids) as $id) { + $post['publishedfileids['.$i.']'] = $id; + $i++; + } + $context = stream_context_create(array('http' => array( + 'method' => 'POST', + 'header' => "Content-type: application/x-www-form-urlencoded\r\n", + 'content' => http_build_query($post), + 'timeout' => 8, + ))); + $json = @file_get_contents('https://api.steampowered.com/ISteamRemoteStorage/GetPublishedFileDetails/v1/', false, $context); + if ($json === false || $json === '') { + $json = @file_get_contents('http://api.steampowered.com/ISteamRemoteStorage/GetPublishedFileDetails/v1/', false, $context); + } + if ($json === false || $json === '') { + return array(); + } + $data = json_decode($json, true); + if (!is_array($data) || empty($data['response']['publishedfiledetails'])) { + return array(); + } + $out = array(); + foreach ((array)$data['response']['publishedfiledetails'] as $row) { + if (empty($row['publishedfileid'])) { + continue; + } + $id = (string)$row['publishedfileid']; + $out[$id] = array( + 'title' => isset($row['title']) ? (string)$row['title'] : '', + 'author' => '', + 'thumbnail_url' => isset($row['preview_url']) ? (string)$row['preview_url'] : '', + 'published_date' => !empty($row['time_created']) ? date('Y-m-d H:i:s', (int)$row['time_created']) : null, + 'last_updated' => !empty($row['time_updated']) ? date('Y-m-d H:i:s', (int)$row['time_updated']) : null, + ); + } + return $out; +} +function scm_workshop_record_catalog_items($db, $workshop_app_id, array $item_ids, array $home_info = array(), array $item_details = array(), $mark_update = false) +{ + if (empty($item_ids) || !scm_ensure_workshop_schema($db)) { + return false; + } + $workshop_app_id = preg_match('/^[0-9]+$/', (string)$workshop_app_id) ? (string)$workshop_app_id : ''; + $game_key = isset($home_info['game_key']) ? (string)$home_info['game_key'] : ''; + $steam_details = scm_fetch_steam_workshop_details($item_ids); + $steam_details = scm_fetch_steam_workshop_details($item_ids); + foreach ($item_ids as $item_id) { + $item_id = (string)$item_id; + if (!preg_match('/^[0-9]+$/', $item_id)) { + continue; + } + $detail = isset($item_details[$item_id]) && is_array($item_details[$item_id]) ? $item_details[$item_id] : array(); + $title = isset($detail['title']) ? (string)$detail['title'] : ''; + $author = isset($detail['author']) ? (string)$detail['author'] : ''; + $thumbnail = isset($detail['thumbnail_url']) ? (string)$detail['thumbnail_url'] : ''; + if (isset($steam_details[$item_id])) { + if ($title === '' && !empty($steam_details[$item_id]['title'])) { + $title = (string)$steam_details[$item_id]['title']; + } + if ($thumbnail === '' && !empty($steam_details[$item_id]['thumbnail_url'])) { + $thumbnail = (string)$steam_details[$item_id]['thumbnail_url']; + } + } + if (isset($steam_details[$item_id])) { + if ($title === '' && !empty($steam_details[$item_id]['title'])) { + $title = (string)$steam_details[$item_id]['title']; + } + if ($thumbnail === '' && !empty($steam_details[$item_id]['thumbnail_url'])) { + $thumbnail = (string)$steam_details[$item_id]['thumbnail_url']; + } + } + $install_path = isset($detail['target_path_resolved']) ? (string)$detail['target_path_resolved'] : ''; + $db->query( + "INSERT INTO `".OGP_DB_PREFIX."server_content_workshop_catalog` + (workshop_id, app_id, title, author, thumbnail_url, install_count, first_seen, last_installed, last_updated, game_key, local_cache_path) + VALUES ( + '".$db->realEscapeSingle($item_id)."', + '".$db->realEscapeSingle($workshop_app_id)."', + ".($title === '' ? "NULL" : "'".$db->realEscapeSingle($title)."'").", + ".($author === '' ? "NULL" : "'".$db->realEscapeSingle($author)."'").", + ".($thumbnail === '' ? "NULL" : "'".$db->realEscapeSingle($thumbnail)."'").", + 1, + NOW(), + NOW(), + ".($mark_update ? "NOW()" : "NULL").", + ".($game_key === '' ? "NULL" : "'".$db->realEscapeSingle($game_key)."'").", + ".($install_path === '' ? "NULL" : "'".$db->realEscapeSingle($install_path)."'")." + ) + ON DUPLICATE KEY UPDATE + install_count=install_count+1, + title=IF(VALUES(title) IS NULL OR VALUES(title)='', title, VALUES(title)), + author=IF(VALUES(author) IS NULL OR VALUES(author)='', author, VALUES(author)), + thumbnail_url=IF(VALUES(thumbnail_url) IS NULL OR VALUES(thumbnail_url)='', thumbnail_url, VALUES(thumbnail_url)), + last_installed=NOW(), + last_updated=".($mark_update ? "NOW()" : "last_updated").", + game_key=IF(VALUES(game_key) IS NULL OR VALUES(game_key)='', game_key, VALUES(game_key)), + local_cache_path=IF(VALUES(local_cache_path) IS NULL OR VALUES(local_cache_path)='', local_cache_path, VALUES(local_cache_path))" + ); + } + return true; +} + +function scm_extract_workshop_item_id($value) +{ + $value = trim((string)$value); + if ($value === '') { + return ''; + } + if (preg_match('/^[0-9]{1,20}$/', $value)) { + return ltrim($value, '0') === '' ? '' : $value; + } + if (preg_match('/[?&]id=([0-9]{1,20})(?:[^0-9]|$)/i', $value, $matches)) { + return ltrim($matches[1], '0') === '' ? '' : $matches[1]; + } + if (preg_match('/steamcommunity\.com\/(?:sharedfiles|workshop)\/filedetails\/\?[^ \t\r\n]*id=([0-9]{1,20})/i', $value, $matches)) { + return ltrim($matches[1], '0') === '' ? '' : $matches[1]; + } + return ''; +} + +function scm_parse_workshop_ids($raw, &$invalid = array()) +{ + $invalid = array(); + $ids = array(); + // Accept IDs or full Steam Workshop URLs separated by commas, whitespace, + // or newlines. Customer input is reduced to numeric IDs before it reaches + // manifests, shell commands, or install paths. + $normalized = preg_replace('/[\r\n\t ]+/', ',', (string)$raw); + $parts = explode(',', (string)$normalized); + foreach ((array)$parts as $part) { + $value = trim((string)$part); + if ($value === '') { + continue; + } + $item_id = scm_extract_workshop_item_id($value); + if ($item_id === '') { + $invalid[] = $value; + continue; + } + $ids[$item_id] = $item_id; + } + return array_values($ids); +} + +function scm_parse_selected_workshop_ids($selected) +{ + $ids = array(); + if (!is_array($selected)) { + return $ids; + } + foreach ($selected as $item_id) { + $item_id = trim((string)$item_id); + if ($item_id !== '' && preg_match('/^[0-9]+$/', $item_id)) { + $ids[$item_id] = $item_id; + } + } + return array_values($ids); +} + +function scm_h($value) +{ + return htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8'); +} + +function scm_is_windows_home(array $home_info) +{ + $game_key = isset($home_info['game_key']) ? strtolower((string)$home_info['game_key']) : ''; + $cfg_file = isset($home_info['home_cfg_file']) ? strtolower((string)$home_info['home_cfg_file']) : ''; + return (strpos($game_key, 'win') !== false) || (strpos($cfg_file, 'win') !== false); +} + +function scm_path_is_under_home($home_path, $candidate_path) +{ + $home_path = rtrim(clean_path((string)$home_path), '/'); + $candidate_path = clean_path((string)$candidate_path); + if ($home_path === '' || $candidate_path === '') { + return false; + } + return strpos($candidate_path.'/', $home_path.'/') === 0; +} + +function scm_get_workshop_manifest_path(array $home_info) +{ + $home_path = rtrim(clean_path((string)$home_info['home_path']), '/'); + $manifest_path = clean_path($home_path . '/gsp_server_content/workshop_manifest.json'); + if (!scm_path_is_under_home($home_path, $manifest_path)) { + return false; + } + return $manifest_path; +} + +function scm_extract_workshop_app_id($server_xml) +{ + if (isset($server_xml->workshop_support->workshop_app_id)) { + $value = trim((string)$server_xml->workshop_support->workshop_app_id); + if ($value !== '' && preg_match('/^[0-9]+$/', $value)) { + return $value; + } + } + return ""; +} + +function scm_workshop_is_supported($server_xml) +{ + if (!isset($server_xml->workshop_support)) { + return false; + } + if (isset($server_xml->workshop_support->enabled) && !scm_workshop_xml_bool((string)$server_xml->workshop_support->enabled, true)) { + return false; + } + return scm_extract_workshop_app_id($server_xml) !== ''; +} + +function scm_extract_workshop_steam_app_id($server_xml) +{ + if (isset($server_xml->workshop_support->steam_app_id)) { + $value = trim((string)$server_xml->workshop_support->steam_app_id); + if ($value !== '' && preg_match('/^[0-9]+$/', $value)) { + return $value; + } + } + return ''; +} + +function scm_extract_workshop_install_path($server_xml) +{ + if (isset($server_xml->workshop_support->install_path)) { + $value = trim((string)$server_xml->workshop_support->install_path); + if ($value !== '' && preg_match('/^[^\\r\\n\\0]+$/', $value)) { + return $value; + } + } + return ''; +} + +function scm_workshop_xml_bool($value, $default = false) +{ + $value = strtolower(trim((string)$value)); + if ($value === '') { + return (bool)$default; + } + if (in_array($value, array('1', 'yes', 'true', 'on'), true)) { + return true; + } + if (in_array($value, array('0', 'no', 'false', 'off'), true)) { + return false; + } + return (bool)$default; +} + +function scm_workshop_mod_prefix($server_xml) +{ + if (isset($server_xml->workshop_support->mod_prefix)) { + $prefix = trim((string)$server_xml->workshop_support->mod_prefix); + if ($prefix !== '' && strpos($prefix, '/') === false && strpos($prefix, '\\') === false && strpos($prefix, "\0") === false) { + return $prefix; + } + } + return '@'; +} + +function scm_get_bundled_workshop_script_source(array $home_info) +{ + $filename = scm_is_windows_home($home_info) ? SCM_WORKSHOP_SCRIPT_WINDOWS_DEFAULT : SCM_WORKSHOP_SCRIPT_LINUX_DEFAULT; + return dirname(__FILE__) . '/scripts/workshop/' . $filename; +} + +function scm_get_agent_managed_workshop_script_path(array $home_info) +{ + $home_path = rtrim(clean_path((string)$home_info['home_path']), '/'); + $filename = 'workshop_job_' . date('Ymd_His') . '_' . mt_rand(1000, 9999) . '.sh'; + $remote_path = clean_path($home_path . '/gsp_server_content/jobs/workshop/' . $filename); + if (!scm_path_is_under_home($home_path, $remote_path)) { + return false; + } + return $remote_path; +} + +function scm_prepare_workshop_script_for_agent($remote, array $home_info, $server_xml, &$error = '') +{ + $error = ''; + $source_path = scm_get_bundled_workshop_script_source($home_info); + if (!is_file($source_path)) { + $error = 'Panel Workshop job template is missing: ' . $source_path; + return false; + } + + $remote_path = scm_get_agent_managed_workshop_script_path($home_info); + if ($remote_path === false) { + $error = 'Unable to resolve an agent-managed workshop script path for this server.'; + return false; + } + + $script_body = @file_get_contents($source_path); + if ($script_body === false || $script_body === '') { + $error = 'Failed to read Panel Workshop job template: ' . $source_path; + return false; + } + + $remote_dir = dirname($remote_path); + $remote->exec("mkdir -p " . escapeshellarg($remote_dir)); + if ((int)$remote->remote_writefile($remote_path, $script_body) !== 1) { + $error = 'Failed to sync workshop script to agent host.'; + return false; + } + $remote->exec("chmod 755 " . escapeshellarg($remote_path) . " >/dev/null 2>&1 || true"); + return $remote_path; +} + +function scm_get_default_workshop_target_template($install_strategy = '') +{ + $install_strategy = strtolower(trim((string)$install_strategy)); + if (in_array($install_strategy, array('dayz_mod_folder', 'arma_mod_folder'), true)) { + return SCM_WORKSHOP_TARGET_MOD_ROOT_DEFAULT; + } + return SCM_WORKSHOP_TARGET_GENERIC_DEFAULT; +} + +function scm_get_csrf_token() +{ + if (empty($_SESSION['addonsmanager_workshop_csrf'])) { + $_SESSION['addonsmanager_workshop_csrf'] = md5(uniqid((string)mt_rand(), true)); + } + return $_SESSION['addonsmanager_workshop_csrf']; +} + +function scm_validate_csrf_token($token) +{ + if (!isset($_SESSION['addonsmanager_workshop_csrf'])) { + return false; + } + return hash_equals((string)$_SESSION['addonsmanager_workshop_csrf'], (string)$token); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Phase 2 helpers – schema guard, cache mode, manifest, install history +// ───────────────────────────────────────────────────────────────────────────── + +/** + * Returns the allowed values for the server_content_cache_mode panel setting. + * + * disabled – Always install from the configured source. No scanning, + * no shared cache. (DEFAULT – safest choice) + * search_existing_servers – Agent may scan other local game-server folders for + * matching cacheable content and copy directly if safe. + * shared_cache – Agent may store cacheable content in a shared cache + * folder and reuse it on future installs. + * shared_cache_and_search – Both shared_cache and search_existing_servers are + * active simultaneously. + * + * Security note: only content explicitly marked is_cacheable=1 on the addon + * record may ever be shared or cached. Private configs, user-edited files, + * saves, databases, logs, and credentials must never be included. + * + * @return array key => human-readable label + */ +function scm_get_valid_cache_modes() +{ + return array( + 'disabled' => 'Disabled (always install from source)', + 'search_existing_servers' => 'Search existing servers (copy from local installs)', + 'shared_cache' => 'Shared cache (store and reuse cached copies)', + 'shared_cache_and_search' => 'Shared cache + search existing servers', + ); +} + +/** + * Reads the current server_content_cache_mode panel setting. + * Returns 'disabled' if not set. + * + * @param object $db Panel DB handle + * @return string One of the scm_get_valid_cache_modes() keys + */ +function scm_get_cache_mode($db) +{ + $valid = scm_get_valid_cache_modes(); + $value = ''; + if (method_exists($db, 'getSetting')) { + $value = (string)$db->getSetting('server_content_cache_mode'); + } + return array_key_exists($value, $valid) ? $value : 'disabled'; +} + +/** + * Returns allowed install_method values and their display labels. + * + * @return array + */ +function scm_get_install_methods() +{ + return array( + 'download_zip' => 'Downloadable Mod', + 'steam_workshop' => 'Steam Workshop Mods', + 'config_edit' => 'Configuration Package', + 'post_script' => 'Scripted Installer', + ); +} + +function scm_get_install_method_help_text() +{ + return array( + 'download_zip' => 'Download and extract a ZIP, RAR, or archive file.', + 'steam_workshop' => 'Users install Steam Workshop items from their server page. App IDs, install paths, key-copy rules, and launch parameter format come from the game XML.', + 'config_edit' => 'Install configuration files, profiles, or templates.', + 'post_script' => 'Run a custom scripted installation process.', + ); +} + +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'), + ); +} + +function scm_get_install_method_validation_errors() +{ + return array( + 'download_zip' => 'Please enter a download URL.', + 'steam_workshop' => 'Workshop behavior is configured in the game XML.', + 'config_edit' => 'Please enter the config target and edit action.', + 'post_script' => 'Please enter the installer script/action.', + ); +} + +function scm_get_install_method_default($value = '') +{ + $value = trim((string)$value); + if ($value === 'download_file') { + $value = 'download_zip'; + } + if ($value === 'create_folder') { + $value = 'config_edit'; + } + $methods = scm_get_install_methods(); + return isset($methods[$value]) ? $value : 'download_zip'; +} + +function scm_get_install_payload_keys() +{ + return array( + 'url', + 'path', + 'workshop_item_id', + 'workshop_app_id', + 'target_path_template', + 'optional_folder_name', + 'post_script', + 'config_edit_rule', + 'launch_param_additions', + ); +} + +function scm_collect_install_payload(array $defaults = array(), array $request = array(), array $override_keys = array()) +{ + $payload = array(); + foreach (scm_get_install_payload_keys() as $key) { + $payload[$key] = isset($defaults[$key]) ? trim((string)$defaults[$key]) : ''; + } + foreach ($override_keys as $key) { + if (array_key_exists($key, $request)) { + $payload[$key] = trim((string)$request[$key]); + } + } + return $payload; +} + +function scm_validate_numeric_content_value($value, $error_message, &$message, $allow_blank = false) +{ + $value = trim((string)$value); + if ($value === '') { + if ($allow_blank) { + $message = ''; + return true; + } + $message = $error_message; + return false; + } + if (!preg_match('/^[0-9]+$/', $value)) { + $message = $error_message; + return false; + } + $message = ''; + return true; +} + +function scm_validate_download_content(array $payload, &$message = '') +{ + $url = isset($payload['url']) ? trim((string)$payload['url']) : ''; + if ($url === '') { + $message = 'Please enter a download URL.'; + return false; + } + $message = ''; + return true; +} + +function scm_validate_workshop_content(array $payload, &$message = '') +{ + $message = ''; + return true; +} + +function scm_validate_workshop_user_ids($raw_ids, &$message = '') +{ + $invalid = array(); + $ids = scm_parse_workshop_ids($raw_ids, $invalid); + if (!empty($invalid)) { + $message = 'Invalid Workshop item entries. Use a numeric Workshop ID or a Steam Workshop URL: ' . implode(', ', $invalid); + return false; + } + if (empty($ids)) { + $message = 'Enter at least one Steam Workshop ID or Workshop URL.'; + return false; + } + $message = ''; + return true; +} + +function scm_validate_scripted_installer(array $payload, &$message = '') +{ + $script = isset($payload['post_script']) ? trim((string)$payload['post_script']) : ''; + if ($script === '') { + $message = 'Please enter the installer script/action.'; + return false; + } + $message = ''; + return true; +} + +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; +} + +function scm_validate_install_method_payload($install_method, array $payload, &$message = '') +{ + $install_method = scm_get_install_method_default($install_method); + if (!isset(scm_get_install_method_required_fields()[$install_method])) { + $message = 'Invalid install/content type selected.'; + 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); + } + $message = ''; + return true; +} + +function scm_build_workshop_runtime_context($db, array $home_info, $server_xml, array $payload, &$message = '') +{ + if (!scm_validate_workshop_content($payload, $message)) { + return false; + } + + $workshop_item_id = trim((string)(isset($payload['workshop_item_id']) ? $payload['workshop_item_id'] : '')); + $install_strategy = isset($payload['install_strategy']) ? trim((string)$payload['install_strategy']) : ''; + $workshop_app_id = scm_extract_workshop_app_id($server_xml); + $steam_app_id = scm_extract_workshop_steam_app_id($server_xml); + $folder_prefix = scm_workshop_mod_prefix($server_xml); + $folder_name = $folder_prefix . $workshop_item_id; + $xml_install_path = scm_extract_workshop_install_path($server_xml); + $effective_template = $xml_install_path !== '' ? $xml_install_path : scm_get_default_workshop_target_template($install_strategy); + $placeholder_map = scm_build_placeholder_map($home_info, array('exe_location' => isset($server_xml->exe_location) ? (string)$server_xml->exe_location : ''), array( + 'WORKSHOP_ID' => $workshop_item_id, + 'WORKSHOP_APP_ID' => $workshop_app_id, + 'STEAM_APP_ID' => $steam_app_id, + 'FOLDER_NAME' => $folder_name, + 'MOD_FOLDER' => $folder_name, + )); + $message = ''; + return array( + 'workshop_item_id' => $workshop_item_id, + 'workshop_app_id' => $workshop_app_id, + 'steam_app_id' => $steam_app_id, + 'folder_name' => $folder_name, + 'target_path_template' => $effective_template, + 'target_path_resolved' => scm_apply_placeholders($effective_template, $placeholder_map), + 'server_root' => rtrim((string)$home_info['home_path'], '/'), + 'steamcmd_path' => '', + 'workshop_download_dir' => '', + ); +} + +function scm_detect_workshop_install_strategy(array $home_info, $server_xml, array $template = array()) +{ + if (isset($server_xml->workshop_support->install_strategy)) { + $strategy = trim((string)$server_xml->workshop_support->install_strategy); + if ($strategy !== '' && preg_match('/^[a-z0-9_\-]+$/i', $strategy)) { + return strtolower($strategy); + } + } + $game_key = strtolower((string)(isset($home_info['game_key']) ? $home_info['game_key'] : '')); + $cfg_file = strtolower((string)(isset($home_info['home_cfg_file']) ? $home_info['home_cfg_file'] : '')); + $name = strtolower((string)(isset($home_info['game_name']) ? $home_info['game_name'] : '')); + $haystack = $game_key . ' ' . $cfg_file . ' ' . $name; + if (strpos($haystack, 'dayz') !== false) { + return 'dayz_mod_folder'; + } + if (strpos($haystack, 'arma') !== false) { + return 'arma_mod_folder'; + } + return 'copy_to_mod_folder'; +} + +function scm_workshop_should_copy_keys($server_xml, $install_strategy) +{ + if (isset($server_xml->workshop_support->copy_keys)) { + $attrs = $server_xml->workshop_support->copy_keys->attributes(); + if (isset($attrs['enabled'])) { + return scm_workshop_xml_bool((string)$attrs['enabled'], false); + } + } + return in_array((string)$install_strategy, array('dayz_mod_folder', 'arma_mod_folder'), true); +} + +function scm_workshop_keys_target_path($server_xml, array $home_info) +{ + $template = ''; + if (isset($server_xml->workshop_support->copy_keys->target_path)) { + $template = trim((string)$server_xml->workshop_support->copy_keys->target_path); + } + if ($template === '') { + $template = '{SERVER_ROOT}/keys'; + } + $map = scm_build_placeholder_map($home_info); + return scm_apply_placeholders($template, $map); +} + +function scm_workshop_post_install_action($server_xml) +{ + if (!isset($server_xml->workshop_support->post_install_action)) { + return ''; + } + $action = trim((string)$server_xml->workshop_support->post_install_action); + if ($action === '' || strpos($action, "\0") !== false || strpos($action, "\r") !== false || strpos($action, "\n") !== false) { + return ''; + } + return $action; +} + +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}' => '', + '{FOLDER_NAME}' => '', + '{MOD_FOLDER}' => '', + ); + 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_get_workshop_enabled_games($query = '', $tag = '') +{ + $games = array(); + $config_dir = defined('SERVER_CONFIG_LOCATION') ? SERVER_CONFIG_LOCATION : 'modules/config_games/server_configs/'; + $schema = defined('XML_SCHEMA') ? XML_SCHEMA : 'modules/config_games/schema_server_config.xml'; + $query = strtolower(trim((string)$query)); + $tag = strtolower(trim((string)$tag)); + foreach (glob(rtrim($config_dir, '/') . '/*.xml') ?: array() as $file) { + $xml = @simplexml_load_file($file); + if ($xml === false || !scm_workshop_is_supported($xml)) { + continue; + } + $game_key = isset($xml->game_key) ? (string)$xml->game_key : ''; + $game_name = isset($xml->game_name) ? (string)$xml->game_name : basename($file, '.xml'); + $app_id = scm_extract_workshop_app_id($xml); + $haystack = strtolower($game_key . ' ' . $game_name . ' ' . basename($file) . ' ' . $app_id); + if ($query !== '' && strpos($haystack, $query) === false) { + continue; + } + if ($tag !== '' && strpos($haystack, $tag) === false) { + continue; + } + $games[] = array( + 'game_key' => $game_key, + 'game_name' => $game_name, + 'config_file' => basename($file), + 'workshop_app_id' => $app_id, + 'steam_app_id' => scm_extract_workshop_steam_app_id($xml), + 'install_strategy' => isset($xml->workshop_support->install_strategy) ? (string)$xml->workshop_support->install_strategy : '', + 'schema' => $schema, + ); + } + usort($games, function ($a, $b) { + return strcasecmp($a['game_name'] . $a['config_file'], $b['game_name'] . $b['config_file']); + }); + return $games; +} + +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 + * installs that have not yet run the module updater are covered. + * + * @param object $db Panel DB handle + * @return bool + */ +function scm_ensure_phase2_schema($db) +{ + static $phase2_checked = false; + if ($phase2_checked) { + return true; + } + $phase2_checked = true; + $prefix = OGP_DB_PREFIX; + + // ── Extend addons table ─────────────────────────────────────────────────── + $new_columns = array( + 'install_method' => "VARCHAR(32) NOT NULL DEFAULT 'download_zip'", + 'content_version' => "VARCHAR(64) NULL", + 'requires_stop' => "TINYINT(1) NOT NULL DEFAULT 1", + 'backup_before_install' => "TINYINT(1) NOT NULL DEFAULT 1", + '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", + 'allow_user_workshop_ids' => "TINYINT(1) NOT NULL DEFAULT 1", + 'max_workshop_ids' => "INT NULL", + 'required_workshop_ids' => "TEXT NULL", + 'blocked_workshop_ids' => "TEXT NULL", + ); + 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)) { + $db->query("ALTER TABLE `{$prefix}addons` ADD COLUMN `{$col}` {$definition}"); + } + } + + // ── Per-server manifest ─────────────────────────────────────────────────── + $db->query( + "CREATE TABLE IF NOT EXISTS `{$prefix}server_content_manifest` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + `home_id` INT NOT NULL, + `addon_id` INT NOT NULL, + `install_method` VARCHAR(32) NOT NULL DEFAULT 'download_zip', + `content_version` VARCHAR(64) NULL, + `install_state` VARCHAR(32) NOT NULL DEFAULT 'installed', + `checksum_sha256` VARCHAR(64) NULL, + `source_url` VARCHAR(255) NULL, + `installed_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `installed_by` INT NULL, + `updated_at` DATETIME NULL, + `notes` TEXT NULL, + UNIQUE KEY `uniq_home_addon` (`home_id`, `addon_id`), + KEY `idx_home_id` (`home_id`), + KEY `idx_addon_id` (`addon_id`), + KEY `idx_install_state` (`install_state`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" + ); + + // ── Install history ─────────────────────────────────────────────────────── + $db->query( + "CREATE TABLE IF NOT EXISTS `{$prefix}server_content_install_history` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + `home_id` INT NOT NULL, + `addon_id` INT NOT NULL, + `installed_by` INT NULL, + `started_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `completed_at` DATETIME NULL, + `install_state` VARCHAR(32) NOT NULL DEFAULT 'started', + `install_method` VARCHAR(32) NULL, + `content_version` VARCHAR(64) NULL, + `source_url` VARCHAR(255) NULL, + `cache_mode_used` VARCHAR(32) NULL, + `result_code` INT NULL, + `log_output` MEDIUMTEXT NULL, + KEY `idx_home_id` (`home_id`), + KEY `idx_addon_id` (`addon_id`), + KEY `idx_started_at` (`started_at`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" + ); + + return true; +} + +/** + * Returns all manifest rows for a given server home. + * + * @param object $db + * @param int $home_id + * @return array + */ +function scm_get_manifest_rows($db, $home_id) +{ + $home_id = (int)$home_id; + if ($home_id <= 0 || !scm_ensure_phase2_schema($db)) { + return array(); + } + $rows = $db->resultQuery( + "SELECT m.*, a.name AS addon_name, a.addon_type, a.install_method AS addon_install_method + FROM `".OGP_DB_PREFIX."server_content_manifest` m + LEFT JOIN `".OGP_DB_PREFIX."addons` a ON a.addon_id = m.addon_id + WHERE m.home_id = {$home_id} + ORDER BY m.installed_at DESC" + ); + return is_array($rows) ? $rows : array(); +} + +/** + * Creates a new install history row and returns its insert ID. + * Returns 0 on failure. + * + * @param object $db + * @param int $home_id + * @param int $addon_id + * @param int $user_id + * @param string $source_url + * @param string $content_version + * @param string $install_method + * @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') +{ + $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); + $cache_mode_used = $db->realEscapeSingle((string)$cache_mode_used); + + if (!scm_ensure_phase2_schema($db)) { + return 0; + } + $id = $db->resultInsertId( + 'server_content_install_history', + array( + 'home_id' => $home_id, + 'addon_id' => $addon_id, + 'installed_by' => $user_id, + 'install_state' => 'started', + 'install_method' => $install_method, + 'content_version' => $content_version, + 'source_url' => $source_url, + 'cache_mode_used' => $cache_mode_used, + ) + ); + return is_numeric($id) ? (int)$id : 0; +} + +/** + * Updates an existing install history row with the final result. + * + * @param object $db + * @param int $history_id + * @param string $state 'installed' | 'failed' | 'cancelled' + * @param int $result_code Exit code (0 = success) + * @param string $log_output Script/download log snippet + * @return bool + */ +function scm_record_install_done($db, $history_id, $state = 'installed', $result_code = 0, $log_output = '') +{ + $history_id = (int)$history_id; + $state = $db->realEscapeSingle((string)$state); + $result_code = (int)$result_code; + $log_output = $db->realEscapeSingle((string)$log_output); + if ($history_id <= 0) { + return false; + } + return (bool)$db->query( + "UPDATE `".OGP_DB_PREFIX."server_content_install_history` + SET install_state = '{$state}', + result_code = {$result_code}, + log_output = '{$log_output}', + completed_at = NOW() + WHERE id = {$history_id}" + ); +} + +/** + * Inserts or updates a server_content_manifest row for a successful install. + * + * @param object $db + * @param int $home_id + * @param int $addon_id + * @param array $fields Optional overrides: install_method, content_version, + * install_state, source_url, checksum_sha256, installed_by + * @return bool + */ +function scm_upsert_manifest($db, $home_id, $addon_id, array $fields = array()) +{ + $home_id = (int)$home_id; + $addon_id = (int)$addon_id; + 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')); + $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'] : '')); + $checksum = $db->realEscapeSingle((string)(isset($fields['checksum_sha256']) ? $fields['checksum_sha256'] : '')); + $installed_by = isset($fields['installed_by']) ? (int)$fields['installed_by'] : 'NULL'; + if ($installed_by !== 'NULL' && $installed_by <= 0) { + $installed_by = 'NULL'; + } + + return (bool)$db->query( + "INSERT INTO `".OGP_DB_PREFIX."server_content_manifest` + (`home_id`,`addon_id`,`install_method`,`content_version`,`install_state`,`source_url`,`checksum_sha256`,`installed_by`,`installed_at`,`updated_at`) + VALUES + ({$home_id},{$addon_id},'{$install_method}','{$content_version}','{$install_state}','{$source_url}','{$checksum}',{$installed_by},NOW(),NOW()) + ON DUPLICATE KEY UPDATE + install_method = VALUES(install_method), + content_version = VALUES(content_version), + install_state = VALUES(install_state), + source_url = VALUES(source_url), + checksum_sha256 = VALUES(checksum_sha256), + installed_at = NOW(), + updated_at = NOW()" + ); +} diff --git a/Panel/modules/addonsmanager/workshop_action.php b/Panel/modules/addonsmanager/workshop_action.php index 292b9a62..261eafbe 100644 --- a/Panel/modules/addonsmanager/workshop_action.php +++ b/Panel/modules/addonsmanager/workshop_action.php @@ -1,487 +1,11 @@ logger("server_content_workshop home_id=".(int)$home_id." user_id=".(int)$user_id." ".$message); -} - -function scm_workshop_update_rows_state($db, $home_id, array $item_ids, $state, $error = null, $mark_install = false, $mark_update = false) -{ - if (empty($item_ids)) { - return true; - } - $escaped_ids = array(); - foreach ($item_ids as $item_id) { - $escaped_ids[] = "'" . $db->realEscapeSingle((string)$item_id) . "'"; - } - $set = array( - "install_state='" . $db->realEscapeSingle($state) . "'", - "updated_at=NOW()", - ); - if ($mark_install) { - $set[] = "last_installed_at=NOW()"; - } - if ($mark_update) { - $set[] = "last_updated_at=NOW()"; - } - if ($error === null) { - $set[] = "last_error=NULL"; - } else { - $set[] = "last_error='" . $db->realEscapeSingle($error) . "'"; - } - - $query = "UPDATE `".OGP_DB_PREFIX."server_content_workshop` - SET ".implode(", ", $set)." - WHERE home_id=".(int)$home_id." AND workshop_item_id IN (".implode(",", $escaped_ids).")"; - return (bool)$db->query($query); -} - -function scm_workshop_filter_existing_ids($db, $home_id, array $item_ids) -{ - if (empty($item_ids)) { - return array(); - } - $escaped_ids = array(); - foreach ($item_ids as $item_id) { - $escaped_ids[] = "'" . $db->realEscapeSingle((string)$item_id) . "'"; - } - $rows = $db->resultQuery( - "SELECT workshop_item_id FROM `".OGP_DB_PREFIX."server_content_workshop` - WHERE home_id=".(int)$home_id." AND workshop_item_id IN (".implode(",", $escaped_ids).")" - ); - $allowed = array(); - if (is_array($rows)) { - foreach ((array)$rows as $row) { - $allowed[(string)$row['workshop_item_id']] = (string)$row['workshop_item_id']; - } - } - return array_values($allowed); -} - -function scm_workshop_get_content_template($db, $addon_id) -{ - $addon_id = (int)$addon_id; - if ($addon_id <= 0) { - return array(); - } - scm_ensure_phase2_schema($db); - $rows = $db->resultQuery( - "SELECT addon_id, name, content_version, description - FROM `" . OGP_DB_PREFIX . "addons` - WHERE addon_id=" . $addon_id . " AND install_method='steam_workshop' - LIMIT 1" - ); - return (is_array($rows) && !empty($rows)) ? $rows[0] : array(); -} - -function scm_workshop_build_manifest_context($db, array $home_info, $server_xml, array $item_ids, array $template = array()) -{ - $install_strategy = scm_detect_workshop_install_strategy($home_info, $server_xml, $template); - $copy_keys = scm_workshop_should_copy_keys($server_xml, $install_strategy); - $xml_install_path = scm_extract_workshop_install_path($server_xml); - $keys_target_path = scm_workshop_keys_target_path($server_xml, $home_info); - $item_details = array(); - $resolved_app_id = ''; - $steam_app_id = ''; - - foreach ($item_ids as $item_id) { - $payload = array( - 'workshop_item_id' => (string)$item_id, - 'install_strategy' => $install_strategy, - ); - $message = ''; - $runtime = scm_build_workshop_runtime_context($db, $home_info, $server_xml, $payload, $message); - if ($runtime === false) { - $runtime = array(); - } - $item_app_id = isset($runtime['workshop_app_id']) ? (string)$runtime['workshop_app_id'] : ''; - if ($resolved_app_id === '' && $item_app_id !== '') { - $resolved_app_id = $item_app_id; - } - if ($steam_app_id === '' && !empty($runtime['steam_app_id'])) { - $steam_app_id = (string)$runtime['steam_app_id']; - } - $item_details[(string)$item_id] = array( - 'workshop_item_id' => (string)$item_id, - 'title' => '', - 'folder_name' => isset($runtime['folder_name']) && $runtime['folder_name'] !== '' ? (string)$runtime['folder_name'] : '@' . $item_id, - 'target_path_template' => isset($runtime['target_path_template']) ? (string)$runtime['target_path_template'] : scm_get_default_workshop_target_template($install_strategy), - 'target_path_resolved' => isset($runtime['target_path_resolved']) ? (string)$runtime['target_path_resolved'] : '', - 'install_strategy' => $install_strategy, - 'copy_keys' => $copy_keys ? 1 : 0, - 'keys_target_path' => $keys_target_path, - ); - } - - if ($resolved_app_id === '') { - $resolved_app_id = scm_extract_workshop_app_id($server_xml); - } - if ($steam_app_id === '') { - $steam_app_id = scm_extract_workshop_steam_app_id($server_xml); - } - - return array( - 'workshop_app_id' => $resolved_app_id, - 'steam_app_id' => $steam_app_id, - 'server_root' => rtrim((string)$home_info['home_path'], '/'), - 'install_strategy' => $install_strategy, - 'copy_keys' => $copy_keys ? 1 : 0, - 'target_path_template' => $xml_install_path !== '' ? $xml_install_path : scm_get_default_workshop_target_template($install_strategy), - 'keys_target_path' => $keys_target_path, - 'post_install_script' => scm_workshop_post_install_action($server_xml), - 'launch_param_additions' => isset($server_xml->workshop_support->startup_param_format) ? trim((string)$server_xml->workshop_support->startup_param_format) : '', - 'content_template_id' => isset($template['addon_id']) ? (int)$template['addon_id'] : 0, - 'content_template_name' => isset($template['name']) ? (string)$template['name'] : '', - 'item_details' => $item_details, - ); -} - -function scm_workshop_write_manifest_and_run($db, array $home_info, $server_xml, $action, array $item_ids, &$error = '', array $extra_manifest = array(), &$result_details = array()) -{ - $error = ''; - $result_details = array(); - if (empty($item_ids)) { - $error = 'No Workshop IDs were selected for this action.'; - return false; - } - - $manifest_path = scm_get_workshop_manifest_path($home_info); - if ($manifest_path === false) { - $error = 'Manifest path validation failed for this server home.'; - return false; - } - - $home_path = rtrim(clean_path((string)$home_info['home_path']), '/'); - if (!scm_path_is_under_home($home_path, $manifest_path)) { - $error = 'Manifest path is outside of the server home.'; - return false; - } - - $manifest_dir = dirname($manifest_path); - $workshop_app_id = !empty($extra_manifest['workshop_app_id']) ? (string)$extra_manifest['workshop_app_id'] : scm_extract_workshop_app_id($server_xml); - $steam_app_id = !empty($extra_manifest['steam_app_id']) ? (string)$extra_manifest['steam_app_id'] : scm_extract_workshop_steam_app_id($server_xml); - if ($workshop_app_id === '' && $steam_app_id !== '') { - $workshop_app_id = $steam_app_id; - } - if ($steam_app_id === '' && $workshop_app_id !== '') { - $steam_app_id = $workshop_app_id; - } - if ($workshop_app_id === '' || !preg_match('/^[0-9]+$/', $workshop_app_id)) { - $error = 'Workshop App ID is missing from the game XML workshop_support block.'; - return false; - } - $manifest = array( - 'manifest_version' => 1, - 'action' => (string)$action, - 'home_id' => (int)$home_info['home_id'], - 'home_cfg_id' => (int)$home_info['home_cfg_id'], - 'game_path' => $home_path, - 'server_path' => $home_path, - 'workshop_app_id' => $workshop_app_id, - 'steam_app_id' => $steam_app_id, - 'items' => array_values($item_ids), - 'item_details' => !empty($extra_manifest['item_details']) && is_array($extra_manifest['item_details']) ? $extra_manifest['item_details'] : array(), - 'install_strategy' => !empty($extra_manifest['install_strategy']) ? (string)$extra_manifest['install_strategy'] : '', - 'target_path' => !empty($extra_manifest['target_path_template']) ? (string)$extra_manifest['target_path_template'] : scm_get_default_workshop_target_template(!empty($extra_manifest['install_strategy']) ? (string)$extra_manifest['install_strategy'] : ''), - 'generated_at' => date('Y-m-d H:i:s'), - ); - if (!empty($extra_manifest)) { - $manifest['extra'] = $extra_manifest; - } - $manifest_json = json_encode($manifest); - if ($manifest_json === false) { - $error = 'Failed to encode workshop manifest JSON.'; - return false; - } - - $remote = new OGPRemoteLibrary( - $home_info['agent_ip'], - $home_info['agent_port'], - $home_info['encryption_key'], - $home_info['timeout'] - ); - - $remote->exec("mkdir -p " . escapeshellarg($manifest_dir)); - if ((int)$remote->remote_writefile($manifest_path, $manifest_json) !== 1) { - $error = 'Failed to write workshop manifest to remote server.'; - return false; - } - $script_path = scm_prepare_workshop_script_for_agent($remote, $home_info, $server_xml, $error); - if ($script_path === false) { - return false; - } - - $command = "bash " . escapeshellarg($script_path) . " " . escapeshellarg($manifest_path) . " ; echo __GSP_WORKSHOP_EXIT:$?"; - $output = $remote->exec($command); - if (!is_string($output) || $output === '') { - $error = 'Workshop script did not return an execution status.'; - return false; - } - if (!preg_match('/__GSP_WORKSHOP_EXIT:(\d+)/', $output, $matches)) { - $error = 'Workshop script exit marker not found in output.'; - return false; - } - $result_details = array( - 'manifest_path' => $manifest_path, - 'script_path' => $script_path, - 'log_path' => clean_path($manifest_dir . (scm_is_windows_home($home_info) ? '/workshop_install_windows.log' : '/workshop_install.log')), - 'output' => trim(preg_replace('/__GSP_WORKSHOP_EXIT:\d+/', '', $output)), - ); - $exit_code = (int)$matches[1]; - if ($exit_code !== 0) { - $error = 'Workshop script failed (exit '.$exit_code.'): '.trim($output); - return false; - } - return true; -} - -function scm_workshop_handle_action($db, array $home_info, $user_id, $action, $raw_ids, array $selected_ids, &$message, &$is_error, $addon_id = 0, array $options = array()) -{ - $message = ''; - $is_error = true; - if (!scm_ensure_workshop_schema($db)) { - $message = 'Workshop schema migration failed.'; - return false; - } - scm_ensure_phase2_schema($db); - - $home_id = (int)$home_info['home_id']; - $user_id = (int)$user_id; - $addon_id = (int)$addon_id; - $server_xml = read_server_config(SERVER_CONFIG_LOCATION . "/" . $home_info['home_cfg_file']); - if ($server_xml === false) { - $message = 'Unable to read server configuration for workshop action.'; - return false; - } - if (!scm_workshop_is_supported($server_xml)) { - $message = 'This game XML does not enable Steam Workshop support. Add a valid workshop_support block before installing Workshop items.'; - return false; - } - - $template = scm_workshop_get_content_template($db, $addon_id); - - if ($action === 'install_new') { - $invalid = array(); - $item_ids = scm_parse_workshop_ids($raw_ids, $invalid); - if (!empty($invalid)) { - $message = 'Invalid Workshop item entries. Use a numeric Workshop ID or Steam Workshop URL: ' . implode(', ', $invalid); - return false; - } - if (empty($item_ids)) { - $message = 'Enter at least one Steam Workshop ID or Workshop URL.'; - return false; - } - - $manifest_context = scm_workshop_build_manifest_context($db, $home_info, $server_xml, $item_ids, $template); - $resolved_app_id = isset($manifest_context['workshop_app_id']) ? (string)$manifest_context['workshop_app_id'] : ''; - if ($resolved_app_id === '') { - $message = 'Workshop App ID is missing from the game XML workshop_support block.'; - return false; - } - - // Check whether the content_id column exists (added in db_version 6). - $has_content_id_col = (bool)$db->resultQuery( - "SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE TABLE_SCHEMA = DATABASE() - AND TABLE_NAME = '" . $db->realEscapeSingle(OGP_DB_PREFIX . 'server_content_workshop') . "' - AND COLUMN_NAME = 'content_id'" - ); - - $next_order_rows = $db->resultQuery( - "SELECT COALESCE(MAX(load_order), 0) AS max_order FROM `".OGP_DB_PREFIX."server_content_workshop` - WHERE home_id=".$home_id - ); - $next_order = (is_array($next_order_rows) && isset($next_order_rows[0]['max_order'])) ? (int)$next_order_rows[0]['max_order'] : 0; - foreach ($item_ids as $item_id) { - $item_detail = isset($manifest_context['item_details'][(string)$item_id]) ? $manifest_context['item_details'][(string)$item_id] : array(); - $install_path = isset($item_detail['target_path_resolved']) ? (string)$item_detail['target_path_resolved'] : ''; - $install_strategy = isset($item_detail['install_strategy']) ? (string)$item_detail['install_strategy'] : (string)$manifest_context['install_strategy']; - $next_order++; - $content_id_col = $has_content_id_col && $addon_id > 0 ? ", content_id" : ''; - $content_id_val = $has_content_id_col && $addon_id > 0 ? ", " . $addon_id : ''; - $content_id_upd = $has_content_id_col && $addon_id > 0 ? ", content_id=VALUES(content_id)" : ''; - $query = "INSERT INTO `".OGP_DB_PREFIX."server_content_workshop` - (home_id, home_cfg_id, remote_server_id, workshop_app_id, workshop_item_id, install_path, install_strategy, enabled, load_order, install_state, created_by, created_at, updated_at" . $content_id_col . ") - VALUES ( - ".$home_id.", - ".(int)$home_info['home_cfg_id'].", - ".(int)$home_info['remote_server_id'].", - '".$db->realEscapeSingle($resolved_app_id)."', - '".$db->realEscapeSingle($item_id)."', - '".$db->realEscapeSingle($install_path)."', - '".$db->realEscapeSingle($install_strategy)."', - 1, - ".$next_order.", - 'queued', - ".$user_id.", - NOW(), - NOW() - " . $content_id_val . " - ) - ON DUPLICATE KEY UPDATE - home_cfg_id=VALUES(home_cfg_id), - remote_server_id=VALUES(remote_server_id), - workshop_app_id=VALUES(workshop_app_id), - install_path=VALUES(install_path), - install_strategy=VALUES(install_strategy), - enabled=1, - install_state='queued', - last_error=NULL, - updated_at=NOW()" . $content_id_upd; - $db->query($query); - } - - scm_workshop_update_rows_state($db, $home_id, $item_ids, 'installing', null, false, false); - $error = ''; - $details = array(); - $ok = scm_workshop_write_manifest_and_run($db, $home_info, $server_xml, 'install', $item_ids, $error, $manifest_context, $details); - if ($ok) { - scm_workshop_update_rows_state($db, $home_id, $item_ids, 'installed', null, true, true); - scm_workshop_record_catalog_items($db, $resolved_app_id, $item_ids, $home_info, isset($manifest_context['item_details']) ? $manifest_context['item_details'] : array(), true); - scm_workshop_log_action($db, $home_id, $user_id, "install_new ids=".implode(',', $item_ids)." addon_id=".$addon_id." status=success"); - $is_error = false; - $message = 'Workshop item(s) installed successfully. Manifest: '.scm_h(isset($details['manifest_path']) ? $details['manifest_path'] : '').' Log: '.scm_h(isset($details['log_path']) ? $details['log_path'] : ''); - return true; - } - scm_workshop_update_rows_state($db, $home_id, $item_ids, 'failed', $error, false, false); - scm_workshop_log_action($db, $home_id, $user_id, "install_new ids=".implode(',', $item_ids)." addon_id=".$addon_id." status=failed error=".$error); - $message = $error; - return false; - } - - if ($action === 'enable_selected' || $action === 'disable_selected' || $action === 'save_update_policy') { - $item_ids = scm_workshop_filter_existing_ids($db, $home_id, scm_parse_selected_workshop_ids($selected_ids)); - if (empty($item_ids)) { - $message = 'Select one or more saved Workshop IDs.'; - return false; - } - if ($action === 'save_update_policy') { - $policy = isset($options['update_policy']) ? (string)$options['update_policy'] : 'manual'; - if (!scm_workshop_set_update_policy($db, $home_id, $item_ids, $policy)) { - $message = 'Failed to save Workshop update policy.'; - return false; - } - $is_error = false; - $message = 'Workshop update policy saved for selected item(s).'; - return true; - } - $enabled = ($action === 'enable_selected') ? 1 : 0; - if (!scm_workshop_set_enabled($db, $home_id, $item_ids, $enabled)) { - $message = 'Failed to update Workshop enabled state.'; - return false; - } - $is_error = false; - $message = $enabled ? 'Selected Workshop item(s) enabled.' : 'Selected Workshop item(s) disabled.'; - return true; - } - - - if ($action === 'remove_selected') { - $item_ids = scm_workshop_filter_existing_ids($db, $home_id, scm_parse_selected_workshop_ids($selected_ids)); - if (empty($item_ids)) { - $message = 'Select one or more saved Workshop IDs to remove.'; - return false; - } - $escaped_ids = array(); - foreach ($item_ids as $item_id) { - $escaped_ids[] = "'" . $db->realEscapeSingle((string)$item_id) . "'"; - } - $db->query( - "DELETE FROM `" . OGP_DB_PREFIX . "server_content_workshop` - WHERE home_id=" . (int)$home_id . " - AND workshop_item_id IN (" . implode(",", $escaped_ids) . ")" - ); - scm_workshop_log_action($db, $home_id, $user_id, "remove_selected ids=" . implode(',', $item_ids) . " status=db_removed"); - $is_error = false; - $message = 'Selected Workshop item(s) removed from this server list. Installed files, if any, can be cleaned up separately.'; - return true; - } - - if ($action === 'update_selected' || $action === 'download_selected') { - $item_ids = scm_workshop_filter_existing_ids($db, $home_id, scm_parse_selected_workshop_ids($selected_ids)); - if (empty($item_ids)) { - $message = 'Select one or more saved Workshop IDs.'; - return false; - } - $target_action = ($action === 'remove_selected') ? 'remove' : (($action === 'download_selected') ? 'download_only' : 'update'); - $manifest_context = scm_workshop_build_manifest_context($db, $home_info, $server_xml, $item_ids, $template); - scm_workshop_update_rows_state($db, $home_id, $item_ids, 'installing', null, false, false); - $error = ''; - $details = array(); - $ok = scm_workshop_write_manifest_and_run($db, $home_info, $server_xml, $target_action, $item_ids, $error, $manifest_context, $details); - if ($ok) { - if ($target_action === 'remove') { - scm_workshop_update_rows_state($db, $home_id, $item_ids, 'removed', null, false, true); - } elseif ($target_action === 'download_only') { - scm_workshop_update_rows_state($db, $home_id, $item_ids, 'downloaded', null, false, true); - scm_workshop_set_update_policy($db, $home_id, $item_ids, 'install_on_restart'); - scm_workshop_record_catalog_items($db, isset($manifest_context['workshop_app_id']) ? (string)$manifest_context['workshop_app_id'] : '', $item_ids, $home_info, isset($manifest_context['item_details']) ? $manifest_context['item_details'] : array(), true); - } else { - scm_workshop_update_rows_state($db, $home_id, $item_ids, 'installed', null, false, true); - scm_workshop_record_catalog_items($db, isset($manifest_context['workshop_app_id']) ? (string)$manifest_context['workshop_app_id'] : '', $item_ids, $home_info, isset($manifest_context['item_details']) ? $manifest_context['item_details'] : array(), true); - } - scm_workshop_log_action($db, $home_id, $user_id, $action." ids=".implode(',', $item_ids)." addon_id=".$addon_id." status=success"); - $is_error = false; - if ($target_action === 'remove') { - $message = 'Selected Workshop item(s) removed.'; - } elseif ($target_action === 'download_only') { - $message = 'Selected Workshop item(s) downloaded and marked for install on next restart.'; - } else { - $message = 'Selected Workshop item(s) updated successfully.'; - } - $message .= ' Log: ' . scm_h(isset($details['log_path']) ? $details['log_path'] : ''); - return true; - } - scm_workshop_update_rows_state($db, $home_id, $item_ids, 'failed', $error, false, false); - scm_workshop_log_action($db, $home_id, $user_id, $action." ids=".implode(',', $item_ids)." addon_id=".$addon_id." status=failed error=".$error); - $message = $error; - return false; - } - - if ($action === 'update_all') { - $rows = $db->resultQuery( - "SELECT workshop_item_id FROM `".OGP_DB_PREFIX."server_content_workshop` - WHERE home_id=".$home_id." AND install_state<>'removed'" - ); - $item_ids = array(); - if (is_array($rows)) { - foreach ((array)$rows as $row) { - $item_ids[] = (string)$row['workshop_item_id']; - } - } - $item_ids = scm_parse_selected_workshop_ids($item_ids); - if (empty($item_ids)) { - $message = 'No Workshop IDs are currently saved for this server.'; - return false; - } - $manifest_context = scm_workshop_build_manifest_context($db, $home_info, $server_xml, $item_ids, $template); - scm_workshop_update_rows_state($db, $home_id, $item_ids, 'installing', null, false, false); - $error = ''; - $details = array(); - $ok = scm_workshop_write_manifest_and_run($db, $home_info, $server_xml, 'update', $item_ids, $error, $manifest_context, $details); - if ($ok) { - scm_workshop_update_rows_state($db, $home_id, $item_ids, 'installed', null, false, true); - scm_workshop_record_catalog_items($db, isset($manifest_context['workshop_app_id']) ? (string)$manifest_context['workshop_app_id'] : '', $item_ids, $home_info, isset($manifest_context['item_details']) ? $manifest_context['item_details'] : array(), true); - scm_workshop_log_action($db, $home_id, $user_id, "update_all ids=".implode(',', $item_ids)." addon_id=".$addon_id." status=success"); - $is_error = false; - $message = 'All saved Workshop item(s) updated successfully. Log: ' . scm_h(isset($details['log_path']) ? $details['log_path'] : ''); - return true; - } - scm_workshop_update_rows_state($db, $home_id, $item_ids, 'failed', $error, false, false); - scm_workshop_log_action($db, $home_id, $user_id, "update_all ids=".implode(',', $item_ids)." addon_id=".$addon_id." status=failed error=".$error); - $message = $error; - return false; - } - - $message = 'Invalid workshop action.'; - return false; + return 'Steam Workshop has been removed from Server Content. Use the Steam Workshop module instead.'; } +?> diff --git a/Panel/modules/addonsmanager/workshop_content.php b/Panel/modules/addonsmanager/workshop_content.php index ed8cad53..8ab9c7d8 100644 --- a/Panel/modules/addonsmanager/workshop_content.php +++ b/Panel/modules/addonsmanager/workshop_content.php @@ -1,308 +1,16 @@ '.htmlspecialchars($msg, ENT_QUOTES, 'UTF-8').''; } - - $home_info = scm_get_home_for_user($db, $home_id, $user_id); - if ($home_info === false) { - print_failure(get_lang('no_rights')); - echo create_back_button("addonsmanager","user_addons"); - return; - } - - if (!scm_ensure_workshop_schema($db)) { - print_failure('Failed to initialize Workshop Content storage.'); - return; - } - scm_ensure_phase2_schema($db); - $server_xml = read_server_config(SERVER_CONFIG_LOCATION . "/" . $home_info['home_cfg_file']); - if ($server_xml === false || !scm_workshop_is_supported($server_xml)) { - print_failure('Steam Workshop is not enabled for this game XML. Add a valid workshop_support block before using Workshop management.'); - echo create_back_button("addonsmanager","user_addons"); - return; - } - - // Template records are optional and used only as labels/history anchors. - $addon_template = null; - if ($addon_id > 0) { - $template_rows = $db->resultQuery( - "SELECT addon_id, name, description - FROM `" . OGP_DB_PREFIX . "addons` - WHERE addon_id=" . $addon_id . " AND install_method='steam_workshop'" - ); - if (is_array($template_rows) && !empty($template_rows)) { - $addon_template = $template_rows[0]; - } - } - - $message = ''; - $is_error = false; - $entered_ids = ''; - $catalog_sort = isset($_REQUEST['catalog_sort']) ? (string)$_REQUEST['catalog_sort'] : 'last_installed'; - $catalog_query = isset($_REQUEST['workshop_search']) ? trim((string)$_REQUEST['workshop_search']) : ''; - $catalog_tag = isset($_REQUEST['workshop_tag']) ? trim((string)$_REQUEST['workshop_tag']) : ''; - - if ($_SERVER['REQUEST_METHOD'] === 'POST') { - $posted_home_id = isset($_POST['home_id']) ? (int)$_POST['home_id'] : 0; - $csrf_token = isset($_POST['workshop_csrf']) ? (string)$_POST['workshop_csrf'] : ''; - $entered_ids = isset($_POST['workshop_ids']) ? (string)$_POST['workshop_ids'] : ''; - $selected_ids = isset($_POST['selected_ids']) ? $_POST['selected_ids'] : array(); - $action = isset($_POST['workshop_action']) ? (string)$_POST['workshop_action'] : ''; - $posted_addon_id = isset($_POST['addon_id']) ? (int)$_POST['addon_id'] : 0; - $options = array( - 'update_policy' => isset($_POST['update_policy']) ? (string)$_POST['update_policy'] : 'manual', - ); - - if ($posted_home_id !== $home_id) { - $is_error = true; - $message = 'Invalid server context for workshop action.'; - } - elseif (!scm_validate_csrf_token($csrf_token)) { - $is_error = true; - $message = 'Invalid CSRF token for workshop action.'; - } - else { - scm_workshop_handle_action($db, $home_info, $user_id, $action, $entered_ids, (array)$selected_ids, $message, $is_error, $posted_addon_id > 0 ? $posted_addon_id : $addon_id, $options); - } - } - - $catalog_app_id = ($server_xml !== false) ? scm_extract_workshop_app_id($server_xml) : ''; - $steam_app_id = ($server_xml !== false) ? scm_extract_workshop_steam_app_id($server_xml) : ''; - $install_strategy = ($server_xml !== false) ? scm_detect_workshop_install_strategy($home_info, $server_xml) : ''; - $rows = scm_get_workshop_rows($db, $home_id); - $catalog_rows = scm_get_workshop_catalog_rows($db, $catalog_app_id, $catalog_sort, 50, $catalog_query, $catalog_tag); - $game_search_rows = scm_get_workshop_enabled_games($catalog_query, $catalog_tag); - $csrf_token = scm_get_csrf_token(); - $base_query = 'm=addonsmanager&p=workshop_content&home_id=' . (int)$home_id . - '&mod_id=' . (int)$mod_id . '&ip=' . urlencode($ip) . '&port=' . urlencode($port) . - '&addon_id=' . (int)$addon_id . - '&workshop_search=' . urlencode($catalog_query) . '&workshop_tag=' . urlencode($catalog_tag); - - echo "

Workshop Mods: " . scm_h($home_info['home_name']) . "

"; - if ($addon_template !== null) { - echo "

Content template: " . scm_h($addon_template['name']) . ""; - if (!empty($addon_template['description'])) { - echo " – " . scm_h($addon_template['description']); - } - echo "

"; - } - echo "

Enter a Steam Workshop URL or numeric item ID. GSP stores only the numeric Workshop ID. App IDs, install paths, mod folder strategy, key-copy behavior, and launch parameter format come from this game's XML.

"; - - if ($message !== '') { - if ($is_error) { - print_failure($message); - } else { - print_success($message); - } - } - ?> - - - - - - -
Server Name:
Game Name:
Workshop App ID:
Steam App ID:
Install Strategy:
- -

Search Workshop

-
- - - - - - - - - - - - - - - - - - - - - -
Keyword / ID / URLTag
- '>Open matching Steam Workshop search -
-
- - -

Workshop Search Results

-

- GSP searched the local catalog for this selected game's Workshop App ID. - If nothing appears below, use the Steam Workshop search link above, then paste the Workshop URL or numeric ID into the install box. -

- - -
- - - - - - - - - - - - - - - -
Workshop URLs / IDs - -
Enter one or more Steam Workshop URLs or numeric IDs, one per line, comma-separated, or space-separated.
Example for Arma 3 CBA_A3: https://steamcommunity.com/sharedfiles/filedetails/?id=450814997
-
- -
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Workshop IDTitleEnabledOrderUpdate PolicyStateInstall PathLast InstalledLast UpdatedLast Error
No Workshop items saved for this server yet.
'>
-
- - - - - - - - - - - -
- -
-
- -

Known Workshop Items

-

These are Workshop items previously installed through Server Content Manager. The catalog grows automatically from real installs. Metadata is optional; direct ID or URL install remains available even when Steam metadata has not been fetched yet.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Workshop IDNameAuthorThumbnailInstall CountPublishedLast UpdatedLast InstalledSort IDAction
No known Workshop items have been installed for this app yet.
- - ' alt='' style='max-width:72px;max-height:48px;' /> - - -
- - - - - - - - - ' /> - -
-
- -
- - - - - - - -
- Steam Workshop'; +print_failure('Steam Workshop has been removed from Server Content. Use the Steam Workshop module instead.'); +echo '

The working legacy Steam Workshop module handles Workshop installs. Server Content will continue to manage normal downloadable content/addons only.

'; +?>