Add Phase 1 Workshop Content flow to addonsmanager

Agent-Logs-Url: https://github.com/GameServerPanel/GSP/sessions/7643e55f-473c-4084-baa0-cf8ae8c9a10a

Co-authored-by: iaretechnician <2749183+iaretechnician@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2026-05-18 21:40:24 +00:00 committed by GitHub
parent 5580a3e787
commit 7a80812fe7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 854 additions and 3 deletions

View file

@ -1,6 +1,7 @@
# Changelog
## 2026-05-18
- **Server Content Workshop Phase 1 in addonsmanager:** Added a new `Workshop Content` flow under Server Content with per-home Workshop ID storage, ID validation/deduplication, install/update/remove/update-all actions, manifest-based script handoff (`gsp_server_content/workshop_manifest.json`), safe placeholder workshop scripts for Linux/Cygwin, and schema support via `server_content_workshop` plus `addons.addon_type VARCHAR(32)`.
- **Updater layout hardening + pre-update patch framework:** Reworked `modules/administration/panel_update.php` to resolve explicit GSP root/Panel/Website paths, run mandatory preflight checks, self-update updater files before main sync when drift is detected, and apply ordered required patches from `modules/update/patches/` with DB/local state tracking. Backup/rollback now includes both Panel + Website archives and root `version.json`, logs moved to root `logs/update_trace.log`, and the admin Update UI now exposes preflight, patch apply, Apache path scan/fix, backup, update, and rollback actions.
- **Billing runtime relocation + portable path bootstrap:** Re-homed storefront runtime to `Panel/modules/billing`, added portable runtime helpers (`billing_bootstrap.php`, `site_config.php`, `site_config.example.php`) with env/local override support for base path and panel path, normalized critical storefront redirects/links to computed billing URLs, and added `Website` compatibility wrappers for key billing entrypoints.
- **Panel updater panel-subtree safety:** Hardened updater logic to treat repository `/panel` as the update source when present (ZIP + git flows) so root-level docs/examples/scripts are no longer candidates for panel file overwrite during updates.

View file

@ -17,3 +17,4 @@
- Add a panel settings health check that validates reCAPTCHA site/secret keys against active panel/storefront domains and warns admins before registration users see widget errors.
- Add an automated deployment check that fails when `Website/timestamp.txt` and `modules/billing/timestamp.txt` diverge after storefront/content changes.
- Add an admin preview/diff panel for Apache path repairs so staff can review exact vhost line changes before confirming `Fix Apache Paths`.
- Add Phase 2 Workshop Content UX in `addonsmanager`: browse/search/select Workshop items with metadata while reusing the Phase 1 per-home saved-ID action pipeline.

View file

@ -0,0 +1,52 @@
# Server Content Workshop Phase 1
Phase 1 adds manual Workshop ID support inside the existing `addonsmanager` module (user-facing label: **Server Content**).
## Scope (Phase 1)
- No Steam Workshop browser/search UI yet.
- No Steam scraping.
- User enters comma-separated numeric Workshop IDs.
- Panel validates IDs, removes duplicates, and stores them per server home.
- Panel lists saved IDs and supports:
- Install New
- Update Selected
- Remove Selected
- Update All
- Panel generates a per-server manifest at:
- `%home_path%/gsp_server_content/workshop_manifest.json`
- Panel runs an approved script path (safe default or game-specific config), never user-supplied command/path.
## Security model
- Ownership check: non-admin users can only access homes assigned to them; admins can access any home.
- Actions are scoped to one `home_id`.
- IDs must be numeric only.
- Script path is not user-editable.
- Manifest path is validated to remain under server home.
- Remove is non-destructive in the generic scripts (preserve/move behavior for Phase 1).
- All actions are logged through panel logging.
## Database
Phase 1 introduces:
- `OGP_DB_PREFIXserver_content_workshop`
and keeps `OGP_DB_PREFIXaddons.addon_type` at `VARCHAR(32)` so `workshop` is valid.
## Game/admin config TODO (next phase hardening)
Each game should define and document:
- `workshop_app_id`
- Linux workshop script path
- Windows/Cygwin workshop script path
- target install location
- restart/update behavior
## Phase 2 (not included here)
- Workshop browsing/search/select UI
- richer metadata/title lookups
- per-game install adapters and deeper status reporting

View file

@ -111,6 +111,7 @@ require_once("modules/config_games/server_config_parser.php");
require_once("protocol/lgsl/lgsl_protocol.php");
// Central category map — all valid addon_type values and their labels.
require_once(dirname(__FILE__) . '/server_content_categories.php');
require_once(dirname(__FILE__) . '/server_content_helpers.php');
function exec_ogp_module() {
@ -325,6 +326,11 @@ function exec_ogp_module() {
return;
}
if ($addon_type === 'workshop') {
scm_ensure_workshop_schema($db);
$view->refresh('?m=addonsmanager&p=workshop_content&home_id='.(int)$home_id.'&mod_id='.(int)$mod_id.'&ip='.urlencode((string)$ip).'&port='.urlencode((string)$port), 0);
return;
}
?>
<h2><?php echo htmlentities($home_info['home_name'])."&nbsp;".get_lang($addon_type) ;?></h2>

View file

@ -18,8 +18,8 @@
// Module general information
$module_title = "Server Content Manager";
$module_version = "2.0";
$db_version = 2;
$module_version = "2.1";
$db_version = 3;
$module_required = TRUE;
$module_menus = array(
array( 'subpage' => 'addons_manager', 'name' => 'Server Content Manager', 'group' => 'admin' )
@ -48,4 +48,28 @@ $install_queries[1] = array(
"ALTER TABLE `".OGP_DB_PREFIX."addons`
MODIFY `addon_type` VARCHAR(32) NOT NULL;"
);
// ── db_version 3 : workshop item selections per server home ───────────────────
$install_queries[2] = array(
"CREATE TABLE IF NOT EXISTS `".OGP_DB_PREFIX."server_content_workshop` (
`id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
`home_id` INT NOT NULL,
`home_cfg_id` INT NOT NULL,
`remote_server_id` INT NULL,
`workshop_app_id` VARCHAR(32) NULL,
`workshop_item_id` VARCHAR(64) NOT NULL,
`title` VARCHAR(255) NULL,
`install_state` VARCHAR(32) NOT NULL DEFAULT 'selected',
`last_installed_at` DATETIME NULL,
`last_updated_at` DATETIME NULL,
`last_error` TEXT NULL,
`created_by` INT NULL,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NULL,
UNIQUE KEY `uniq_home_workshop_item` (`home_id`, `workshop_item_id`),
KEY `idx_home_id` (`home_id`),
KEY `idx_home_cfg_id` (`home_cfg_id`),
KEY `idx_install_state` (`install_state`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;"
);
?>

View file

@ -2,4 +2,5 @@
<page key="user_addons" file="user_addons.php" access="admin,user" />
<page key="addons_manager" file="addons_manager.php" access="admin" />
<page key="addons" file="addons_installer.php" access="admin,user" />
<page key="workshop_content" file="workshop_content.php" access="admin,user" />
</navigation>

View file

@ -0,0 +1,63 @@
#!/usr/bin/env bash
set -euo pipefail
MANIFEST_PATH="${1:-}"
if [[ -z "$MANIFEST_PATH" ]]; then
echo "Usage: $0 <manifest_path>"
exit 1
fi
if [[ ! -f "$MANIFEST_PATH" ]]; then
echo "Manifest not found: $MANIFEST_PATH"
exit 1
fi
MANIFEST_DIR="$(dirname "$MANIFEST_PATH")"
WORKSHOP_DIR="${MANIFEST_DIR}/workshop"
REMOVED_DIR="${WORKSHOP_DIR}/removed"
LOG_FILE="${MANIFEST_DIR}/workshop_phase1.log"
mkdir -p "$WORKSHOP_DIR" "$REMOVED_DIR"
ACTION="$(python3 - <<'PY' "$MANIFEST_PATH"
import json,sys
with open(sys.argv[1], "r", encoding="utf-8") as f:
data=json.load(f)
print(data.get("action",""))
PY
)"
ITEMS="$(python3 - <<'PY' "$MANIFEST_PATH"
import json,sys
with open(sys.argv[1], "r", encoding="utf-8") as f:
data=json.load(f)
items=data.get("items",[])
print(",".join(str(x) for x in items if str(x).isdigit()))
PY
)"
{
echo "[$(date '+%Y-%m-%d %H:%M:%S')] workshop_phase1 action=${ACTION} manifest=${MANIFEST_PATH}"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] workshop_phase1 items=${ITEMS}"
} >> "$LOG_FILE"
case "$ACTION" in
install|update)
# TODO: Replace with game-specific SteamCMD workshop install/update logic.
# Example flow:
# 1) Use workshop_app_id + item IDs from the manifest.
# 2) Download/refresh content into a controlled staging folder.
# 3) Copy/sync approved files into the game server content path.
;;
remove)
# Phase 1 safety behavior: avoid destructive delete.
# TODO: move/disable per-item content using game-specific mapping rules.
echo "[$(date '+%Y-%m-%d %H:%M:%S')] remove requested; preserving files (non-destructive phase 1)." >> "$LOG_FILE"
;;
*)
echo "Unknown workshop action: ${ACTION}" >> "$LOG_FILE"
exit 1
;;
esac
exit 0

View file

@ -0,0 +1,58 @@
#!/usr/bin/env bash
set -euo pipefail
MANIFEST_PATH="${1:-}"
if [[ -z "$MANIFEST_PATH" ]]; then
echo "Usage: $0 <manifest_path>"
exit 1
fi
if [[ ! -f "$MANIFEST_PATH" ]]; then
echo "Manifest not found: $MANIFEST_PATH"
exit 1
fi
MANIFEST_DIR="$(dirname "$MANIFEST_PATH")"
WORKSHOP_DIR="${MANIFEST_DIR}/workshop"
REMOVED_DIR="${WORKSHOP_DIR}/removed"
LOG_FILE="${MANIFEST_DIR}/workshop_phase1_windows.log"
mkdir -p "$WORKSHOP_DIR" "$REMOVED_DIR"
ACTION="$(python3 - <<'PY' "$MANIFEST_PATH"
import json,sys
with open(sys.argv[1], "r", encoding="utf-8") as f:
data=json.load(f)
print(data.get("action",""))
PY
)"
ITEMS="$(python3 - <<'PY' "$MANIFEST_PATH"
import json,sys
with open(sys.argv[1], "r", encoding="utf-8") as f:
data=json.load(f)
items=data.get("items",[])
print(",".join(str(x) for x in items if str(x).isdigit()))
PY
)"
{
echo "[$(date '+%Y-%m-%d %H:%M:%S')] workshop_phase1_windows action=${ACTION} manifest=${MANIFEST_PATH}"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] workshop_phase1_windows items=${ITEMS}"
} >> "$LOG_FILE"
case "$ACTION" in
install|update)
# TODO: Replace with game-specific SteamCMD workshop install/update logic for Cygwin environments.
;;
remove)
# Phase 1 safety behavior: avoid destructive delete.
echo "[$(date '+%Y-%m-%d %H:%M:%S')] remove requested; preserving files (non-destructive phase 1)." >> "$LOG_FILE"
;;
*)
echo "Unknown workshop action: ${ACTION}" >> "$LOG_FILE"
exit 1
;;
esac
exit 0

View file

@ -0,0 +1,200 @@
<?php
/*
*
* GSP - Server Content helpers (addonsmanager)
*
*/
if (!defined('SCM_WORKSHOP_SCRIPT_LINUX_DEFAULT')) {
define('SCM_WORKSHOP_SCRIPT_LINUX_DEFAULT', '/home/gameserver/OGP_User_Files/modules/addonsmanager/scripts/workshop/generic_steam_workshop_linux.sh');
}
if (!defined('SCM_WORKSHOP_SCRIPT_WINDOWS_DEFAULT')) {
define('SCM_WORKSHOP_SCRIPT_WINDOWS_DEFAULT', '/home/gameserver/OGP_User_Files/modules/addonsmanager/scripts/workshop/generic_steam_workshop_windows_cygwin.sh');
}
function scm_ensure_workshop_schema($db)
{
static $schema_checked = false;
if ($schema_checked) {
return true;
}
$schema_checked = true;
$db->query("ALTER TABLE `".OGP_DB_PREFIX."addons` MODIFY `addon_type` VARCHAR(32) NOT NULL");
return (bool)$db->query(
"CREATE TABLE IF NOT EXISTS `".OGP_DB_PREFIX."server_content_workshop` (
`id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
`home_id` INT NOT NULL,
`home_cfg_id` INT NOT NULL,
`remote_server_id` INT NULL,
`workshop_app_id` VARCHAR(32) NULL,
`workshop_item_id` VARCHAR(64) NOT NULL,
`title` VARCHAR(255) NULL,
`install_state` VARCHAR(32) NOT NULL DEFAULT 'selected',
`last_installed_at` DATETIME NULL,
`last_updated_at` DATETIME NULL,
`last_error` TEXT NULL,
`created_by` INT NULL,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NULL,
UNIQUE KEY `uniq_home_workshop_item` (`home_id`, `workshop_item_id`),
KEY `idx_home_id` (`home_id`),
KEY `idx_home_cfg_id` (`home_cfg_id`),
KEY `idx_install_state` (`install_state`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
);
}
function scm_get_home_for_user($db, $home_id, $user_id)
{
$home_id = (int)$home_id;
$user_id = (int)$user_id;
if ($home_id <= 0 || $user_id <= 0) {
return false;
}
if ($db->isAdmin($user_id)) {
return $db->getGameHome($home_id);
}
return $db->getUserGameHome($user_id, $home_id);
}
function scm_get_workshop_saved_count($db, $home_id)
{
$home_id = (int)$home_id;
if ($home_id <= 0 || !scm_ensure_workshop_schema($db)) {
return 0;
}
$rows = $db->resultQuery(
"SELECT COUNT(*) AS cnt FROM `".OGP_DB_PREFIX."server_content_workshop` WHERE home_id=".$home_id." AND install_state<>'removed'"
);
if (!is_array($rows) || !isset($rows[0]['cnt'])) {
return 0;
}
return (int)$rows[0]['cnt'];
}
function scm_get_workshop_rows($db, $home_id)
{
$home_id = (int)$home_id;
if ($home_id <= 0 || !scm_ensure_workshop_schema($db)) {
return array();
}
$rows = $db->resultQuery(
"SELECT * FROM `".OGP_DB_PREFIX."server_content_workshop` WHERE home_id=".$home_id." ORDER BY created_at DESC, workshop_item_id ASC"
);
return is_array($rows) ? $rows : array();
}
function scm_parse_workshop_ids($raw, &$invalid = array())
{
$invalid = array();
$ids = array();
$parts = explode(',', (string)$raw);
foreach ((array)$parts as $part) {
$value = trim((string)$part);
if ($value === '') {
continue;
}
if (!preg_match('/^[0-9]+$/', $value)) {
$invalid[] = $value;
continue;
}
$ids[$value] = $value;
}
return array_values($ids);
}
function scm_parse_selected_workshop_ids($selected)
{
$ids = array();
if (!is_array($selected)) {
return $ids;
}
foreach ($selected as $item_id) {
$item_id = trim((string)$item_id);
if ($item_id !== '' && preg_match('/^[0-9]+$/', $item_id)) {
$ids[$item_id] = $item_id;
}
}
return array_values($ids);
}
function scm_h($value)
{
return htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8');
}
function scm_is_windows_home(array $home_info)
{
$game_key = isset($home_info['game_key']) ? strtolower((string)$home_info['game_key']) : '';
$cfg_file = isset($home_info['home_cfg_file']) ? strtolower((string)$home_info['home_cfg_file']) : '';
return (strpos($game_key, 'win') !== false) || (strpos($cfg_file, 'win') !== false);
}
function scm_path_is_under_home($home_path, $candidate_path)
{
$home_path = rtrim(clean_path((string)$home_path), '/');
$candidate_path = clean_path((string)$candidate_path);
if ($home_path === '' || $candidate_path === '') {
return false;
}
return strpos($candidate_path.'/', $home_path.'/') === 0;
}
function scm_get_workshop_manifest_path(array $home_info)
{
$home_path = rtrim(clean_path((string)$home_info['home_path']), '/');
$manifest_path = clean_path($home_path . '/gsp_server_content/workshop_manifest.json');
if (!scm_path_is_under_home($home_path, $manifest_path)) {
return false;
}
return $manifest_path;
}
function scm_extract_workshop_app_id($server_xml)
{
$candidates = array(
'workshop_app_id',
'workshop_appid',
'steam_workshop_app_id',
'steam_workshop_appid',
);
foreach ((array)$candidates as $candidate) {
if (isset($server_xml->$candidate)) {
$value = trim((string)$server_xml->$candidate);
if ($value !== '' && preg_match('/^[0-9]+$/', $value)) {
return $value;
}
}
}
return "";
}
function scm_get_workshop_script_path(array $home_info, $server_xml)
{
$key = scm_is_windows_home($home_info) ? 'workshop_script_windows' : 'workshop_script_linux';
if (isset($server_xml->$key)) {
$xml_path = trim((string)$server_xml->$key);
if ($xml_path !== '' && preg_match('/^[^\\r\\n\\0]+$/', $xml_path)) {
return $xml_path;
}
}
return scm_is_windows_home($home_info) ? SCM_WORKSHOP_SCRIPT_WINDOWS_DEFAULT : SCM_WORKSHOP_SCRIPT_LINUX_DEFAULT;
}
function scm_get_csrf_token()
{
if (empty($_SESSION['addonsmanager_workshop_csrf'])) {
$_SESSION['addonsmanager_workshop_csrf'] = md5(uniqid((string)mt_rand(), true));
}
return $_SESSION['addonsmanager_workshop_csrf'];
}
function scm_validate_csrf_token($token)
{
if (!isset($_SESSION['addonsmanager_workshop_csrf'])) {
return false;
}
return hash_equals((string)$_SESSION['addonsmanager_workshop_csrf'], (string)$token);
}

View file

@ -17,6 +17,7 @@
// Central category map — load so we can iterate all types dynamically.
require_once(dirname(__FILE__) . '/server_content_categories.php');
require_once(dirname(__FILE__) . '/server_content_helpers.php');
function exec_ogp_module() {
global $db;
@ -44,8 +45,9 @@ function exec_ogp_module() {
}
if ($home_info)
{
scm_ensure_workshop_schema($db);
$home_cfg_id = $home_info['home_cfg_id'];
echo "<h2>".get_lang('user_addons').": ".htmlentities($home_info['home_name'])."</h2>\n".
echo "<h2>Server Content: ".htmlentities($home_info['home_name'])."</h2>\n".
"<table class='center' >\n".
"<tr>\n";
@ -58,6 +60,24 @@ function exec_ogp_module() {
foreach ((array)$categories as $type_key => $type_label)
{
if ($type_key === 'workshop')
{
$workshop_count = scm_get_workshop_saved_count($db, (int)$home_id);
if ($printed_any_cell)
echo "</td><td>\n";
else
echo "<td>\n";
$printed_any_cell = true;
echo "<a href='?m=addonsmanager&amp;p=workshop_content" .
"&amp;home_id=" . (int)$home_id .
"&amp;mod_id=" . (int)$mod_id .
"&amp;ip=" . htmlspecialchars($ip) .
"&amp;port=" . htmlspecialchars($port) . "'>" .
"Workshop Content (" . (int)$workshop_count . ")" .
"</a>\n";
continue;
}
$items = $db->resultQuery(
"SELECT DISTINCT addon_id, name, game_name " .
"FROM OGP_DB_PREFIXaddons " .

View file

@ -0,0 +1,277 @@
<?php
/*
*
* GSP - Workshop Content actions (Phase 1)
*
*/
require_once("includes/lib_remote.php");
require_once("modules/config_games/server_config_parser.php");
require_once(dirname(__FILE__) . '/server_content_helpers.php');
function scm_workshop_log_action($db, $home_id, $user_id, $message)
{
$db->logger("server_content_workshop home_id=".(int)$home_id." user_id=".(int)$user_id." ".$message);
}
function scm_workshop_update_rows_state($db, $home_id, array $item_ids, $state, $error = null, $mark_install = false, $mark_update = false)
{
if (empty($item_ids)) {
return true;
}
$escaped_ids = array();
foreach ($item_ids as $item_id) {
$escaped_ids[] = "'" . $db->realEscapeSingle((string)$item_id) . "'";
}
$set = array(
"install_state='" . $db->realEscapeSingle($state) . "'",
"updated_at=NOW()",
);
if ($mark_install) {
$set[] = "last_installed_at=NOW()";
}
if ($mark_update) {
$set[] = "last_updated_at=NOW()";
}
if ($error === null) {
$set[] = "last_error=NULL";
} else {
$set[] = "last_error='" . $db->realEscapeSingle($error) . "'";
}
$query = "UPDATE `".OGP_DB_PREFIX."server_content_workshop`
SET ".implode(", ", $set)."
WHERE home_id=".(int)$home_id." AND workshop_item_id IN (".implode(",", $escaped_ids).")";
return (bool)$db->query($query);
}
function scm_workshop_filter_existing_ids($db, $home_id, array $item_ids)
{
if (empty($item_ids)) {
return array();
}
$escaped_ids = array();
foreach ($item_ids as $item_id) {
$escaped_ids[] = "'" . $db->realEscapeSingle((string)$item_id) . "'";
}
$rows = $db->resultQuery(
"SELECT workshop_item_id FROM `".OGP_DB_PREFIX."server_content_workshop`
WHERE home_id=".(int)$home_id." AND workshop_item_id IN (".implode(",", $escaped_ids).")"
);
$allowed = array();
if (is_array($rows)) {
foreach ((array)$rows as $row) {
$allowed[(string)$row['workshop_item_id']] = (string)$row['workshop_item_id'];
}
}
return array_values($allowed);
}
function scm_workshop_write_manifest_and_run($db, array $home_info, $server_xml, $action, array $item_ids, &$error = '')
{
$error = '';
if (empty($item_ids)) {
$error = 'No Workshop IDs were selected for this action.';
return false;
}
$manifest_path = scm_get_workshop_manifest_path($home_info);
if ($manifest_path === false) {
$error = 'Manifest path validation failed for this server home.';
return false;
}
$script_path = scm_get_workshop_script_path($home_info, $server_xml);
$script_path = trim((string)$script_path);
if ($script_path === '' || !preg_match('/^[^\\r\\n\\0]+$/', $script_path)) {
$error = 'Workshop script path is invalid.';
return false;
}
$home_path = rtrim(clean_path((string)$home_info['home_path']), '/');
if (!scm_path_is_under_home($home_path, $manifest_path)) {
$error = 'Manifest path is outside of the server home.';
return false;
}
$manifest_dir = dirname($manifest_path);
$manifest = array(
'action' => (string)$action,
'home_id' => (int)$home_info['home_id'],
'home_cfg_id' => (int)$home_info['home_cfg_id'],
'workshop_app_id' => scm_extract_workshop_app_id($server_xml),
'items' => array_values($item_ids),
);
$manifest_json = json_encode($manifest);
if ($manifest_json === false) {
$error = 'Failed to encode workshop manifest JSON.';
return false;
}
$remote = new OGPRemoteLibrary(
$home_info['agent_ip'],
$home_info['agent_port'],
$home_info['encryption_key'],
$home_info['timeout']
);
$remote->exec("mkdir -p " . escapeshellarg($manifest_dir));
if ((int)$remote->remote_writefile($manifest_path, $manifest_json) !== 1) {
$error = 'Failed to write workshop manifest to remote server.';
return false;
}
if ((int)$remote->rfile_exists($script_path) !== 1) {
$error = 'Configured workshop script not found on agent host: ' . $script_path;
return false;
}
$command = "bash " . escapeshellarg($script_path) . " " . escapeshellarg($manifest_path) . " ; echo __GSP_WORKSHOP_EXIT:$?";
$output = $remote->exec($command);
if (!is_string($output) || $output === '') {
$error = 'Workshop script did not return an execution status.';
return false;
}
if (!preg_match('/__GSP_WORKSHOP_EXIT:(\d+)/', $output, $matches)) {
$error = 'Workshop script exit marker not found in output.';
return false;
}
$exit_code = (int)$matches[1];
if ($exit_code !== 0) {
$error = 'Workshop script failed (exit '.$exit_code.'): '.trim($output);
return false;
}
return true;
}
function scm_workshop_handle_action($db, array $home_info, $user_id, $action, $raw_ids, array $selected_ids, &$message, &$is_error)
{
$message = '';
$is_error = true;
if (!scm_ensure_workshop_schema($db)) {
$message = 'Workshop schema migration failed.';
return false;
}
$home_id = (int)$home_info['home_id'];
$user_id = (int)$user_id;
$server_xml = read_server_config(SERVER_CONFIG_LOCATION . "/" . $home_info['home_cfg_file']);
if ($server_xml === false) {
$message = 'Unable to read server configuration for workshop action.';
return false;
}
if ($action === 'install_new') {
$invalid = array();
$item_ids = scm_parse_workshop_ids($raw_ids, $invalid);
if (!empty($invalid)) {
$message = 'Invalid Workshop IDs: ' . implode(', ', $invalid);
return false;
}
if (empty($item_ids)) {
$message = 'Enter at least one numeric Workshop ID.';
return false;
}
foreach ($item_ids as $item_id) {
$query = "INSERT INTO `".OGP_DB_PREFIX."server_content_workshop`
(home_id, home_cfg_id, remote_server_id, workshop_app_id, workshop_item_id, install_state, created_by, created_at, updated_at)
VALUES (
".$home_id.",
".(int)$home_info['home_cfg_id'].",
".(int)$home_info['remote_server_id'].",
'".$db->realEscapeSingle(scm_extract_workshop_app_id($server_xml))."',
'".$db->realEscapeSingle($item_id)."',
'selected',
".$user_id.",
NOW(),
NOW()
)
ON DUPLICATE KEY UPDATE
home_cfg_id=VALUES(home_cfg_id),
remote_server_id=VALUES(remote_server_id),
workshop_app_id=VALUES(workshop_app_id),
install_state='selected',
last_error=NULL,
updated_at=NOW()";
$db->query($query);
}
scm_workshop_update_rows_state($db, $home_id, $item_ids, 'installing', null, false, false);
$error = '';
$ok = scm_workshop_write_manifest_and_run($db, $home_info, $server_xml, 'install', $item_ids, $error);
if ($ok) {
scm_workshop_update_rows_state($db, $home_id, $item_ids, 'installed', null, true, true);
scm_workshop_log_action($db, $home_id, $user_id, "install_new ids=".implode(',', $item_ids)." status=success");
$is_error = false;
$message = 'Workshop IDs installed successfully.';
return true;
}
scm_workshop_update_rows_state($db, $home_id, $item_ids, 'failed', $error, false, false);
scm_workshop_log_action($db, $home_id, $user_id, "install_new ids=".implode(',', $item_ids)." status=failed error=".$error);
$message = $error;
return false;
}
if ($action === 'update_selected' || $action === 'remove_selected') {
$item_ids = scm_workshop_filter_existing_ids($db, $home_id, scm_parse_selected_workshop_ids($selected_ids));
if (empty($item_ids)) {
$message = 'Select one or more saved Workshop IDs.';
return false;
}
$target_action = ($action === 'remove_selected') ? 'remove' : 'update';
scm_workshop_update_rows_state($db, $home_id, $item_ids, 'installing', null, false, false);
$error = '';
$ok = scm_workshop_write_manifest_and_run($db, $home_info, $server_xml, $target_action, $item_ids, $error);
if ($ok) {
if ($target_action === 'remove') {
scm_workshop_update_rows_state($db, $home_id, $item_ids, 'removed', null, false, true);
} else {
scm_workshop_update_rows_state($db, $home_id, $item_ids, 'installed', null, false, true);
}
scm_workshop_log_action($db, $home_id, $user_id, $action." ids=".implode(',', $item_ids)." status=success");
$is_error = false;
$message = ($target_action === 'remove') ? 'Selected Workshop IDs marked removed.' : 'Selected Workshop IDs updated successfully.';
return true;
}
scm_workshop_update_rows_state($db, $home_id, $item_ids, 'failed', $error, false, false);
scm_workshop_log_action($db, $home_id, $user_id, $action." ids=".implode(',', $item_ids)." status=failed error=".$error);
$message = $error;
return false;
}
if ($action === 'update_all') {
$rows = $db->resultQuery(
"SELECT workshop_item_id FROM `".OGP_DB_PREFIX."server_content_workshop`
WHERE home_id=".$home_id." AND install_state<>'removed'"
);
$item_ids = array();
if (is_array($rows)) {
foreach ((array)$rows as $row) {
$item_ids[] = (string)$row['workshop_item_id'];
}
}
$item_ids = scm_parse_selected_workshop_ids($item_ids);
if (empty($item_ids)) {
$message = 'No Workshop IDs are currently saved for this server.';
return false;
}
scm_workshop_update_rows_state($db, $home_id, $item_ids, 'installing', null, false, false);
$error = '';
$ok = scm_workshop_write_manifest_and_run($db, $home_info, $server_xml, 'update', $item_ids, $error);
if ($ok) {
scm_workshop_update_rows_state($db, $home_id, $item_ids, 'installed', null, false, true);
scm_workshop_log_action($db, $home_id, $user_id, "update_all ids=".implode(',', $item_ids)." status=success");
$is_error = false;
$message = 'All saved Workshop IDs updated successfully.';
return true;
}
scm_workshop_update_rows_state($db, $home_id, $item_ids, 'failed', $error, false, false);
scm_workshop_log_action($db, $home_id, $user_id, "update_all ids=".implode(',', $item_ids)." status=failed error=".$error);
$message = $error;
return false;
}
$message = 'Invalid workshop action.';
return false;
}

View file

@ -0,0 +1,148 @@
<?php
/*
*
* GSP - Server Content Workshop page (Phase 1)
*
*/
require_once(dirname(__FILE__) . '/server_content_helpers.php');
require_once(dirname(__FILE__) . '/workshop_action.php');
function exec_ogp_module() {
global $db;
$user_id = isset($_SESSION['user_id']) ? (int)$_SESSION['user_id'] : 0;
$home_id = isset($_REQUEST['home_id']) ? (int)$_REQUEST['home_id'] : 0;
$mod_id = isset($_REQUEST['mod_id']) ? (int)$_REQUEST['mod_id'] : 0;
$ip = isset($_REQUEST['ip']) ? (string)$_REQUEST['ip'] : '';
$port = isset($_REQUEST['port']) ? (string)$_REQUEST['port'] : '';
if ($home_id <= 0 || $user_id <= 0) {
print_failure(get_lang('no_rights'));
echo create_back_button("addonsmanager","user_addons");
return;
}
$home_info = scm_get_home_for_user($db, $home_id, $user_id);
if ($home_info === false) {
print_failure(get_lang('no_rights'));
echo create_back_button("addonsmanager","user_addons");
return;
}
if (!scm_ensure_workshop_schema($db)) {
print_failure('Failed to initialize Workshop Content storage.');
return;
}
$message = '';
$is_error = false;
$entered_ids = '';
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'] : '';
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);
}
}
$rows = scm_get_workshop_rows($db, $home_id);
$csrf_token = scm_get_csrf_token();
echo "<h2>Workshop Content: ".scm_h($home_info['home_name'])."</h2>";
if ($message !== '') {
if ($is_error) {
print_failure($message);
} else {
print_success($message);
}
}
?>
<table class='center'>
<tr><td align='right'><strong>Server Name:</strong></td><td align='left'><?php echo scm_h($home_info['home_name']); ?></td></tr>
<tr><td align='right'><strong>Game Name:</strong></td><td align='left'><?php echo scm_h($home_info['game_name']); ?></td></tr>
</table>
<form method='post' action=''>
<input type='hidden' name='m' value='addonsmanager' />
<input type='hidden' name='p' value='workshop_content' />
<input type='hidden' name='home_id' value='<?php echo (int)$home_id; ?>' />
<input type='hidden' name='mod_id' value='<?php echo (int)$mod_id; ?>' />
<input type='hidden' name='ip' value='<?php echo scm_h($ip); ?>' />
<input type='hidden' name='port' value='<?php echo scm_h($port); ?>' />
<input type='hidden' name='workshop_csrf' value='<?php echo scm_h($csrf_token); ?>' />
<table class='center'>
<tr>
<td align='right'><strong>Enter Workshop IDs</strong></td>
<td align='left'>
<input type='text' name='workshop_ids' size='72' value='<?php echo scm_h($entered_ids); ?>' placeholder='1234567890, 9876543210, 555555555' />
</td>
<td align='left'>
<button type='submit' name='workshop_action' value='install_new'>Install New</button>
</td>
</tr>
</table>
<br>
<table class='center'>
<tr>
<th></th>
<th>Workshop ID</th>
<th>Title</th>
<th>State</th>
<th>Last Installed</th>
<th>Last Updated</th>
<th>Last Error</th>
</tr>
<?php if (empty($rows)): ?>
<tr><td colspan='7' class='info'>No Workshop IDs saved for this server yet.</td></tr>
<?php else: ?>
<?php foreach ((array)$rows as $row): ?>
<tr>
<td><input type='checkbox' name='selected_ids[]' value='<?php echo scm_h($row['workshop_item_id']); ?>'></td>
<td><?php echo scm_h($row['workshop_item_id']); ?></td>
<td><?php echo scm_h($row['title']); ?></td>
<td><?php echo scm_h($row['install_state']); ?></td>
<td><?php echo scm_h($row['last_installed_at']); ?></td>
<td><?php echo scm_h($row['last_updated_at']); ?></td>
<td><?php echo scm_h($row['last_error']); ?></td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</table>
<br>
<table class='center'>
<tr>
<td><button type='submit' name='workshop_action' value='update_selected'>Update Selected</button></td>
<td><button type='submit' name='workshop_action' value='remove_selected'>Remove Selected</button></td>
<td><button type='submit' name='workshop_action' value='update_all'>Update All</button></td>
</tr>
</table>
</form>
<form method='get' action=''>
<input type='hidden' name='m' value='addonsmanager' />
<input type='hidden' name='p' value='user_addons' />
<input type='hidden' name='home_id' value='<?php echo (int)$home_id; ?>' />
<input type='hidden' name='mod_id' value='<?php echo (int)$mod_id; ?>' />
<input type='hidden' name='ip' value='<?php echo scm_h($ip); ?>' />
<input type='hidden' name='port' value='<?php echo scm_h($port); ?>' />
<input type='submit' value='Back to Server Content' />
</form>
<?php
}