diff --git a/Panel/modules/addonsmanager/scripts/workshop/panel_generated_steamcmd_job b/Panel/modules/addonsmanager/scripts/workshop/panel_generated_steamcmd_job new file mode 100755 index 00000000..daca4928 --- /dev/null +++ b/Panel/modules/addonsmanager/scripts/workshop/panel_generated_steamcmd_job @@ -0,0 +1,133 @@ +#!/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_string() { + local key="$1" + perl -0777 -ne ' + my $key = shift @ARGV; + if (/"\Q$key\E"\s*:\s*"([^"]*)"/s) { + my $v = $1; + $v =~ s/\\\//\//g; + print $v; + } + ' "$key" "$MANIFEST" +} + +json_items() { + perl -0777 -ne ' + if (/"items"\s*:\s*\[(.*?)\]/s) { + my $x = $1; + while ($x =~ /"([0-9]{3,20})"/g) { print "$1\n"; } + 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 55e2548f..fed5465c 100644 --- a/Panel/modules/addonsmanager/server_content_helpers.php +++ b/Panel/modules/addonsmanager/server_content_helpers.php @@ -287,6 +287,58 @@ function scm_get_workshop_catalog_rows($db, $app_id = '', $sort = 'last_installe 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)) { @@ -294,6 +346,8 @@ function scm_workshop_record_catalog_items($db, $workshop_app_id, array $item_id } $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)) { @@ -303,6 +357,22 @@ function scm_workshop_record_catalog_items($db, $workshop_app_id, array $item_id $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` diff --git a/Panel/modules/addonsmanager/server_content_helpers.php.bak.20260609-083408 b/Panel/modules/addonsmanager/server_content_helpers.php.bak.20260609-083408 new file mode 100644 index 00000000..55e2548f --- /dev/null +++ b/Panel/modules/addonsmanager/server_content_helpers.php.bak.20260609-083408 @@ -0,0 +1,1231 @@ +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_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'] : ''; + 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'] : ''; + $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/server_content_helpers.php.bak.20260609-083446 b/Panel/modules/addonsmanager/server_content_helpers.php.bak.20260609-083446 new file mode 100644 index 00000000..1dff23c4 --- /dev/null +++ b/Panel/modules/addonsmanager/server_content_helpers.php.bak.20260609-083446 @@ -0,0 +1,1292 @@ +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); + 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']; + } + } + $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 6c785ec9..3c8b3c29 100644 --- a/Panel/modules/addonsmanager/workshop_action.php +++ b/Panel/modules/addonsmanager/workshop_action.php @@ -372,7 +372,29 @@ function scm_workshop_handle_action($db, array $home_info, $user_id, $action, $r return true; } - if ($action === 'update_selected' || $action === 'remove_selected' || $action === 'download_selected') { + + 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.'; diff --git a/Panel/modules/addonsmanager/workshop_action.php.bak.20260609-070033 b/Panel/modules/addonsmanager/workshop_action.php.bak.20260609-070033 new file mode 100644 index 00000000..6c785ec9 --- /dev/null +++ b/Panel/modules/addonsmanager/workshop_action.php.bak.20260609-070033 @@ -0,0 +1,453 @@ +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); + $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' => (!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'] : '', + '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 === 'update_selected' || $action === 'remove_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; +} diff --git a/Panel/modules/addonsmanager/workshop_action.php.bak.20260609-070146 b/Panel/modules/addonsmanager/workshop_action.php.bak.20260609-070146 new file mode 100644 index 00000000..6c785ec9 --- /dev/null +++ b/Panel/modules/addonsmanager/workshop_action.php.bak.20260609-070146 @@ -0,0 +1,453 @@ +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); + $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' => (!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'] : '', + '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 === 'update_selected' || $action === 'remove_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; +} diff --git a/Panel/modules/addonsmanager/workshop_action.php.bak.20260609-071012 b/Panel/modules/addonsmanager/workshop_action.php.bak.20260609-071012 new file mode 100644 index 00000000..6ba59856 --- /dev/null +++ b/Panel/modules/addonsmanager/workshop_action.php.bak.20260609-071012 @@ -0,0 +1,377 @@ +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(); + + $error = 'Workshop install/update is currently blocked because the legacy static agent-side workshop script path is still being used. The workshop system must be migrated to Panel-generated SteamCMD job execution.'; + + return false; +} + +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 === 'update_selected' || $action === 'remove_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; +} diff --git a/Panel/modules/addonsmanager/workshop_action.php.bak.20260609-083408 b/Panel/modules/addonsmanager/workshop_action.php.bak.20260609-083408 new file mode 100644 index 00000000..6c785ec9 --- /dev/null +++ b/Panel/modules/addonsmanager/workshop_action.php.bak.20260609-083408 @@ -0,0 +1,453 @@ +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); + $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' => (!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'] : '', + '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 === 'update_selected' || $action === 'remove_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; +} diff --git a/Panel/modules/addonsmanager/workshop_action.php.bak.20260609-083446 b/Panel/modules/addonsmanager/workshop_action.php.bak.20260609-083446 new file mode 100644 index 00000000..3c8b3c29 --- /dev/null +++ b/Panel/modules/addonsmanager/workshop_action.php.bak.20260609-083446 @@ -0,0 +1,475 @@ +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); + $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' => (!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'] : '', + '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; +} diff --git a/Panel/modules/addonsmanager/workshop_content.php b/Panel/modules/addonsmanager/workshop_content.php index 53782861..ed8cad53 100644 --- a/Panel/modules/addonsmanager/workshop_content.php +++ b/Panel/modules/addonsmanager/workshop_content.php @@ -157,22 +157,11 @@ function exec_ogp_module() { -

Workshop-Enabled Games

- - - - - - - - - - - - - - -
GameConfigWorkshop App IDStrategy
No Workshop-enabled game XML files matched this 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. +

diff --git a/Panel/modules/addonsmanager/workshop_content.php.bak.20260609-070033 b/Panel/modules/addonsmanager/workshop_content.php.bak.20260609-070033 new file mode 100644 index 00000000..53782861 --- /dev/null +++ b/Panel/modules/addonsmanager/workshop_content.php.bak.20260609-070033 @@ -0,0 +1,319 @@ + 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-Enabled Games

+ + + + + + + + + + + + + + +
GameConfigWorkshop App IDStrategy
No Workshop-enabled game XML files matched this search.
+ + +
+ + + + + + + + + + + + + + + +
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;' /> + + +
+ + + + + + + + + ' /> + +
+
+ +
+ + + + + + + +
+ 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-Enabled Games

+ + + + + + + + + + + + + + +
GameConfigWorkshop App IDStrategy
No Workshop-enabled game XML files matched this search.
+ + +
+ + + + + + + + + + + + + + + +
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;' /> + + +
+ + + + + + + + + ' /> + +
+
+ +
+ + + + + + + +
+ 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-Enabled Games

+ + + + + + + + + + + + + + +
GameConfigWorkshop App IDStrategy
No Workshop-enabled game XML files matched this search.
+ + +
+ + + + + + + + + + + + + + + +
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;' /> + + +
+ + + + + + + + + ' /> + +
+
+ +
+ + + + + + + +
+ 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-Enabled Games

+ + + + + + + + + + + + + + +
GameConfigWorkshop App IDStrategy
No Workshop-enabled game XML files matched this search.
+ + +
+ + + + + + + + + + + + + + + +
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;' /> + + +
+ + + + + + + + + ' /> + +
+
+ +
+ + + + + + + +
+ 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;' /> + + +
+ + + + + + + + + ' /> + +
+
+ +
+ + + + + + + +
+