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:
parent
5580a3e787
commit
7a80812fe7
12 changed files with 854 additions and 3 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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'])." ".get_lang($addon_type) ;?></h2>
|
||||
|
|
|
|||
|
|
@ -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;"
|
||||
);
|
||||
?>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
200
Panel/modules/addonsmanager/server_content_helpers.php
Normal file
200
Panel/modules/addonsmanager/server_content_helpers.php
Normal 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);
|
||||
}
|
||||
|
||||
|
|
@ -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&p=workshop_content" .
|
||||
"&home_id=" . (int)$home_id .
|
||||
"&mod_id=" . (int)$mod_id .
|
||||
"&ip=" . htmlspecialchars($ip) .
|
||||
"&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 " .
|
||||
|
|
|
|||
277
Panel/modules/addonsmanager/workshop_action.php
Normal file
277
Panel/modules/addonsmanager/workshop_action.php
Normal 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;
|
||||
}
|
||||
|
||||
148
Panel/modules/addonsmanager/workshop_content.php
Normal file
148
Panel/modules/addonsmanager/workshop_content.php
Normal 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
|
||||
}
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue