From 7a80812fe7554a4a102d542ab8c24af2bd5aff69 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 18 May 2026 21:40:24 +0000 Subject: [PATCH] Add Phase 1 Workshop Content flow to addonsmanager Agent-Logs-Url: https://github.com/GameServerPanel/GSP/sessions/7643e55f-473c-4084-baa0-cf8ae8c9a10a Co-authored-by: iaretechnician <2749183+iaretechnician@users.noreply.github.com> --- Panel/CHANGELOG.md | 1 + Panel/docs/COPILOT_TODO.md | 1 + .../SERVER_CONTENT_WORKSHOP_PHASE1.md | 52 ++++ .../addonsmanager/addons_installer.php | 6 + Panel/modules/addonsmanager/module.php | 28 +- Panel/modules/addonsmanager/navigation.xml | 1 + .../workshop/generic_steam_workshop_linux.sh | 63 ++++ .../generic_steam_workshop_windows_cygwin.sh | 58 ++++ .../addonsmanager/server_content_helpers.php | 200 +++++++++++++ Panel/modules/addonsmanager/user_addons.php | 22 +- .../modules/addonsmanager/workshop_action.php | 277 ++++++++++++++++++ .../addonsmanager/workshop_content.php | 148 ++++++++++ 12 files changed, 854 insertions(+), 3 deletions(-) create mode 100644 Panel/modules/addonsmanager/SERVER_CONTENT_WORKSHOP_PHASE1.md create mode 100644 Panel/modules/addonsmanager/scripts/workshop/generic_steam_workshop_linux.sh create mode 100644 Panel/modules/addonsmanager/scripts/workshop/generic_steam_workshop_windows_cygwin.sh create mode 100644 Panel/modules/addonsmanager/server_content_helpers.php create mode 100644 Panel/modules/addonsmanager/workshop_action.php create mode 100644 Panel/modules/addonsmanager/workshop_content.php diff --git a/Panel/CHANGELOG.md b/Panel/CHANGELOG.md index 8f573d50..921ec2a3 100644 --- a/Panel/CHANGELOG.md +++ b/Panel/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog ## 2026-05-18 +- **Server Content Workshop Phase 1 in addonsmanager:** Added a new `Workshop Content` flow under Server Content with per-home Workshop ID storage, ID validation/deduplication, install/update/remove/update-all actions, manifest-based script handoff (`gsp_server_content/workshop_manifest.json`), safe placeholder workshop scripts for Linux/Cygwin, and schema support via `server_content_workshop` plus `addons.addon_type VARCHAR(32)`. - **Updater layout hardening + pre-update patch framework:** Reworked `modules/administration/panel_update.php` to resolve explicit GSP root/Panel/Website paths, run mandatory preflight checks, self-update updater files before main sync when drift is detected, and apply ordered required patches from `modules/update/patches/` with DB/local state tracking. Backup/rollback now includes both Panel + Website archives and root `version.json`, logs moved to root `logs/update_trace.log`, and the admin Update UI now exposes preflight, patch apply, Apache path scan/fix, backup, update, and rollback actions. - **Billing runtime relocation + portable path bootstrap:** Re-homed storefront runtime to `Panel/modules/billing`, added portable runtime helpers (`billing_bootstrap.php`, `site_config.php`, `site_config.example.php`) with env/local override support for base path and panel path, normalized critical storefront redirects/links to computed billing URLs, and added `Website` compatibility wrappers for key billing entrypoints. - **Panel updater panel-subtree safety:** Hardened updater logic to treat repository `/panel` as the update source when present (ZIP + git flows) so root-level docs/examples/scripts are no longer candidates for panel file overwrite during updates. diff --git a/Panel/docs/COPILOT_TODO.md b/Panel/docs/COPILOT_TODO.md index 50e18511..038459e7 100644 --- a/Panel/docs/COPILOT_TODO.md +++ b/Panel/docs/COPILOT_TODO.md @@ -17,3 +17,4 @@ - Add a panel settings health check that validates reCAPTCHA site/secret keys against active panel/storefront domains and warns admins before registration users see widget errors. - Add an automated deployment check that fails when `Website/timestamp.txt` and `modules/billing/timestamp.txt` diverge after storefront/content changes. - Add an admin preview/diff panel for Apache path repairs so staff can review exact vhost line changes before confirming `Fix Apache Paths`. +- Add Phase 2 Workshop Content UX in `addonsmanager`: browse/search/select Workshop items with metadata while reusing the Phase 1 per-home saved-ID action pipeline. diff --git a/Panel/modules/addonsmanager/SERVER_CONTENT_WORKSHOP_PHASE1.md b/Panel/modules/addonsmanager/SERVER_CONTENT_WORKSHOP_PHASE1.md new file mode 100644 index 00000000..3068e84d --- /dev/null +++ b/Panel/modules/addonsmanager/SERVER_CONTENT_WORKSHOP_PHASE1.md @@ -0,0 +1,52 @@ +# Server Content Workshop – Phase 1 + +Phase 1 adds manual Workshop ID support inside the existing `addonsmanager` module (user-facing label: **Server Content**). + +## Scope (Phase 1) + +- No Steam Workshop browser/search UI yet. +- No Steam scraping. +- User enters comma-separated numeric Workshop IDs. +- Panel validates IDs, removes duplicates, and stores them per server home. +- Panel lists saved IDs and supports: + - Install New + - Update Selected + - Remove Selected + - Update All +- Panel generates a per-server manifest at: + - `%home_path%/gsp_server_content/workshop_manifest.json` +- Panel runs an approved script path (safe default or game-specific config), never user-supplied command/path. + +## Security model + +- Ownership check: non-admin users can only access homes assigned to them; admins can access any home. +- Actions are scoped to one `home_id`. +- IDs must be numeric only. +- Script path is not user-editable. +- Manifest path is validated to remain under server home. +- Remove is non-destructive in the generic scripts (preserve/move behavior for Phase 1). +- All actions are logged through panel logging. + +## Database + +Phase 1 introduces: + +- `OGP_DB_PREFIXserver_content_workshop` + +and keeps `OGP_DB_PREFIXaddons.addon_type` at `VARCHAR(32)` so `workshop` is valid. + +## Game/admin config TODO (next phase hardening) + +Each game should define and document: + +- `workshop_app_id` +- Linux workshop script path +- Windows/Cygwin workshop script path +- target install location +- restart/update behavior + +## Phase 2 (not included here) + +- Workshop browsing/search/select UI +- richer metadata/title lookups +- per-game install adapters and deeper status reporting diff --git a/Panel/modules/addonsmanager/addons_installer.php b/Panel/modules/addonsmanager/addons_installer.php index 826adf7b..7e86b645 100644 --- a/Panel/modules/addonsmanager/addons_installer.php +++ b/Panel/modules/addonsmanager/addons_installer.php @@ -111,6 +111,7 @@ 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'); function exec_ogp_module() { @@ -325,6 +326,11 @@ function exec_ogp_module() { return; } + if ($addon_type === 'workshop') { + scm_ensure_workshop_schema($db); + $view->refresh('?m=addonsmanager&p=workshop_content&home_id='.(int)$home_id.'&mod_id='.(int)$mod_id.'&ip='.urlencode((string)$ip).'&port='.urlencode((string)$port), 0); + return; + } ?>

diff --git a/Panel/modules/addonsmanager/module.php b/Panel/modules/addonsmanager/module.php index 8da16b21..8b625779 100644 --- a/Panel/modules/addonsmanager/module.php +++ b/Panel/modules/addonsmanager/module.php @@ -18,8 +18,8 @@ // Module general information $module_title = "Server Content Manager"; -$module_version = "2.0"; -$db_version = 2; +$module_version = "2.1"; +$db_version = 3; $module_required = TRUE; $module_menus = array( array( 'subpage' => 'addons_manager', 'name' => 'Server Content Manager', 'group' => 'admin' ) @@ -48,4 +48,28 @@ $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;" +); ?> diff --git a/Panel/modules/addonsmanager/navigation.xml b/Panel/modules/addonsmanager/navigation.xml index 9e75f1d3..da0b4cc5 100644 --- a/Panel/modules/addonsmanager/navigation.xml +++ b/Panel/modules/addonsmanager/navigation.xml @@ -2,4 +2,5 @@ + diff --git a/Panel/modules/addonsmanager/scripts/workshop/generic_steam_workshop_linux.sh b/Panel/modules/addonsmanager/scripts/workshop/generic_steam_workshop_linux.sh new file mode 100644 index 00000000..ab6b5271 --- /dev/null +++ b/Panel/modules/addonsmanager/scripts/workshop/generic_steam_workshop_linux.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +set -euo pipefail + +MANIFEST_PATH="${1:-}" +if [[ -z "$MANIFEST_PATH" ]]; then + echo "Usage: $0 " + exit 1 +fi + +if [[ ! -f "$MANIFEST_PATH" ]]; then + echo "Manifest not found: $MANIFEST_PATH" + exit 1 +fi + +MANIFEST_DIR="$(dirname "$MANIFEST_PATH")" +WORKSHOP_DIR="${MANIFEST_DIR}/workshop" +REMOVED_DIR="${WORKSHOP_DIR}/removed" +LOG_FILE="${MANIFEST_DIR}/workshop_phase1.log" + +mkdir -p "$WORKSHOP_DIR" "$REMOVED_DIR" + +ACTION="$(python3 - <<'PY' "$MANIFEST_PATH" +import json,sys +with open(sys.argv[1], "r", encoding="utf-8") as f: + data=json.load(f) +print(data.get("action","")) +PY +)" + +ITEMS="$(python3 - <<'PY' "$MANIFEST_PATH" +import json,sys +with open(sys.argv[1], "r", encoding="utf-8") as f: + data=json.load(f) +items=data.get("items",[]) +print(",".join(str(x) for x in items if str(x).isdigit())) +PY +)" + +{ + echo "[$(date '+%Y-%m-%d %H:%M:%S')] workshop_phase1 action=${ACTION} manifest=${MANIFEST_PATH}" + echo "[$(date '+%Y-%m-%d %H:%M:%S')] workshop_phase1 items=${ITEMS}" +} >> "$LOG_FILE" + +case "$ACTION" in + install|update) + # TODO: Replace with game-specific SteamCMD workshop install/update logic. + # Example flow: + # 1) Use workshop_app_id + item IDs from the manifest. + # 2) Download/refresh content into a controlled staging folder. + # 3) Copy/sync approved files into the game server content path. + ;; + remove) + # Phase 1 safety behavior: avoid destructive delete. + # TODO: move/disable per-item content using game-specific mapping rules. + echo "[$(date '+%Y-%m-%d %H:%M:%S')] remove requested; preserving files (non-destructive phase 1)." >> "$LOG_FILE" + ;; + *) + echo "Unknown workshop action: ${ACTION}" >> "$LOG_FILE" + exit 1 + ;; +esac + +exit 0 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 new file mode 100644 index 00000000..c9b419ac --- /dev/null +++ b/Panel/modules/addonsmanager/scripts/workshop/generic_steam_workshop_windows_cygwin.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +set -euo pipefail + +MANIFEST_PATH="${1:-}" +if [[ -z "$MANIFEST_PATH" ]]; then + echo "Usage: $0 " + exit 1 +fi + +if [[ ! -f "$MANIFEST_PATH" ]]; then + echo "Manifest not found: $MANIFEST_PATH" + exit 1 +fi + +MANIFEST_DIR="$(dirname "$MANIFEST_PATH")" +WORKSHOP_DIR="${MANIFEST_DIR}/workshop" +REMOVED_DIR="${WORKSHOP_DIR}/removed" +LOG_FILE="${MANIFEST_DIR}/workshop_phase1_windows.log" + +mkdir -p "$WORKSHOP_DIR" "$REMOVED_DIR" + +ACTION="$(python3 - <<'PY' "$MANIFEST_PATH" +import json,sys +with open(sys.argv[1], "r", encoding="utf-8") as f: + data=json.load(f) +print(data.get("action","")) +PY +)" + +ITEMS="$(python3 - <<'PY' "$MANIFEST_PATH" +import json,sys +with open(sys.argv[1], "r", encoding="utf-8") as f: + data=json.load(f) +items=data.get("items",[]) +print(",".join(str(x) for x in items if str(x).isdigit())) +PY +)" + +{ + echo "[$(date '+%Y-%m-%d %H:%M:%S')] workshop_phase1_windows action=${ACTION} manifest=${MANIFEST_PATH}" + echo "[$(date '+%Y-%m-%d %H:%M:%S')] workshop_phase1_windows items=${ITEMS}" +} >> "$LOG_FILE" + +case "$ACTION" in + install|update) + # TODO: Replace with game-specific SteamCMD workshop install/update logic for Cygwin environments. + ;; + remove) + # Phase 1 safety behavior: avoid destructive delete. + echo "[$(date '+%Y-%m-%d %H:%M:%S')] remove requested; preserving files (non-destructive phase 1)." >> "$LOG_FILE" + ;; + *) + echo "Unknown workshop action: ${ACTION}" >> "$LOG_FILE" + exit 1 + ;; +esac + +exit 0 diff --git a/Panel/modules/addonsmanager/server_content_helpers.php b/Panel/modules/addonsmanager/server_content_helpers.php new file mode 100644 index 00000000..cb5c1d38 --- /dev/null +++ b/Panel/modules/addonsmanager/server_content_helpers.php @@ -0,0 +1,200 @@ +query("ALTER TABLE `".OGP_DB_PREFIX."addons` MODIFY `addon_type` VARCHAR(32) NOT NULL"); + return (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_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" + ); +} + +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 created_at DESC, workshop_item_id ASC" + ); + return is_array($rows) ? $rows : array(); +} + +function scm_parse_workshop_ids($raw, &$invalid = array()) +{ + $invalid = array(); + $ids = array(); + $parts = explode(',', (string)$raw); + foreach ((array)$parts as $part) { + $value = trim((string)$part); + if ($value === '') { + continue; + } + if (!preg_match('/^[0-9]+$/', $value)) { + $invalid[] = $value; + continue; + } + $ids[$value] = $value; + } + 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) +{ + $candidates = array( + 'workshop_app_id', + 'workshop_appid', + 'steam_workshop_app_id', + 'steam_workshop_appid', + ); + foreach ((array)$candidates as $candidate) { + if (isset($server_xml->$candidate)) { + $value = trim((string)$server_xml->$candidate); + if ($value !== '' && preg_match('/^[0-9]+$/', $value)) { + return $value; + } + } + } + return ""; +} + +function scm_get_workshop_script_path(array $home_info, $server_xml) +{ + $key = scm_is_windows_home($home_info) ? 'workshop_script_windows' : 'workshop_script_linux'; + if (isset($server_xml->$key)) { + $xml_path = trim((string)$server_xml->$key); + if ($xml_path !== '' && preg_match('/^[^\\r\\n\\0]+$/', $xml_path)) { + return $xml_path; + } + } + return scm_is_windows_home($home_info) ? SCM_WORKSHOP_SCRIPT_WINDOWS_DEFAULT : SCM_WORKSHOP_SCRIPT_LINUX_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); +} + diff --git a/Panel/modules/addonsmanager/user_addons.php b/Panel/modules/addonsmanager/user_addons.php index d5c84bbf..77b1c2a6 100644 --- a/Panel/modules/addonsmanager/user_addons.php +++ b/Panel/modules/addonsmanager/user_addons.php @@ -17,6 +17,7 @@ // Central category map — load so we can iterate all types dynamically. require_once(dirname(__FILE__) . '/server_content_categories.php'); +require_once(dirname(__FILE__) . '/server_content_helpers.php'); function exec_ogp_module() { global $db; @@ -44,8 +45,9 @@ function exec_ogp_module() { } if ($home_info) { + scm_ensure_workshop_schema($db); $home_cfg_id = $home_info['home_cfg_id']; - echo "

".get_lang('user_addons').": ".htmlentities($home_info['home_name'])."

\n". + echo "

Server Content: ".htmlentities($home_info['home_name'])."

\n". "\n". "\n"; @@ -58,6 +60,24 @@ function exec_ogp_module() { foreach ((array)$categories as $type_key => $type_label) { + if ($type_key === 'workshop') + { + $workshop_count = scm_get_workshop_saved_count($db, (int)$home_id); + if ($printed_any_cell) + echo "
\n"; + else + echo "\n"; + $printed_any_cell = true; + echo "" . + "Workshop Content (" . (int)$workshop_count . ")" . + "\n"; + continue; + } + $items = $db->resultQuery( "SELECT DISTINCT addon_id, name, game_name " . "FROM OGP_DB_PREFIXaddons " . diff --git a/Panel/modules/addonsmanager/workshop_action.php b/Panel/modules/addonsmanager/workshop_action.php new file mode 100644 index 00000000..5a865957 --- /dev/null +++ b/Panel/modules/addonsmanager/workshop_action.php @@ -0,0 +1,277 @@ +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_write_manifest_and_run($db, array $home_info, $server_xml, $action, array $item_ids, &$error = '') +{ + $error = ''; + 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; + } + + $script_path = scm_get_workshop_script_path($home_info, $server_xml); + $script_path = trim((string)$script_path); + if ($script_path === '' || !preg_match('/^[^\\r\\n\\0]+$/', $script_path)) { + $error = 'Workshop script path is invalid.'; + 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); + $manifest = array( + 'action' => (string)$action, + 'home_id' => (int)$home_info['home_id'], + 'home_cfg_id' => (int)$home_info['home_cfg_id'], + 'workshop_app_id' => scm_extract_workshop_app_id($server_xml), + 'items' => array_values($item_ids), + ); + $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; + } + if ((int)$remote->rfile_exists($script_path) !== 1) { + $error = 'Configured workshop script not found on agent host: ' . $script_path; + 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; + } + $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) +{ + $message = ''; + $is_error = true; + if (!scm_ensure_workshop_schema($db)) { + $message = 'Workshop schema migration failed.'; + return false; + } + + $home_id = (int)$home_info['home_id']; + $user_id = (int)$user_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 ($action === 'install_new') { + $invalid = array(); + $item_ids = scm_parse_workshop_ids($raw_ids, $invalid); + if (!empty($invalid)) { + $message = 'Invalid Workshop IDs: ' . implode(', ', $invalid); + return false; + } + if (empty($item_ids)) { + $message = 'Enter at least one numeric Workshop ID.'; + return false; + } + + foreach ($item_ids as $item_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_state, created_by, created_at, updated_at) + VALUES ( + ".$home_id.", + ".(int)$home_info['home_cfg_id'].", + ".(int)$home_info['remote_server_id'].", + '".$db->realEscapeSingle(scm_extract_workshop_app_id($server_xml))."', + '".$db->realEscapeSingle($item_id)."', + 'selected', + ".$user_id.", + NOW(), + NOW() + ) + 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_state='selected', + last_error=NULL, + updated_at=NOW()"; + $db->query($query); + } + + scm_workshop_update_rows_state($db, $home_id, $item_ids, 'installing', null, false, false); + $error = ''; + $ok = scm_workshop_write_manifest_and_run($db, $home_info, $server_xml, 'install', $item_ids, $error); + if ($ok) { + scm_workshop_update_rows_state($db, $home_id, $item_ids, 'installed', null, true, true); + scm_workshop_log_action($db, $home_id, $user_id, "install_new ids=".implode(',', $item_ids)." status=success"); + $is_error = false; + $message = 'Workshop IDs installed successfully.'; + 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)." status=failed error=".$error); + $message = $error; + return false; + } + + if ($action === 'update_selected' || $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.'; + return false; + } + $target_action = ($action === 'remove_selected') ? 'remove' : 'update'; + scm_workshop_update_rows_state($db, $home_id, $item_ids, 'installing', null, false, false); + $error = ''; + $ok = scm_workshop_write_manifest_and_run($db, $home_info, $server_xml, $target_action, $item_ids, $error); + if ($ok) { + if ($target_action === 'remove') { + scm_workshop_update_rows_state($db, $home_id, $item_ids, 'removed', null, false, true); + } else { + scm_workshop_update_rows_state($db, $home_id, $item_ids, 'installed', null, false, true); + } + scm_workshop_log_action($db, $home_id, $user_id, $action." ids=".implode(',', $item_ids)." status=success"); + $is_error = false; + $message = ($target_action === 'remove') ? 'Selected Workshop IDs marked removed.' : 'Selected Workshop IDs updated successfully.'; + 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)." 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; + } + scm_workshop_update_rows_state($db, $home_id, $item_ids, 'installing', null, false, false); + $error = ''; + $ok = scm_workshop_write_manifest_and_run($db, $home_info, $server_xml, 'update', $item_ids, $error); + if ($ok) { + scm_workshop_update_rows_state($db, $home_id, $item_ids, 'installed', null, false, true); + scm_workshop_log_action($db, $home_id, $user_id, "update_all ids=".implode(',', $item_ids)." status=success"); + $is_error = false; + $message = 'All saved Workshop IDs updated successfully.'; + 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)." status=failed error=".$error); + $message = $error; + return false; + } + + $message = 'Invalid workshop action.'; + return false; +} + diff --git a/Panel/modules/addonsmanager/workshop_content.php b/Panel/modules/addonsmanager/workshop_content.php new file mode 100644 index 00000000..b5f915a3 --- /dev/null +++ b/Panel/modules/addonsmanager/workshop_content.php @@ -0,0 +1,148 @@ +Workshop Content: ".scm_h($home_info['home_name']).""; + if ($message !== '') { + if ($is_error) { + print_failure($message); + } else { + print_success($message); + } + } + ?> + + + +
Server Name:
Game Name:
+ +
+ + + + + + + + + + + + + + +
Enter Workshop IDs + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Workshop IDTitleStateLast InstalledLast UpdatedLast Error
No Workshop IDs saved for this server yet.
'>
+
+ + + + + + +
+
+ +
+ + + + + + + +
+