From 8ab665ce3cfdb670970c5b2e20c517e4d6bf7d3c Mon Sep 17 00:00:00 2001 From: Frank Harris Date: Fri, 12 Jun 2026 06:44:57 -0500 Subject: [PATCH] steam fix copy --- Panel/lang/English/modules/steam_workshop.php | 6 +- Panel/modules/steam_workshop/functions.php | 81 ++++++++++--- .../modules/steam_workshop/workshop_admin.php | 107 ++++++++++-------- docs/features/WORKSHOP_SYSTEM.md | 1 + docs/modules/steam_workshop.md | 4 +- 5 files changed, 129 insertions(+), 70 deletions(-) diff --git a/Panel/lang/English/modules/steam_workshop.php b/Panel/lang/English/modules/steam_workshop.php index 335a7f09..4a5762bd 100644 --- a/Panel/lang/English/modules/steam_workshop.php +++ b/Panel/lang/English/modules/steam_workshop.php @@ -42,8 +42,8 @@ define('LANG_open_workshop_page', "Open Workshop page"); define('LANG_unable_to_contact_steam_workshop', "Unable to contact the Steam Workshop."); define('LANG_unable_to_parse_steam_workshop_response', "Unable to parse the Steam Workshop response."); define('LANG_failed_to_fetch_workshop_item_details', "Failed to fetch Workshop item details for %s."); -define('LANG_copy_from_existing_workshop_config', "Copy From Existing Workshop Config"); -define('LANG_copy_from_existing_workshop_config_info', "Copy an existing Workshop XML into the currently selected game and OS configuration."); +define('LANG_copy_from_existing_workshop_config', "Copy Settings From Existing Workshop Config"); +define('LANG_copy_from_existing_workshop_config_info', "Copies Workshop configuration fields only. This does not copy installed mods or game server files."); define('LANG_select_config_to_copy', "Select config to copy"); define('LANG_copy_selected_config', "Copy Selected Config"); define('LANG_confirm_copy_workshop_config', "Confirm Workshop config copy"); @@ -51,7 +51,7 @@ define('LANG_workshop_config_copy_confirm_info', "Copy %s into %s and overwrite define('LANG_invalid_workshop_config_copy_source', "Invalid Workshop config source."); define('LANG_cannot_copy_workshop_config_to_itself', "Cannot copy a Workshop config onto itself."); define('LANG_failed_to_copy_workshop_config', "Failed to copy the selected Workshop config."); -define('LANG_workshop_config_copied', "Copied Workshop config %s to %s."); +define('LANG_workshop_config_copied', "Copied Workshop configuration settings from %s to %s."); define('LANG_select_game', "Select Game"); define('LANG_save_config', "Save Config"); define('LANG_mod_key_not_found_from_xml', "Mod key %s not found from xml."); diff --git a/Panel/modules/steam_workshop/functions.php b/Panel/modules/steam_workshop/functions.php index 0fdd9afc..1c13ed66 100644 --- a/Panel/modules/steam_workshop/functions.php +++ b/Panel/modules/steam_workshop/functions.php @@ -102,46 +102,93 @@ function steam_workshop_list_existing_configs() return $configs; } -function steam_workshop_get_existing_config_options($exclude_filename = '') +function steam_workshop_get_config_display_label($filename, $config, $friendly_names = array()) +{ + $label = isset($friendly_names[$filename]) && trim((string)$friendly_names[$filename]) !== '' ? (string)$friendly_names[$filename].' - '.$filename : $filename; + $meta = array(); + if(isset($config['workshop_id']) && $config['workshop_id'] !== '') + $meta[] = 'Workshop '.$config['workshop_id']; + if(isset($config['download_method']) && $config['download_method'] !== '') + $meta[] = $config['download_method']; + if(isset($config['mods_path']) && $config['mods_path'] !== '') + $meta[] = 'mods_path='.$config['mods_path']; + if(!empty($meta)) + $label .= ' ['.implode(' | ', $meta).']'; + return $label; +} + +function steam_workshop_get_existing_config_options($exclude_filename = '', $friendly_names = array()) { $options = array('' => get_lang('select_config_to_copy')); foreach(steam_workshop_list_existing_configs() as $basename => $config) { if($basename === $exclude_filename) continue; - $label = $basename; - $meta = array(); - if($config['workshop_id'] !== '') - $meta[] = 'Workshop '.$config['workshop_id']; - if($config['download_method'] !== '') - $meta[] = $config['download_method']; - if($config['mods_path'] !== '') - $meta[] = 'mods_path='.$config['mods_path']; - if(!empty($meta)) - $label .= ' ['.implode(' | ', $meta).']'; - $options[$basename] = $label; + $options[$basename] = steam_workshop_get_config_display_label($basename, $config, $friendly_names); } return $options; } +function steam_workshop_is_safe_config_filename($filename) +{ + $filename = (string)$filename; + if($filename === '') + return false; + if(strpos($filename, '/') !== false || strpos($filename, '\\') !== false || strpos($filename, '..') !== false) + return false; + if(pathinfo($filename, PATHINFO_EXTENSION) !== 'xml') + return false; + return preg_match('/^[A-Za-z0-9._-]+\.xml$/', $filename) === 1; +} + function steam_workshop_get_safe_config_copy_paths($selected_source, $destination_file) { - $selected_source = basename((string)$selected_source); - $destination_file = basename((string)$destination_file); $config_dir = realpath(steam_workshop_get_configs_dir()); - if($config_dir === false || $selected_source === '' || $destination_file === '') + $selected_source = (string)$selected_source; + $destination_file = (string)$destination_file; + if($config_dir === false || !steam_workshop_is_safe_config_filename($selected_source) || !steam_workshop_is_safe_config_filename($destination_file)) return array(false, false); $source_path = realpath($config_dir.'/'.$selected_source); $destination_path = $config_dir.'/'.$destination_file; if($source_path === false || strpos($source_path, $config_dir.DIRECTORY_SEPARATOR) !== 0) return array(false, false); - if(pathinfo($source_path, PATHINFO_EXTENSION) !== 'xml' || pathinfo($destination_path, PATHINFO_EXTENSION) !== 'xml') - return array(false, false); + if(file_exists($destination_path)) + { + $destination_realpath = realpath($destination_path); + if($destination_realpath === false || strpos($destination_realpath, $config_dir.DIRECTORY_SEPARATOR) !== 0) + return array(false, false); + $destination_path = $destination_realpath; + } return array($source_path, $destination_path); } +function steam_workshop_copy_config_settings_only($source_path, $destination_path) +{ + $source = new DOMDocument(); + $source->preserveWhiteSpace = false; + if(!@$source->load($source_path) || !$source->documentElement) + return false; + + $destination = new DOMDocument('1.0', 'UTF-8'); + $destination->formatOutput = true; + $root = $destination->createElement('workshop_settings'); + $destination->appendChild($root); + + foreach($source->documentElement->childNodes as $child) + { + if($child->nodeType !== XML_ELEMENT_NODE) + continue; + if($child->nodeName === 'mods') + continue; + $root->appendChild($destination->importNode($child, true)); + } + + $root->appendChild($destination->createElement('mods')); + return $destination->save($destination_path) !== false; +} + function steam_workshop_request($url, $context, &$error = "") { $response = @file_get_contents($url, false, $context); diff --git a/Panel/modules/steam_workshop/workshop_admin.php b/Panel/modules/steam_workshop/workshop_admin.php index 54bb47a0..36f7ca47 100644 --- a/Panel/modules/steam_workshop/workshop_admin.php +++ b/Panel/modules/steam_workshop/workshop_admin.php @@ -93,12 +93,57 @@ function steam_workshop_admin_hidden_state($values) return $html; } +function steam_workshop_admin_get_game_options($db, &$workshop_config_labels) +{ + $games = array(0 => get_lang('select_game')); + $workshop_config_labels = array(); + $game_cfgs = $db->getGameCfgs(); + foreach($game_cfgs as $game_cfg) + { + $server_xml = read_server_config(SERVER_CONFIG_LOCATION."/".$game_cfg['home_cfg_file']); + if(isset($server_xml->installer) and $server_xml->installer == "steamcmd") + { + $cfgMods = $db->getCfgMods($game_cfg['home_cfg_id']); + foreach($cfgMods as $cfgMod) + { + $mod_xml = xml_get_mod($server_xml, $cfgMod['mod_key']); + if(isset($mod_xml->installer_name) and !in_array((string)$mod_xml->installer_name, get_blacklist())) + { + preg_match('/(linux|win)(32|64)?/i', $game_cfg['game_key'], $matches); + if(empty($matches[1])) + continue; + + if(strtolower($matches[1]) == 'linux') + $os = "Linux"; + elseif(strtolower($matches[1]) == 'win') + $os = "Windows"; + else + continue; + + if(isset($matches[2]) and strtolower($matches[2]) == '64') + $arch = "64"; + else + $arch = "32"; + + $modname = strtolower($cfgMod['mod_name']) == "none"? "":" [MOD:" . $cfgMod['mod_name']."]"; + $friendly_name = $game_cfg['game_name'] . " (" . $os . " " . $arch . "bits)$modname"; + $games[$game_cfg['home_cfg_id'].'-'.$cfgMod['mod_cfg_id'].'-'.$os] = $friendly_name; + $workshop_config_labels[(string)$mod_xml->installer_name."_".$os.".xml"] = $friendly_name; + } + } + } + } + return $games; +} + function exec_ogp_module() { Global $db,$view; echo '

Steam Workshop

'; define('CONFIGS', "modules/steam_workshop/game_configs/"); + $workshop_config_labels = array(); + $games = steam_workshop_admin_get_game_options($db, $workshop_config_labels); if(isset($_REQUEST['home_cfg_id-mod_cfg_id-os'])) list($home_cfg_id, $mod_cfg_id, $os) = explode('-', $_REQUEST['home_cfg_id-mod_cfg_id-os']); @@ -149,11 +194,13 @@ function exec_ogp_module() steam_workshop_admin_save_xml($xml_file, $_POST, $removed_ids); } - if(isset($_POST['copy_selected_config']) || isset($_POST['confirm_copy_config'])) + if(isset($_POST['copy_selected_config'])) { + $copy_source_filename = isset($_POST['copy_source_config']) ? (string)$_POST['copy_source_config'] : ''; + $destination_filename = basename($xml_file); list($source_path, $destination_path) = steam_workshop_get_safe_config_copy_paths( - isset($_POST['copy_source_config']) ? $_POST['copy_source_config'] : '', - basename($xml_file) + $copy_source_filename, + $destination_filename ); if($source_path === false || $destination_path === false) { @@ -163,19 +210,13 @@ function exec_ogp_module() { print_failure(get_lang('cannot_copy_workshop_config_to_itself')); } - elseif(file_exists($destination_path) && !isset($_POST['confirm_copy_config'])) + elseif(steam_workshop_copy_config_settings_only($source_path, $destination_path)) { - echo "
".get_lang('confirm_copy_workshop_config')."
". - get_lang_f('workshop_config_copy_confirm_info', basename($source_path), basename($destination_path)). - "
". - "". - "". - "". - "
"; - } - elseif(@copy($source_path, $destination_path)) - { - print_success(get_lang_f('workshop_config_copied', basename($source_path), basename($destination_path))); + $configs = steam_workshop_list_existing_configs(); + $source_filename = basename($source_path); + $source_label = isset($configs[$source_filename]) ? steam_workshop_get_config_display_label($source_filename, $configs[$source_filename], $workshop_config_labels) : $source_filename; + $destination_label = isset($configs[$destination_filename]) ? steam_workshop_get_config_display_label($destination_filename, $configs[$destination_filename], $workshop_config_labels) : (isset($workshop_config_labels[$destination_filename]) ? $workshop_config_labels[$destination_filename].' - '.$destination_filename : $destination_filename); + print_success(get_lang_f('workshop_config_copied', steam_workshop_h($source_label), steam_workshop_h($destination_label))); } else { @@ -220,38 +261,6 @@ function exec_ogp_module() 'uninstall' => isset($uninstall) ? $uninstall : '', ); - $game_cfgs = $db->getGameCfgs(); - $games[0] = get_lang('select_game'); - foreach($game_cfgs as $game_cfg) - { - $server_xml = read_server_config(SERVER_CONFIG_LOCATION."/".$game_cfg['home_cfg_file']); - if(isset($server_xml->installer) and $server_xml->installer == "steamcmd") - { - $cfgMods = $db->getCfgMods($game_cfg['home_cfg_id']); - foreach($cfgMods as $cfgMod) - { - $mod_xml = xml_get_mod($server_xml, $cfgMod['mod_key']); - if(isset($mod_xml->installer_name) and !in_array((string)$mod_xml->installer_name, get_blacklist())) - { - preg_match('/(linux|win)(32|64)?/i', $game_cfg['game_key'], $matches); - - if(strtolower($matches[1]) == 'linux') - $os = "Linux"; - elseif(strtolower($matches[1]) == 'win') - $os = "Windows"; - if(isset($matches[2]) and strtolower($matches[2]) == '64') - $arch = "64"; - else - $arch = "32"; - - $modname = strtolower($cfgMod['mod_name']) == "none"? "":" [MOD:" . $cfgMod['mod_name']."]"; - - $games[$game_cfg['home_cfg_id'].'-'.$cfgMod['mod_cfg_id'].'-'.$os] = $game_cfg['game_name'] . " (" . $os . " " . $arch . "bits)$modname"; - } - } - } - } - $download_methods = array("steamcmd", "steamapi"); $ft = new FormTable(); @@ -260,12 +269,12 @@ function exec_ogp_module() $ft->add_custom_field('game', create_drop_box_from_array_onchange($games, "home_cfg_id-mod_cfg_id-os", @$_REQUEST['home_cfg_id-mod_cfg_id-os'])); if(isset($home_cfg_id) and isset($mod_cfg_id)) { - $copy_options = steam_workshop_get_existing_config_options(isset($xml_file) ? basename($xml_file) : ''); + $copy_options = steam_workshop_get_existing_config_options(isset($xml_file) ? basename($xml_file) : '', $workshop_config_labels); echo "
".get_lang('copy_from_existing_workshop_config')."
". get_lang('copy_from_existing_workshop_config_info'). "
". "". - create_drop_box_from_array($copy_options, "copy_source_config", ''). + create_drop_box_from_array($copy_options, "copy_source_config", '', false). " ". "
"; diff --git a/docs/features/WORKSHOP_SYSTEM.md b/docs/features/WORKSHOP_SYSTEM.md index 8980934b..59f72c36 100644 --- a/docs/features/WORKSHOP_SYSTEM.md +++ b/docs/features/WORKSHOP_SYSTEM.md @@ -34,6 +34,7 @@ The dedicated module still provides: - monitor-button access from Game Monitor - per-game Workshop configuration files - uninstall and admin support pages +- admin copy support for copying Workshop configuration settings between XML configs without copying installed mods or server files - optional regex/config-file editing for games that need automatic mod-list updates - post-install-only workflows for games that only need a move/copy script diff --git a/docs/modules/steam_workshop.md b/docs/modules/steam_workshop.md index 1b2e6462..fab6e7b9 100644 --- a/docs/modules/steam_workshop.md +++ b/docs/modules/steam_workshop.md @@ -41,7 +41,9 @@ Dedicated Steam Workshop support for game servers. - use `workshop_admin.php` for module administration - `File Path` and the regex/mod-string fields are optional - a blank `File Path` means post-install and uninstall scripts run without any automatic config-file editing -- the admin page can copy one existing XML config into the currently selected game/OS config +- the admin page can copy configuration settings from one existing XML config into the currently selected game/OS config +- copied settings include Workshop App ID, download method, anonymous login, paths, regex/config-edit fields, post-install script, and uninstall script +- copied settings do not include installed mods, cached mod lists, game server files, or any files from a game home directory ## Search Backend