diff --git a/includes/lang.php b/includes/lang.php index a2bec0b3..bea19be6 100644 --- a/includes/lang.php +++ b/includes/lang.php @@ -85,6 +85,66 @@ function ogpLang() } } +function ogp_load_english_fallbacks() +{ + static $coreLoaded = false; + static $loadedModules = array(); + global $lang_modules; + + $englishDir = "lang/English"; + if (!is_dir($englishDir)) { + return; + } + + if ($coreLoaded === false) { + $coreFiles = glob($englishDir . "/*.php"); + if (is_array($coreFiles)) { + foreach ($coreFiles as $coreFile) { + ogp_include_lang_file_safely($coreFile); + } + } + $coreLoaded = true; + } + + $modulesToLoad = array(); + if (isset($_REQUEST['m']) && $_REQUEST['m'] !== '') { + $modulesToLoad[] = $_REQUEST['m']; + } + foreach ((array)$lang_modules as $moduleName) { + $modulesToLoad[] = $moduleName; + } + + foreach (array_unique($modulesToLoad) as $moduleName) { + if (!preg_match('/^([a-z]|[0-9]|_|-)+$/i', (string)$moduleName)) { + continue; + } + if (isset($loadedModules[$moduleName])) { + continue; + } + $moduleFile = $englishDir . "/modules/" . $moduleName . ".php"; + if (is_file($moduleFile)) { + ogp_include_lang_file_safely($moduleFile); + } + $loadedModules[$moduleName] = true; + } +} + +function ogp_include_lang_file_safely($filePath) +{ + $previous = set_error_handler(function ($severity, $message) { + if ($severity === E_WARNING && strpos($message, 'already defined') !== false) { + return true; + } + return false; + }); + include_once($filePath); + if ($previous !== null) { + restore_error_handler(); + } else { + restore_error_handler(); + } +} + function get_lang($lang_index) { global $OGPLangPre; @@ -94,13 +154,26 @@ function get_lang($lang_index) return constant($lang_index); } - if(!startsWith($lang_index, $OGPLangPre)){ + if(!startsWith($lang_index, $OGPLangPre)){ $newLangIndex = $OGPLangPre . $lang_index; if (defined($newLangIndex)) { return constant($newLangIndex); } + ogp_load_english_fallbacks(); + if (defined($newLangIndex)) + { + return constant($newLangIndex); + } } + else + { + ogp_load_english_fallbacks(); + if (defined($lang_index)) + { + return constant($lang_index); + } + } // Any other case is error. return "_".$lang_index."_"; @@ -117,13 +190,26 @@ function get_lang_f() return vsprintf(constant($lang_index),$args); } - if(!startsWith($lang_index, $OGPLangPre)){ + if(!startsWith($lang_index, $OGPLangPre)){ $newLangIndex = $OGPLangPre . $lang_index; if (defined($newLangIndex)) { return vsprintf(constant($newLangIndex),$args); } + ogp_load_english_fallbacks(); + if (defined($newLangIndex)) + { + return vsprintf(constant($newLangIndex),$args); + } } + else + { + ogp_load_english_fallbacks(); + if (defined($lang_index)) + { + return vsprintf(constant($lang_index),$args); + } + } return "_".$lang_index."_".implode("_",$args)."_"; } diff --git a/lang/English/modules/settings.php b/lang/English/modules/settings.php index daa8d3f2..f8559ea2 100644 --- a/lang/English/modules/settings.php +++ b/lang/English/modules/settings.php @@ -86,6 +86,8 @@ define('LANG_recaptcha_use_login', "Use Recaptcha on Login"); define('LANG_recaptcha_use_login_info', "If enabled, users will have to solve the Not a Robot Recaptcha when attempting to login."); define('LANG_login_attempts_before_banned', "Number of failed login attempts before user is banned"); define('LANG_login_attempts_before_banned_info', "If a user tries to login with invalid credentials more than this many times, the user will be banned temporarily by the panel."); +define('LANG_login_ban_time', "Login ban duration (seconds)"); +define('LANG_login_ban_time_info', "How long a user stays temporarily banned after reaching the failed login attempt limit."); define('LANG_custom_github_update_username', "GitHub update username"); define('LANG_custom_github_update_username_info', "Enter your GitHub username ONLY to use your own forked repositories to update OGP. This should only be changed by developers who wish to use their own repos for development rather than checking in possibly buggy code into the main branch."); define('LANG_remote_query', "Remote query"); @@ -129,6 +131,16 @@ define('LANG_default_game_server_home_path_prefix', "Default game server home di define('LANG_default_game_server_home_path_prefix_info', "Enter a path prefix for where you want game server homes to be created by default. You can use \"{USERNAME}\" in the path which will be replaced with the OGP username the game server is being assigned to. You can use \"{GAMEKEY}\" in the path which will be replaced with a friendly lowercase name. You can use \"{SKIPID}\" anywhere in the path to skip appending the home ID to the path. Example: /ogp/games/{USERNAME}/{GAMEKEY}{SKIPID} will become /ogp/games/username/arkse/. Example 2: /ogp/games will become /ogp/games/1 where 1 is the game servers ID."); define('LANG_use_authorized_hosts', "Limit API to Defined Authorized Hosts"); define('LANG_use_authorized_hosts_info', "Enable this setting to only allow API calls from pre-defined and approved IP addresses.  Approved addresses can be set on this page once the setting has been enabled.  If this setting is disabled, a user using a valid key will have access to the API from any IP address.  Users using a valid key will be able to use the API to manage any game server they have permissions to administrate."); +define('LANG_allow_setting_cpu_affinity', "Allow CPU affinity editing"); +define('LANG_allow_setting_cpu_affinity_info', "Allow users to set CPU affinity values for their game servers when supported by the host."); +define('LANG_regex_invalid_file_name_chars', "Invalid filename characters regex"); +define('LANG_regex_invalid_file_name_chars_info', "Regular expression used by the file manager to block unsafe filename characters."); +define('LANG_discord_invite_url', "Discord invite URL"); +define('LANG_discord_invite_url_info', "Invite URL used by panel links that send users to your Discord server."); +define('LANG_discord_webhook_main', "Discord webhook (main)"); +define('LANG_discord_webhook_main_info', "Webhook URL used for general panel notifications."); +define('LANG_discord_webhook_admin', "Discord webhook (admin)"); +define('LANG_discord_webhook_admin_info', "Webhook URL used for administrator-specific notifications."); define('LANG_setup_api_authorized_hosts', "Setup API authorized hosts"); define('LANG_autohorized_hosts', "Authorized hosts"); define('LANG_add', "Add"); @@ -143,6 +155,7 @@ define('LANG_reset_game_server_order_info', "Resets game server ordering back to // Debug level define('LANG_debug_level', "Panel Debug Level"); +define('LANG_debug_level_info', "Controls how much PHP error output is shown in the panel."); define('LANG_debug_off', "Off (production)"); define('LANG_debug_fatal_only', "Fatal errors only (page-breaking)"); define('LANG_debug_errors_warnings', "Errors & Warnings"); diff --git a/modules/config_games/config_servers.php b/modules/config_games/config_servers.php index c910815a..afa0b04f 100644 --- a/modules/config_games/config_servers.php +++ b/modules/config_games/config_servers.php @@ -273,6 +273,22 @@ function config_games_print_editor_css() .xml-node__example code{color:#a0d0a0;background:rgba(30,150,50,0.1);padding:1px 4px;border-radius:3px} .xml-jump-link{display:inline-block;margin-bottom:12px;padding:6px 14px;background:#1c6dd0;color:#fff;border-radius:4px;text-decoration:none;font-size:0.9rem} .xml-jump-link:hover{background:#1f7aec;text-decoration:none} +.xml-section-grid{display:flex;flex-direction:column;gap:14px;margin-bottom:18px} +.xml-section-block{border:1px solid #303030;border-radius:6px;background:#141414;padding:12px} +.xml-section-block__head{display:flex;justify-content:space-between;align-items:flex-start;gap:10px;margin-bottom:8px} +.xml-section-block__title{font-size:1.02rem;color:#f0f0f0;font-weight:600} +.xml-section-block__meta{font-size:0.8rem;color:#9f9f9f} +.xml-section-block__desc{font-size:0.86rem;color:#b0b0b0;margin:0 0 10px} +.xml-section-block textarea{width:100%;min-height:170px;background:#0f0f0f;border:1px solid #3c3c3c;border-radius:4px;color:#f7f7f7;padding:8px;font-family:monospace;font-size:0.84rem} +.xml-section-actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:10px} +.xml-btn{border:1px solid #3f3f3f;background:#222;color:#fff;padding:6px 10px;border-radius:4px;cursor:pointer} +.xml-btn:hover{background:#2a2a2a} +.xml-btn--primary{background:#1c6dd0;border-color:#114b99} +.xml-btn--primary:hover{background:#1f7aec} +.xml-btn--danger{background:#6b1f1f;border-color:#8d2d2d} +.xml-btn--danger:hover{background:#8d2d2d} +.xml-add-section{border:1px dashed #3a3a3a;border-radius:6px;padding:10px;margin-bottom:16px} +.xml-add-section select{min-width:260px} CSS; } @@ -418,6 +434,301 @@ function config_games_render_editor(SimpleXMLElement $xml) return $html; } +function config_games_get_config_file_path($db, $home_cfg_id) +{ + $cfgInfo = $db->getGameCfg((int)$home_cfg_id); + if ($cfgInfo === FALSE) { + return false; + } + return SERVER_CONFIG_LOCATION . $cfgInfo['home_cfg_file']; +} + +function config_games_parse_section_payload($sectionName, $sectionXml) +{ + $sectionName = trim((string)$sectionName); + if ($sectionName === '' || !preg_match('/^[A-Za-z0-9_\\-]+$/', $sectionName)) { + return array(false, 'Invalid section name.'); + } + $sectionXml = trim((string)$sectionXml); + if ($sectionXml === '') { + return array(false, 'Section XML cannot be empty.'); + } + + $tmpDom = new DOMDocument(); + $tmpDom->preserveWhiteSpace = true; + $tmpDom->formatOutput = false; + $wrapped = '' . $sectionXml . ''; + $prev = libxml_use_internal_errors(true); + libxml_clear_errors(); + $ok = $tmpDom->loadXML($wrapped); + $errors = libxml_get_errors(); + libxml_clear_errors(); + libxml_use_internal_errors($prev); + if (!$ok) { + $msg = !empty($errors) ? trim($errors[0]->message) . ' (line ' . $errors[0]->line . ')' : 'Section XML is not well-formed.'; + return array(false, $msg); + } + + $elements = array(); + foreach ($tmpDom->documentElement->childNodes as $child) { + if ($child instanceof DOMElement) { + $elements[] = $child; + } + } + if (count($elements) !== 1) { + return array(false, 'Section XML must contain exactly one top-level element.'); + } + if ($elements[0]->tagName !== $sectionName) { + return array(false, 'Section XML root tag must be <' . htmlspecialchars($sectionName, ENT_QUOTES, 'UTF-8') . '>.'); + } + return array($elements[0], ''); +} + +function config_games_get_top_level_sections($configFile) +{ + $sections = array(); + if (!file_exists($configFile)) { + return $sections; + } + $dom = new DOMDocument(); + $dom->preserveWhiteSpace = true; + $dom->formatOutput = false; + if (!$dom->load($configFile)) { + return $sections; + } + $schema = config_games_schema_order(); + $descriptions = config_games_tag_descriptions(); + foreach ($dom->documentElement->childNodes as $child) { + if (!($child instanceof DOMElement)) { + continue; + } + $name = $child->tagName; + $sections[] = array( + 'name' => $name, + 'required' => ($schema[$name] ?? null) === true, + 'optional' => ($schema[$name] ?? null) === false, + 'xml' => $dom->saveXML($child), + 'description' => $descriptions[$name]['desc'] ?? 'Top-level configuration section.', + ); + } + return $sections; +} + +function config_games_validate_document_or_errors(DOMDocument $dom) +{ + $tmp = tempnam(sys_get_temp_dir(), 'gsp_cfg_section_'); + if ($tmp === false) { + return array('Could not create temporary file for validation.'); + } + $dom->save($tmp); + $errors = config_games_validate_xml_file($tmp); + @unlink($tmp); + return $errors; +} + +function config_games_validate_section_update($db, $home_cfg_id, $sectionName, $sectionXml) +{ + $configFile = config_games_get_config_file_path($db, $home_cfg_id); + if ($configFile === false || !file_exists($configFile)) { + return array(false, array('Configuration file not found.')); + } + + list($sectionNode, $parseError) = config_games_parse_section_payload($sectionName, $sectionXml); + if ($sectionNode === false) { + return array(false, array($parseError)); + } + + $dom = new DOMDocument(); + $dom->preserveWhiteSpace = true; + $dom->formatOutput = false; + if ($dom->load($configFile) === false) { + return array(false, array('Could not parse configuration XML.')); + } + $root = $dom->documentElement; + $import = $dom->importNode($sectionNode, true); + $replaced = false; + foreach ($root->childNodes as $child) { + if ($child instanceof DOMElement && $child->tagName === $sectionName) { + $root->replaceChild($import, $child); + $replaced = true; + break; + } + } + if (!$replaced) { + $root->appendChild($import); + } + + $errors = config_games_validate_document_or_errors($dom); + if (!empty($errors)) { + return array(false, $errors); + } + return array(true, array()); +} + +function config_games_save_dom_and_refresh_cfg($db, $configFile, DOMDocument $dom) +{ + if ($dom->save($configFile) === false) { + return array(false, array('Failed to write configuration file.')); + } + $config = read_server_config($configFile); + if ($config !== FALSE) { + $db->addGameCfg($config); + } + return array(true, array()); +} + +function config_games_upsert_top_level_section($db, $home_cfg_id, $sectionName, $sectionXml) +{ + $configFile = config_games_get_config_file_path($db, $home_cfg_id); + if ($configFile === false || !file_exists($configFile)) { + return array(false, array('Configuration file not found.')); + } + + list($sectionNode, $parseError) = config_games_parse_section_payload($sectionName, $sectionXml); + if ($sectionNode === false) { + return array(false, array($parseError)); + } + + $dom = new DOMDocument(); + $dom->preserveWhiteSpace = true; + $dom->formatOutput = true; + if ($dom->load($configFile) === false) { + return array(false, array('Could not parse configuration XML.')); + } + $import = $dom->importNode($sectionNode, true); + $root = $dom->documentElement; + $replaced = false; + foreach ($root->childNodes as $child) { + if ($child instanceof DOMElement && $child->tagName === $sectionName) { + $root->replaceChild($import, $child); + $replaced = true; + break; + } + } + + if (!$replaced) { + $schemaKeys = array_keys(config_games_schema_order()); + $targetIndex = array_search($sectionName, $schemaKeys, true); + $inserted = false; + if ($targetIndex !== false) { + foreach ($root->childNodes as $child) { + if (!($child instanceof DOMElement)) { + continue; + } + $childIndex = array_search($child->tagName, $schemaKeys, true); + if ($childIndex !== false && $childIndex > $targetIndex) { + $root->insertBefore($import, $child); + $inserted = true; + break; + } + } + } + if (!$inserted) { + $root->appendChild($import); + } + } + + $errors = config_games_validate_document_or_errors($dom); + if (!empty($errors)) { + return array(false, $errors); + } + return config_games_save_dom_and_refresh_cfg($db, $configFile, $dom); +} + +function config_games_remove_optional_section($db, $home_cfg_id, $sectionName) +{ + $schema = config_games_schema_order(); + if (($schema[$sectionName] ?? null) === true) { + return array(false, array('Required sections cannot be removed: ' . $sectionName)); + } + + $configFile = config_games_get_config_file_path($db, $home_cfg_id); + if ($configFile === false || !file_exists($configFile)) { + return array(false, array('Configuration file not found.')); + } + + $dom = new DOMDocument(); + $dom->preserveWhiteSpace = true; + $dom->formatOutput = true; + if ($dom->load($configFile) === false) { + return array(false, array('Could not parse configuration XML.')); + } + $root = $dom->documentElement; + $removed = false; + foreach ($root->childNodes as $child) { + if ($child instanceof DOMElement && $child->tagName === $sectionName) { + $root->removeChild($child); + $removed = true; + break; + } + } + if (!$removed) { + return array(false, array('Section not found: ' . $sectionName)); + } + + $errors = config_games_validate_document_or_errors($dom); + if (!empty($errors)) { + return array(false, $errors); + } + return config_games_save_dom_and_refresh_cfg($db, $configFile, $dom); +} + +function config_games_render_top_level_editor($home_cfg_id, $configFile) +{ + $sections = config_games_get_top_level_sections($configFile); + $schema = config_games_schema_order(); + $presentNames = array_map(function ($section) { + return $section['name']; + }, $sections); + $optionalMissing = array(); + foreach ($schema as $name => $required) { + if ($required === false && !in_array($name, $presentNames, true)) { + $optionalMissing[] = $name; + } + } + + echo "

Section Editor

"; + echo "

Edit one top-level section at a time. Validate a block before saving. Required sections cannot be removed. Optional sections can be added or removed safely.

"; + + if (!empty($optionalMissing)) { + echo "
"; + echo ""; + echo " "; + echo " "; + echo ""; + echo "
"; + } + + echo "
"; + foreach ($sections as $section) { + $safeName = htmlspecialchars($section['name'], ENT_QUOTES, 'UTF-8'); + $safeXml = htmlspecialchars((string)$section['xml'], ENT_QUOTES, 'UTF-8'); + $safeDesc = htmlspecialchars((string)$section['description'], ENT_QUOTES, 'UTF-8'); + $requiredText = $section['required'] ? 'Required' : 'Optional/Custom'; + + echo "
"; + echo ""; + echo ""; + echo "
{$safeName}
{$requiredText}
"; + echo "

{$safeDesc}

"; + echo ""; + echo "
"; + echo ""; + echo ""; + echo ""; + if (!$section['required']) { + echo ""; + } + echo "
"; + echo "
"; + } + echo "
"; +} + /** * Save XML from structured form nodes payload. * Validates against the schema before writing. @@ -613,6 +924,68 @@ function exec_ogp_module() { print_success(get_lang('configs_updated_ok')); } + if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['home_cfg_id']) && + (isset($_POST['validate_section']) || isset($_POST['save_section']) || isset($_POST['remove_section']) || isset($_POST['add_optional_section']) || isset($_POST['reset_section']))) { + $edit_id = (int)$_POST['home_cfg_id']; + $sectionName = trim((string)($_POST['section_name'] ?? '')); + $sectionXml = (string)($_POST['section_xml'] ?? ''); + + if (isset($_POST['reset_section'])) { + print_success('Section reset. No changes were saved.'); + } elseif (isset($_POST['validate_section'])) { + list($ok, $errors) = config_games_validate_section_update($db, $edit_id, $sectionName, $sectionXml); + if ($ok) { + print_success('Section XML is valid.'); + } else { + echo "
⚠ Section validation failed:
"; + } + } elseif (isset($_POST['save_section'])) { + list($ok, $errors) = config_games_upsert_top_level_section($db, $edit_id, $sectionName, $sectionXml); + if ($ok) { + print_success(get_lang('configs_updated_ok')); + } else { + echo "
⚠ Section save failed:
"; + } + } elseif (isset($_POST['remove_section'])) { + list($ok, $errors) = config_games_remove_optional_section($db, $edit_id, $sectionName); + if ($ok) { + print_success('Optional section removed.'); + } else { + echo "
⚠ Could not remove section:
"; + } + } elseif (isset($_POST['add_optional_section'])) { + $schema = config_games_schema_order(); + if (($schema[$sectionName] ?? null) !== false) { + print_failure('Only schema-defined optional sections can be added from this menu.'); + } else { + $newXml = "<{$sectionName}>"; + list($ok, $errors) = config_games_upsert_top_level_section($db, $edit_id, $sectionName, $newXml); + if ($ok) { + print_success('Optional section added.'); + } else { + echo "
⚠ Could not add section:
"; + } + } + } + $_GET['home_cfg_id'] = $edit_id; + } + if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save_xml']) && isset($_POST['home_cfg_id'])) { $edit_id = (int)$_POST['home_cfg_id']; @@ -755,6 +1128,9 @@ function exec_ogp_module() { } else { $raw_xml_content = htmlspecialchars(file_get_contents($config_file), ENT_QUOTES, 'UTF-8'); echo "
"; + config_games_render_top_level_editor($home_cfg_id, $config_file); + + echo "
Open legacy detailed node editor"; echo "
"; echo ""; echo ""; @@ -762,16 +1138,21 @@ function exec_ogp_module() { echo config_games_render_editor($xml); echo "
"; echo "

★ = required field. Use the action dropdown to remove entire sections. Attribute values left blank will be removed. Script sections such as post_install are fully editable. Changes are validated against the schema before saving.

"; + echo "
"; + echo "
"; + // Raw XML editor echo "
"; - echo "

Raw XML Editor

"; + echo "

Full Raw XML Editor

"; echo "
Warning: Saving raw XML bypasses the guided editor. The file will be validated against the schema before saving. Invalid XML will be rejected.
"; echo ""; echo "
"; + echo "
"; + echo ""; echo ""; echo "
"; - echo "
"; echo ""; + echo "
"; echo ""; // #xml-editor-section } } diff --git a/modules/steam_workshop/admin.php b/modules/steam_workshop/admin.php index 5f66c836..84a8fafb 100644 --- a/modules/steam_workshop/admin.php +++ b/modules/steam_workshop/admin.php @@ -2,18 +2,9 @@ /* * GSP – Steam Workshop: Admin profile management * Copyright (C) 2025 WDS / GameServerPanel - * - * Accessible via: home.php?m=steam_workshop&p=admin - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. */ require_once __DIR__ . '/includes/functions.php'; - -// Load the XML config parser so sw_sync_profiles() can read game configs. if (!defined('SERVER_CONFIG_LOCATION')) { require_once __DIR__ . '/../../config_games/server_config_parser.php'; } @@ -22,23 +13,65 @@ function exec_ogp_module() { global $db; - echo '

Steam Workshop – Admin

'; + echo '

Steam Workshop – Admin

'; + sw_admin_print_styles(); - $action = isset($_GET['action']) ? $_GET['action'] : ''; + $action = $_GET['action'] ?? ''; - // ── POST: save a profile edit ───────────────────────────────────── if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save_profile'])) { sw_admin_save_profile($db); return; } - // ── POST: sync profiles from XML configs ────────────────────────── if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['sync_profiles'])) { $n = sw_sync_profiles($db); sw_success("Sync complete. $n new profile(s) created."); } - // ── GET: show edit form for one profile ─────────────────────────── + if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['detect_defaults'])) { + $profile = sw_get_profile_by_id($db, (int)($_POST['id'] ?? 0)); + if (!$profile) { + sw_error('Profile not found.'); + sw_admin_list($db); + return; + } + $detected = sw_detect_profile_defaults_from_xml($profile['config_name']); + if (empty($detected)) { + sw_error('No Steam defaults were detected in this game XML. You can still enter values manually.'); + } else { + sw_success('Detected XML defaults. Review and apply when ready.'); + } + sw_admin_edit_form($profile, $detected, true); + return; + } + + if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['apply_detected_defaults'])) { + $profile = sw_get_profile_by_id($db, (int)($_POST['id'] ?? 0)); + if (!$profile) { + sw_error('Profile not found.'); + sw_admin_list($db); + return; + } + $detected = sw_detect_profile_defaults_from_xml($profile['config_name']); + if (empty($detected)) { + sw_error('No Steam defaults were detected in this game XML.'); + sw_admin_edit_form($profile); + return; + } + + $overwrite = isset($_POST['overwrite_existing']) && $_POST['overwrite_existing'] === '1'; + $updated = sw_apply_detected_profile_defaults($db, $profile, $detected, $overwrite); + if ($updated > 0) { + sw_success("Applied $updated detected default value(s)." . ($overwrite ? ' Existing values were allowed to be overwritten.' : ' Existing non-empty values were kept.')); + } else { + sw_success('No profile values needed updating based on current overwrite setting.'); + } + + $profile = sw_get_profile_by_id($db, (int)$profile['id']); + sw_admin_edit_form($profile, $detected, true); + return; + } + if ($action === 'edit' && isset($_GET['id'])) { $profile = sw_get_profile_by_id($db, (int)$_GET['id']); if ($profile) { @@ -50,18 +83,12 @@ function exec_ogp_module() return; } - // ── Default: list all profiles ──────────────────────────────────── sw_admin_list($db); } -// ───────────────────────────────────────────────────────────────────────── -// Helpers -// ───────────────────────────────────────────────────────────────────────── - function sw_admin_save_profile($db) { $id = isset($_POST['id']) ? (int)$_POST['id'] : 0; - if (!$id) { sw_error('Invalid profile ID.'); sw_admin_list($db); @@ -75,38 +102,34 @@ function sw_admin_save_profile($db) return; } - // Collect and sanitize fields from POST. $fields = array( - 'enabled' => isset($_POST['enabled']) ? 1 : 0, - 'steam_app_id' => trim($_POST['steam_app_id'] ?? ''), - 'workshop_app_id' => trim($_POST['workshop_app_id'] ?? ''), - 'steam_login_required' => isset($_POST['steam_login_required']) ? 1 : 0, - 'steamcmd_login_mode' => $_POST['steamcmd_login_mode'] === 'account' ? 'account' : 'anonymous', - 'steamcmd_path' => trim($_POST['steamcmd_path'] ?? ''), - 'workshop_download_dir_template' => trim($_POST['workshop_download_dir_template'] ?? ''), - 'server_root_template' => trim($_POST['server_root_template'] ?? ''), - 'install_path_template' => trim($_POST['install_path_template'] ?? ''), - 'folder_naming_format' => trim($_POST['folder_naming_format'] ?? ''), - 'mod_launch_param_template' => trim($_POST['mod_launch_param_template'] ?? '-mod='), + 'enabled' => isset($_POST['enabled']) ? 1 : 0, + 'steam_app_id' => trim($_POST['steam_app_id'] ?? ''), + 'workshop_app_id' => trim($_POST['workshop_app_id'] ?? ''), + 'steam_login_required' => isset($_POST['steam_login_required']) ? 1 : 0, + 'steamcmd_login_mode' => (($_POST['steamcmd_login_mode'] ?? 'anonymous') === 'account') ? 'account' : 'anonymous', + 'steamcmd_path' => trim($_POST['steamcmd_path'] ?? ''), + 'workshop_download_dir_template' => trim($_POST['workshop_download_dir_template'] ?? ''), + 'server_root_template' => trim($_POST['server_root_template'] ?? ''), + 'install_path_template' => trim($_POST['install_path_template'] ?? ''), + 'folder_naming_format' => trim($_POST['folder_naming_format'] ?? ''), + 'mod_launch_param_template' => trim($_POST['mod_launch_param_template'] ?? '-mod='), 'servermod_launch_param_template' => trim($_POST['servermod_launch_param_template'] ?? '-serverMod='), - 'install_script_template' => trim($_POST['install_script_template'] ?? ''), - 'update_script_template' => trim($_POST['update_script_template'] ?? ''), - 'copy_bikeys_enabled' => isset($_POST['copy_bikeys_enabled']) ? 1 : 0, - 'notes' => trim($_POST['notes'] ?? ''), + 'install_script_template' => trim($_POST['install_script_template'] ?? ''), + 'update_script_template' => trim($_POST['update_script_template'] ?? ''), + 'copy_bikeys_enabled' => isset($_POST['copy_bikeys_enabled']) ? 1 : 0, + 'notes' => trim($_POST['notes'] ?? ''), ); - $set_parts = array(); + $setParts = array(); foreach ($fields as $col => $val) { - $safe = $db->realEscapeSingle($val); - $set_parts[] = "`$col` = '$safe'"; + $setParts[] = "`$col` = '" . $db->realEscapeSingle($val) . "'"; } - $set_parts[] = "`updated_at` = NOW()"; - - $set_sql = implode(', ', $set_parts); + $setParts[] = "`updated_at` = NOW()"; $ok = $db->query( - "UPDATE `OGP_DB_PREFIXsteam_workshop_game_profiles` - SET $set_sql + "UPDATE " . sw_table('steam_workshop_game_profiles') . " + SET " . implode(', ', $setParts) . " WHERE `id` = $id LIMIT 1" ); @@ -128,297 +151,177 @@ function sw_admin_list($db) { $profiles = sw_get_profiles($db); ?> -

- Each game config XML gets one Workshop profile. - Use Sync Profiles to auto-create rows for new game configs. - Enable and configure each profile to activate Steam Workshop for that game. -

+
+

+ Profiles map game XML configs to Steam Workshop defaults. Sync to create missing profiles, then edit each profile for game-specific paths and launch templates. +

-
- -
+
+ +
-
- - -

No profiles yet. Click Sync Profiles to create them from the installed game configs.

- - - - - - - - - - - - - - - - - - - - - - -
Config NameGame NameWorkshop App IDEnabledActions
- Yes' : 'No' ?> - - Edit -
- +

No profiles yet. Click Sync Profiles to create them from installed game configs.

+ + + + + + + + + + + + + + + + + + + + + + + + +
Config NameGame NameSteam App IDWorkshop App IDEnabledActions
Yes' : 'No' ?>Edit
+ +
+

« Back to profile list

-

Edit Profile:

-

- Supported placeholders (use in path/script templates):
- {HOME_ID}   - {SERVER_ID}   - {REMOTE_SERVER_ID}   - {GAME_NAME}   - {CONFIG_NAME}   - {WORKSHOP_ID}   - {MOD_NAME}   - {FOLDER_NAME}   - {STEAM_APP_ID}   - {WORKSHOP_APP_ID}   - {STEAMCMD_PATH}   - {WORKSHOP_DOWNLOAD_DIR}   - {SERVER_ROOT}   - {INSTALL_PATH}   - {MOD_FOLDER} -

+
+
+ Placeholder tokens: + {SERVER_ROOT} {HOME_ID} {STEAM_APP_ID} {WORKSHOP_APP_ID} {MOD_FOLDER} +
-
- +
+

XML-Assisted Defaults

+

Use values detected from this game XML. Existing values are not overwritten unless you explicitly allow it.

+ + + + - + +
+ Detected values: +
    +
  • Steam App ID:
  • +
  • Workshop App ID:
  • +
  • SteamCMD path:
  • +
  • Workshop download dir:
  • +
  • Server root:
  • +
  • Mod install path:
  • +
+
+ + + + +
+ + - - - + + - - - - +
+

Global Profile Defaults

+
+ + + + + + +
+
- - - +
+

Path Templates

+

Use placeholders so paths stay portable between server homes.

+
+ + + +
+
- - - - +
+

Per-Game Runtime Values

+
+ + + + +
+
- - - - +
+

Optional Script Templates

+ + +
- - - - +
+

Notes

+ +
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
General
- > - Enable Steam Workshop for this game config -
Steam / SteamCMD
- - (e.g. 223350 for DayZ Dedicated Server) -
- - (e.g. 221100 for DayZ Workshop content) -
- -
- > - Requires authenticated Steam login (not anonymous) -
- -
Paths
- -
- Where SteamCMD downloads mods.
- Example: {SERVER_ROOT}/steamapps/workshop/content/{WORKSHOP_APP_ID} -
-
- -
- Root directory of the game server. Example: /home/gameserver/servers/{HOME_ID} - -
- -
- Where the renamed mod folder ends up. Example: {SERVER_ROOT}/{MOD_FOLDER} - -
Folder & Launch Params
- -
- Default folder name template. Common values: @{MOD_NAME} or @{WORKSHOP_ID} - -
- - Prefix for client-required mods (e.g. -mod=) -
- - Prefix for server-only mods (e.g. -serverMod=) -
- > - Copy .bikey files from mod keys/ folder into server keys/ folder -
Scripts (optional)
- - - Shell commands to run when installing a mod for the first time. Placeholders expanded before execution. - -
- - - Shell commands to run when updating an already-installed mod. - -
Notes
- -
- -

- -   - Cancel -

- - - -
-

DayZ Default Values (for reference)

-
    -
  • Steam App ID: 223350 (DayZ Dedicated Server)
  • -
  • Workshop App ID: 221100 (DayZ Workshop)
  • -
  • Workshop Download Dir: {SERVER_ROOT}/steamapps/workshop/content/{WORKSHOP_APP_ID}
  • -
  • Folder Naming Format: @{MOD_NAME}
  • -
  • Client Mod Launch Param: -mod=
  • -
  • Server-Side Mod Launch Param: -serverMod=
  • -
  • Copy .bikey Files: Yes
  • -
+

+ + Cancel +

+ +
+ .sw-admin-panel{background:#171717;border:1px solid #2d2d2d;border-radius:6px;padding:14px;margin:10px 0;color:#e7e7e7} + .sw-admin-table{border-collapse:collapse;background:#121212} + .sw-admin-table th,.sw-admin-table td{border:1px solid #2c2c2c;padding:8px} + .sw-admin-table thead th{background:#232323;color:#fff} + .sw-state-on{color:#78d978;font-weight:700} + .sw-state-off{color:#9a9a9a} + .sw-section{margin-top:14px;padding:12px;border:1px solid #2f2f2f;border-radius:4px;background:#111} + .sw-section h4{margin:0 0 8px 0;color:#f6f6f6} + .sw-note{margin-bottom:10px;background:#202020;border-left:3px solid #3f80d0;padding:10px} + .sw-muted{color:#b3b3b3} + .sw-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:10px} + .sw-grid label, .sw-section > label{display:block} + .sw-grid span, .sw-section span{display:block;font-size:12px;color:#bdbdbd;margin-bottom:4px} + .sw-grid input[type=text], .sw-grid select, .sw-section textarea{width:100%;box-sizing:border-box;background:#0d0d0d;border:1px solid #3a3a3a;color:#eee;padding:7px;border-radius:4px} + .sw-grid input[type=checkbox]{transform:scale(1.1);margin-top:4px} + .sw-detected-box{margin-top:10px;padding:10px;background:#1d2a1d;border:1px solid #335933;border-radius:4px} + .sw-detected-box ul{margin:8px 0 10px 18px} + .sw-detected-box code,.sw-note code{background:#0b0b0b;padding:1px 4px;border-radius:3px;color:#9fd4ff} + '; +} diff --git a/modules/steam_workshop/includes/functions.php b/modules/steam_workshop/includes/functions.php index 84278f6d..c3c139df 100644 --- a/modules/steam_workshop/includes/functions.php +++ b/modules/steam_workshop/includes/functions.php @@ -9,6 +9,25 @@ * (at your option) any later version. */ +function sw_db_prefix() +{ + if (defined('DB_PREFIX') && DB_PREFIX !== '') { + return DB_PREFIX; + } + if (isset($GLOBALS['db_prefix']) && $GLOBALS['db_prefix'] !== '') { + return $GLOBALS['db_prefix']; + } + if (isset($GLOBALS['table_prefix']) && $GLOBALS['table_prefix'] !== '') { + return $GLOBALS['table_prefix']; + } + return 'gsp_'; +} + +function sw_table($tableName) +{ + return '`' . sw_db_prefix() . $tableName . '`'; +} + // ── Profile helpers ─────────────────────────────────────────────────────── /** @@ -20,7 +39,7 @@ function sw_get_profiles($db) { return $db->resultQuery( - "SELECT * FROM `OGP_DB_PREFIXsteam_workshop_game_profiles` + "SELECT * FROM " . sw_table('steam_workshop_game_profiles') . " ORDER BY `game_name` ASC, `config_name` ASC" ); } @@ -36,7 +55,7 @@ function sw_get_profile_by_id($db, $id) { $id = (int)$id; $rows = $db->resultQuery( - "SELECT * FROM `OGP_DB_PREFIXsteam_workshop_game_profiles` + "SELECT * FROM " . sw_table('steam_workshop_game_profiles') . " WHERE `id` = $id LIMIT 1" ); return ($rows && isset($rows[0])) ? $rows[0] : false; @@ -53,7 +72,7 @@ function sw_get_profile_by_config_name($db, $config_name) { $safe = $db->realEscapeSingle($config_name); $rows = $db->resultQuery( - "SELECT * FROM `OGP_DB_PREFIXsteam_workshop_game_profiles` + "SELECT * FROM " . sw_table('steam_workshop_game_profiles') . " WHERE `config_name` = '$safe' LIMIT 1" ); return ($rows && isset($rows[0])) ? $rows[0] : false; @@ -72,11 +91,11 @@ function sw_get_profile_for_home($db, $home_id) $home_id = (int)$home_id; $rows = $db->resultQuery( "SELECT p.* - FROM `OGP_DB_PREFIXsteam_workshop_game_profiles` p - JOIN `OGP_DB_PREFIXconfig_homes` c - ON c.`game_key` = p.`config_name` - JOIN `OGP_DB_PREFIXserver_homes` s - ON s.`home_cfg_id` = c.`home_cfg_id` + FROM " . sw_table('steam_workshop_game_profiles') . " p + JOIN " . sw_table('config_homes') . " c + ON c.`game_key` = p.`config_name` + JOIN " . sw_table('server_homes') . " s + ON s.`home_cfg_id` = c.`home_cfg_id` WHERE s.`home_id` = $home_id AND p.`enabled` = 1 LIMIT 1" @@ -97,7 +116,7 @@ function sw_get_server_mods($db, $home_id) { $home_id = (int)$home_id; return $db->resultQuery( - "SELECT * FROM `OGP_DB_PREFIXsteam_workshop_server_mods` + "SELECT * FROM " . sw_table('steam_workshop_server_mods') . " WHERE `home_id` = $home_id ORDER BY `sort_order` ASC, `id` ASC" ); @@ -114,7 +133,7 @@ function sw_get_mod_by_id($db, $id) { $id = (int)$id; $rows = $db->resultQuery( - "SELECT * FROM `OGP_DB_PREFIXsteam_workshop_server_mods` + "SELECT * FROM " . sw_table('steam_workshop_server_mods') . " WHERE `id` = $id LIMIT 1" ); return ($rows && isset($rows[0])) ? $rows[0] : false; @@ -135,9 +154,9 @@ function sw_get_home_info($db, $home_id) $home_id = (int)$home_id; $rows = $db->resultQuery( "SELECT s.*, c.`game_key`, c.`game_name`, r.`agent_ip`, r.`agent_port` - FROM `OGP_DB_PREFIXserver_homes` s - JOIN `OGP_DB_PREFIXconfig_homes` c ON c.`home_cfg_id` = s.`home_cfg_id` - JOIN `OGP_DB_PREFIXremote_servers` r ON r.`remote_server_id` = s.`remote_server_id` + FROM " . sw_table('server_homes') . " s + JOIN " . sw_table('config_homes') . " c ON c.`home_cfg_id` = s.`home_cfg_id` + JOIN " . sw_table('remote_servers') . " r ON r.`remote_server_id` = s.`remote_server_id` WHERE s.`home_id` = $home_id LIMIT 1" ); return ($rows && isset($rows[0])) ? $rows[0] : false; @@ -167,7 +186,7 @@ function sw_user_owns_home($db, $user_id, $home_id) // Direct owner $rows = $db->resultQuery( - "SELECT 1 FROM `OGP_DB_PREFIXserver_homes` + "SELECT 1 FROM " . sw_table('server_homes') . " WHERE `home_id` = $home_id AND `user_id_main` = $user_id LIMIT 1" ); if ($rows) { @@ -176,7 +195,7 @@ function sw_user_owns_home($db, $user_id, $home_id) // Assigned via user_homes $rows = $db->resultQuery( - "SELECT 1 FROM `OGP_DB_PREFIXuser_homes` + "SELECT 1 FROM " . sw_table('user_homes') . " WHERE `home_id` = $home_id AND `user_id` = $user_id LIMIT 1" ); if ($rows) { @@ -185,8 +204,8 @@ function sw_user_owns_home($db, $user_id, $home_id) // Assigned via group $rows = $db->resultQuery( - "SELECT 1 FROM `OGP_DB_PREFIXuser_group_homes` ugh - JOIN `OGP_DB_PREFIXuser_groups` ug ON ug.`group_id` = ugh.`group_id` + "SELECT 1 FROM " . sw_table('user_group_homes') . " ugh + JOIN " . sw_table('user_groups') . " ug ON ug.`group_id` = ugh.`group_id` WHERE ugh.`home_id` = $home_id AND ug.`user_id` = $user_id LIMIT 1" ); return (bool)$rows; @@ -250,9 +269,9 @@ function sw_sync_profiles($db) $safe_name = $db->realEscapeSingle($game_name); $ok = $db->query( - "INSERT IGNORE INTO `OGP_DB_PREFIXsteam_workshop_game_profiles` + "INSERT IGNORE INTO " . sw_table('steam_workshop_game_profiles') . " (`config_name`, `game_name`, `enabled`) - VALUES ('$safe_config', '$safe_name', 0)" + VALUES ('$safe_config', '$safe_name', 0)" ); if ($ok) { $created++; @@ -364,3 +383,92 @@ function sw_h($v) { return htmlspecialchars((string)$v, ENT_QUOTES, 'UTF-8'); } + +function sw_detect_profile_defaults_from_xml($configName) +{ + $configName = trim((string)$configName); + if ($configName === '') { + return array(); + } + + $matched = null; + foreach (sw_get_all_game_configs() as $xml) { + if ((string)$xml->game_key === $configName) { + $matched = $xml; + break; + } + } + if (!$matched) { + return array(); + } + + $steamAppId = ''; + if (isset($matched->mods->mod)) { + foreach ($matched->mods->mod as $mod) { + $candidate = trim((string)$mod->installer_name); + if ($candidate !== '' && preg_match('/^\d+$/', $candidate)) { + $steamAppId = $candidate; + break; + } + } + } + + $xmlBlob = $matched->asXML(); + $workshopAppId = ''; + if ($xmlBlob !== false && preg_match('/steamapps\/workshop\/content\/(\d+)/i', $xmlBlob, $m)) { + $workshopAppId = $m[1]; + } + if ($workshopAppId === '') { + $workshopAppId = $steamAppId; + } + + return array( + 'steam_app_id' => $steamAppId, + 'workshop_app_id' => $workshopAppId, + 'steamcmd_path' => '/home/gameserver/steamcmd/steamcmd.sh', + 'server_root_template' => '{SERVER_ROOT}', + 'workshop_download_dir_template' => '{SERVER_ROOT}/steamapps/workshop/content/{WORKSHOP_APP_ID}', + 'install_path_template' => '{SERVER_ROOT}/{MOD_FOLDER}', + ); +} + +function sw_apply_detected_profile_defaults($db, array $profile, array $detected, $overwriteExisting = false) +{ + $columns = array( + 'steam_app_id', + 'workshop_app_id', + 'steamcmd_path', + 'workshop_download_dir_template', + 'server_root_template', + 'install_path_template', + ); + $setParts = array(); + $updated = 0; + + foreach ($columns as $column) { + if (!array_key_exists($column, $detected) || $detected[$column] === '') { + continue; + } + $current = trim((string)($profile[$column] ?? '')); + if (!$overwriteExisting && $current !== '') { + continue; + } + if ($current === $detected[$column]) { + continue; + } + $setParts[] = "`$column` = '" . $db->realEscapeSingle($detected[$column]) . "'"; + $updated++; + } + + if (empty($setParts)) { + return 0; + } + + $setParts[] = "`updated_at` = NOW()"; + $db->query( + "UPDATE " . sw_table('steam_workshop_game_profiles') . " + SET " . implode(', ', $setParts) . " + WHERE `id` = " . (int)$profile['id'] . " LIMIT 1" + ); + return $updated; +} diff --git a/modules/steam_workshop/navigation.xml b/modules/steam_workshop/navigation.xml index 0181e0a5..29a5deda 100644 --- a/modules/steam_workshop/navigation.xml +++ b/modules/steam_workshop/navigation.xml @@ -1,7 +1,6 @@ - ---> diff --git a/modules/steam_workshop/user.php b/modules/steam_workshop/user.php index 97f9ce52..9f31ddd3 100644 --- a/modules/steam_workshop/user.php +++ b/modules/steam_workshop/user.php @@ -95,7 +95,7 @@ function sw_user_add_mod($db, $home_id, array $profile) // Prevent duplicates $safe_wid = $db->realEscapeSingle($workshop_id); $exists = $db->resultQuery( - "SELECT id FROM `OGP_DB_PREFIXsteam_workshop_server_mods` + "SELECT id FROM " . sw_table('steam_workshop_server_mods') . " WHERE `home_id` = $home_id AND `workshop_id` = '$safe_wid' LIMIT 1" ); if ($exists) { @@ -105,7 +105,7 @@ function sw_user_add_mod($db, $home_id, array $profile) // Determine next sort_order $last = $db->resultQuery( - "SELECT MAX(`sort_order`) AS m FROM `OGP_DB_PREFIXsteam_workshop_server_mods` + "SELECT MAX(`sort_order`) AS m FROM " . sw_table('steam_workshop_server_mods') . " WHERE `home_id` = $home_id" ); $sort = ($last && isset($last[0]['m'])) ? ((int)$last[0]['m'] + 1) : 0; @@ -127,9 +127,9 @@ function sw_user_add_mod($db, $home_id, array $profile) $safe_mname = $mod_name; // already escaped above via realEscapeSingle $ok = $db->query( - "INSERT INTO `OGP_DB_PREFIXsteam_workshop_server_mods` + "INSERT INTO " . sw_table('steam_workshop_server_mods') . " (`home_id`, `profile_id`, `workshop_id`, `mod_name`, `folder_name`, - `mod_type`, `sort_order`, `enabled`, `install_status`, `created_at`) + `mod_type`, `sort_order`, `enabled`, `install_status`, `created_at`) VALUES ($home_id, $profile_id, '$safe_wid', '$safe_mname', '$safe_fname', '$mod_type', $sort, 1, '', NOW())" ); @@ -164,7 +164,7 @@ function sw_user_save_mod($db, $home_id) } $ok = $db->query( - "UPDATE `OGP_DB_PREFIXsteam_workshop_server_mods` + "UPDATE " . sw_table('steam_workshop_server_mods') . " SET `mod_name` = '$mod_name', `folder_name` = '$folder_name', `mod_type` = '$mod_type', @@ -193,7 +193,7 @@ function sw_user_delete_mod($db, $home_id) } $db->query( - "DELETE FROM `OGP_DB_PREFIXsteam_workshop_server_mods` + "DELETE FROM " . sw_table('steam_workshop_server_mods') . " WHERE `id` = $mod_id AND `home_id` = $home_id LIMIT 1" ); sw_success('Mod removed from list.'); @@ -214,7 +214,7 @@ function sw_user_toggle_mod($db, $home_id) $new_state = $mod['enabled'] ? 0 : 1; $db->query( - "UPDATE `OGP_DB_PREFIXsteam_workshop_server_mods` + "UPDATE " . sw_table('steam_workshop_server_mods') . " SET `enabled` = $new_state, `updated_at` = NOW() WHERE `id` = $mod_id AND `home_id` = $home_id LIMIT 1" ); @@ -241,7 +241,7 @@ function sw_user_reorder_mod($db, $home_id, $direction) $sorted = array_values($mods); foreach ($sorted as $idx => $m) { $db->query( - "UPDATE `OGP_DB_PREFIXsteam_workshop_server_mods` + "UPDATE " . sw_table('steam_workshop_server_mods') . " SET `sort_order` = $idx WHERE `id` = " . (int)$m['id'] . " AND `home_id` = $home_id LIMIT 1" ); @@ -271,12 +271,12 @@ function sw_user_reorder_mod($db, $home_id, $direction) // Swap sort_order values $db->query( - "UPDATE `OGP_DB_PREFIXsteam_workshop_server_mods` + "UPDATE " . sw_table('steam_workshop_server_mods') . " SET `sort_order` = $swap_pos WHERE `id` = $mod_id AND `home_id` = $home_id LIMIT 1" ); $db->query( - "UPDATE `OGP_DB_PREFIXsteam_workshop_server_mods` + "UPDATE " . sw_table('steam_workshop_server_mods') . " SET `sort_order` = $pos WHERE `id` = $swap_id AND `home_id` = $home_id LIMIT 1" ); @@ -286,7 +286,7 @@ function sw_user_queue_update($db, $home_id) { // Mark all enabled mods as 'queued' so the agent picks them up. $db->query( - "UPDATE `OGP_DB_PREFIXsteam_workshop_server_mods` + "UPDATE " . sw_table('steam_workshop_server_mods') . " SET `install_status` = 'queued', `updated_at` = NOW() WHERE `home_id` = $home_id AND `enabled` = 1" );