Removed steam feom server content
This commit is contained in:
parent
93a3ff11a4
commit
8eee9385ef
14 changed files with 2817 additions and 1421 deletions
|
|
@ -113,13 +113,18 @@ 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');
|
||||
require_once(dirname(__FILE__) . '/workshop_action.php');
|
||||
if (file_exists(dirname(__FILE__) . '/../steam_workshop/includes/functions.php')) {
|
||||
require_once(dirname(__FILE__) . '/../steam_workshop/includes/functions.php');
|
||||
}
|
||||
|
||||
function exec_ogp_module() {
|
||||
|
||||
if (isset($install_method) && $install_method === 'steam_workshop') {
|
||||
print_failure('Steam Workshop has been removed from Server Content. Use the Steam Workshop module instead.');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
global $db,$view;
|
||||
$home_id = $_REQUEST['home_id'];
|
||||
$mod_id = $_REQUEST['mod_id'];
|
||||
|
|
@ -549,7 +554,7 @@ function exec_ogp_module() {
|
|||
return;
|
||||
}
|
||||
|
||||
// Workshop items are managed through the dedicated workshop_content page
|
||||
// Steam Workshop is disabled in Server Content. Use the legacy steam_workshop module.
|
||||
// where users enter their own Workshop IDs. Redirect there immediately.
|
||||
if ($addon_type === 'workshop_item') {
|
||||
$first_addon_id = 0;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,654 @@
|
|||
<script type="text/javascript" src="js/modules/addonsmanager.js"></script>
|
||||
<?php
|
||||
/*
|
||||
*
|
||||
* GSP - Game Server Panel (a heavily customized fork of OGP maintained by WDS)
|
||||
*
|
||||
* Server Content Installer (module: addonsmanager, page: addons)
|
||||
* ─────────────────────────────────────────────────────────────────────────────
|
||||
* This file handles the actual download+extraction and post-install script
|
||||
* execution for a Server Content item selected by a user.
|
||||
*
|
||||
* CURRENT FLOW:
|
||||
* 1. User selects a content type (plugin / mappack / config / ...) from
|
||||
* user_addons.php which links here with addon_type=<type>.
|
||||
* 2. User picks a specific content item from a dropdown.
|
||||
* 3. On form submit, state=start is set and start_file_download() is called
|
||||
* on the remote agent with the configured URL and target path.
|
||||
* 4. The agent downloads and extracts the archive.
|
||||
* 5. If a post_script is defined it is run on the agent after extraction.
|
||||
* 6. The page auto-refreshes (state=refresh) to show download/script progress.
|
||||
*
|
||||
* POST-INSTALL SCRIPT REPLACEMENT VARIABLES:
|
||||
* %home_path% – absolute path of the game server home directory
|
||||
* %home_name% – display name of the game server home
|
||||
* %control_password% – RCON / control password for this server instance
|
||||
* %max_players% – maximum player count configured for this mod slot
|
||||
* %ip% – IP address bound to this server instance
|
||||
* %port% – game port bound to this server instance
|
||||
* %query_port% – query/status port (derived from game XML rules)
|
||||
* %incremental% – internal incremental run counter for this mod/home
|
||||
*
|
||||
* SECURITY NOTES:
|
||||
* - Users CANNOT supply arbitrary scripts; only the admin-defined post_script
|
||||
* is executed. Users only pick from the approved list.
|
||||
* - Paths are passed to the agent which is responsible for enforcing that
|
||||
* all paths stay inside the assigned home directory.
|
||||
* - TODO (next phase): add explicit server-side path validation before
|
||||
* sending the command to the agent to block ../ traversal at the panel.
|
||||
*
|
||||
* ─── FUTURE WORK (TODO – next phase) ────────────────────────────────────────
|
||||
* The items below are intentionally NOT implemented here yet. They are
|
||||
* documented so the next contributor knows exactly where to add them.
|
||||
*
|
||||
* TODO: requires_stop flag
|
||||
* If the content item sets requires_stop=1, stop the server before
|
||||
* initiating the download. Poll is_server_running() and abort if it
|
||||
* cannot be stopped within a timeout.
|
||||
*
|
||||
* TODO: backup_before_install flag
|
||||
* If backup_before_install=1, call the agent's backup function or
|
||||
* compress the target path into a timestamped .tar.gz before extraction.
|
||||
*
|
||||
* TODO: restart_after_install flag
|
||||
* If restart_after_install=1, trigger a server start after a successful
|
||||
* install (i.e. after post_script completes with exit code 0).
|
||||
*
|
||||
* TODO: install_method field
|
||||
* Current method is always 'download_zip'. Future methods:
|
||||
* 'download_file' – single-file download, no extraction
|
||||
* 'post_script' – run only the post_script, no download
|
||||
* 'steam_workshop' – pass workshop item IDs to the agent's workshop helper
|
||||
* 'minecraft_jar' – download a Minecraft server jar + update start script
|
||||
* 'profile_copy' – copy a profile directory tree into the server home
|
||||
*
|
||||
* TODO: content_version field
|
||||
* Store the installed version tag so the UI can display "installed: 1.21.1"
|
||||
* and detect whether an update is available.
|
||||
*
|
||||
* TODO: safe script templates
|
||||
* Provide a set of admin-approved script templates so admins do not have to
|
||||
* write raw bash from scratch. Templates are stored in the DB and referenced
|
||||
* by content items.
|
||||
*
|
||||
* TODO: install history / logging
|
||||
* Write a row to a new install_history table (or log file) each time a
|
||||
* content item is installed:
|
||||
* home_id, addon_id, installed_by (user_id), installed_at, result, log_output
|
||||
*
|
||||
* TODO: user-friendly status output
|
||||
* Replace the raw progress-bar with a card-style status block showing:
|
||||
* content item name, version, download progress, script output, final status.
|
||||
*
|
||||
* TODO: Steam Workshop integration
|
||||
* When install_method='steam_workshop', pass the workshop item ID list to
|
||||
* the agent. See SERVER_CONTENT_ROADMAP.md – Part 6 for the full design.
|
||||
*
|
||||
* TODO: Minecraft jar / version switching
|
||||
* When install_method='minecraft_jar', download the jar from Mojang/Paper/
|
||||
* Purpur/Fabric API, place it at the configured server path, and patch the
|
||||
* startup command line. See SERVER_CONTENT_ROADMAP.md – Part 7.
|
||||
* ─────────────────────────────────────────────────────────────────────────────
|
||||
*/
|
||||
|
||||
function do_progress($kbytes,$totalsize)
|
||||
{
|
||||
$mbytes = round($kbytes / 1024, 2);
|
||||
|
||||
if($kbytes > 0)
|
||||
{
|
||||
$pct = round(( $kbytes / $totalsize ) * 100, 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
$pct = "-";
|
||||
}
|
||||
#echo "Percent is $pct";
|
||||
return "$totalsize;$mbytes;$pct";
|
||||
}
|
||||
|
||||
require_once("includes/lib_remote.php");
|
||||
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');
|
||||
require_once(dirname(__FILE__) . '/workshop_action.php');
|
||||
if (file_exists(dirname(__FILE__) . '/../steam_workshop/includes/functions.php')) {
|
||||
require_once(dirname(__FILE__) . '/../steam_workshop/includes/functions.php');
|
||||
}
|
||||
|
||||
function exec_ogp_module() {
|
||||
|
||||
global $db,$view;
|
||||
$home_id = $_REQUEST['home_id'];
|
||||
$mod_id = $_REQUEST['mod_id'];
|
||||
$ip = $_REQUEST['ip'];
|
||||
$port = $_REQUEST['port'];
|
||||
$user_id = $_SESSION['user_id'];
|
||||
|
||||
$isAdmin = $db->isAdmin( $_SESSION['user_id'] );
|
||||
$query_groups = "";
|
||||
if($isAdmin)
|
||||
$home_info = $db->getGameHome($home_id);
|
||||
else
|
||||
{
|
||||
$home_info = $db->getUserGameHome($user_id,$home_id);
|
||||
$groups = $db->getUsersGroups($_SESSION['user_id']);
|
||||
if (!is_array($groups)) {
|
||||
$groups = [];
|
||||
}
|
||||
$query_groups .= " AND (";
|
||||
foreach ((array)$groups as $group)
|
||||
$query_groups .= "group_id=".$group['group_id']." OR ";
|
||||
$query_groups .= "group_id=0 OR group_id IS NULL)";
|
||||
}
|
||||
|
||||
if ( $home_info === FALSE )
|
||||
{
|
||||
print_failure(get_lang('no_rights'));
|
||||
echo create_back_button("addonsmanager","user_addons");
|
||||
return;
|
||||
}
|
||||
|
||||
$home_cfg_id = $home_info['home_cfg_id'];
|
||||
$server_xml = read_server_config(SERVER_CONFIG_LOCATION."/".$home_info['home_cfg_file']);
|
||||
|
||||
// Use the full category map so newly added types are accepted without
|
||||
// editing this file. The original three types are always present.
|
||||
$addon_types = get_server_content_type_keys();
|
||||
$addon_type = isset($_REQUEST['addon_type']) ? scm_normalize_addon_type($_REQUEST['addon_type']) : "";
|
||||
|
||||
$state = isset($_REQUEST['state']) ? $_REQUEST['state'] : "";
|
||||
$pid = isset($_REQUEST['pid']) ? $_REQUEST['pid'] : -1;
|
||||
|
||||
if ( $state != "" )
|
||||
{
|
||||
$addon_id = (int)$_REQUEST['addon_id'];
|
||||
|
||||
$addons_rows = $db->resultQuery("SELECT url, path, post_script, addon_type, install_method, content_version, requires_stop, restart_after_install, workshop_item_id, workshop_app_id, target_path_template, optional_folder_name, config_edit_rule, launch_param_additions, name FROM OGP_DB_PREFIXaddons WHERE addon_id=".$addon_id.$query_groups);
|
||||
if (!is_array($addons_rows)) {
|
||||
$addons_rows = [];
|
||||
}
|
||||
|
||||
if (!$addons_rows) {
|
||||
print_failure(get_lang('invalid_addon'));
|
||||
$view->refresh('?m=addonsmanager&p=user_addons&home_id='. $home_id .'&mod_id='. $mod_id .'&ip='. $ip .'&port='.$port);
|
||||
return;
|
||||
}
|
||||
|
||||
$remote = new OGPRemoteLibrary($home_info['agent_ip'],$home_info['agent_port'],$home_info['encryption_key'],$home_info['timeout']);
|
||||
|
||||
$addon_info = $addons_rows[0];
|
||||
$install_method = scm_get_install_method_default(isset($addon_info['install_method']) ? $addon_info['install_method'] : 'download_zip');
|
||||
$content_version = isset($addon_info['content_version']) ? $addon_info['content_version'] : '';
|
||||
$requires_stop = !empty($addon_info['requires_stop']) ? 1 : 0;
|
||||
$user_override_keys = ($install_method === 'steam_workshop')
|
||||
? array('workshop_item_id')
|
||||
: array();
|
||||
$install_payload = scm_collect_install_payload($addon_info, $_REQUEST, $user_override_keys);
|
||||
$post_script = '';
|
||||
$validation_message = '';
|
||||
if ($state == "start" && !scm_validate_install_method_payload($install_method, $install_payload, $validation_message)) {
|
||||
print_failure($validation_message);
|
||||
return;
|
||||
}
|
||||
|
||||
// ── requires_stop guard ───────────────────────────────────────────────
|
||||
// If the content item requires the server to be stopped first, check
|
||||
// whether the server is currently running and block the install if so.
|
||||
// (Phase 2 blocks install; automatic stop/start is Phase 3.)
|
||||
if ( $state == "start" && $requires_stop ) {
|
||||
$is_running = $remote->is_screen_running( $home_info['home_name'], $home_info['home_id'] );
|
||||
if ( $is_running === 1 ) {
|
||||
print_failure('This content item requires the server to be stopped before installing. Please stop the server and try again.');
|
||||
echo "<p><a href=\"?m=addonsmanager&p=addons&addon_type=".urlencode($addon_info['addon_type'] ?? '')."&home_id=$home_id&mod_id=$mod_id&ip=$ip&port=$port\">".get_lang('back')."</a></p>";
|
||||
return;
|
||||
}
|
||||
}
|
||||
$url = $install_payload['url'];
|
||||
$filename = basename($url);
|
||||
#### Replace template variables in the post-install script with
|
||||
#### live server data before sending to the agent.
|
||||
#### Each variable is replaced case-insensitively.
|
||||
#### SECURITY: only admin-defined variables are substituted; users
|
||||
#### cannot inject additional commands through these fields.
|
||||
if($addon_info['post_script'] != "")
|
||||
{
|
||||
$addon_info['post_script'] = strip_real_escape_string($addon_info['post_script']);
|
||||
$check_passed = FALSE;
|
||||
$address_at_post = $ip.":".$port;
|
||||
$ip_ports = $db->getHomeIpPorts($home_info['home_id']);
|
||||
if (!is_array($ip_ports)) {
|
||||
$ip_ports = [];
|
||||
}
|
||||
foreach ((array)$ip_ports as $ip_port);
|
||||
{
|
||||
$address_owned = $ip_port['ip'].":".$ip_port['port'];
|
||||
if($address_owned == $address_at_post)
|
||||
{
|
||||
$check_passed = TRUE;
|
||||
$ip = $ip_port['ip'];
|
||||
$port = $ip_port['port'];
|
||||
}
|
||||
}
|
||||
if($check_passed)
|
||||
{
|
||||
$home_info['ip'] = $ip;
|
||||
$home_info['port'] = $port;
|
||||
|
||||
if( isset($server_xml->gameq_query_name) )
|
||||
{
|
||||
require_once("modules/gamemanager/home_handling_functions.php");
|
||||
$home_info['query_port'] = get_query_port($server_xml, $home_info['port']);
|
||||
}
|
||||
elseif( isset($server_xml->lgsl_query_name) )
|
||||
{
|
||||
$get_q_and_s = lgsl_port_conversion((string)$server_xml->lgsl_query_name, $home_info['port'], "", "");
|
||||
$home_info['query_port'] = $get_q_and_s['1'];
|
||||
}
|
||||
|
||||
$home_info["incremental"] = $db->incrementalNumByHomeId( $home_info['home_id'], $home_info['mods'][$mod_id]['mod_cfg_id'], $home_info['remote_server_id'] );
|
||||
|
||||
$post_script = preg_replace( "/\%home_path\%/i", $home_info['home_path'], $addon_info['post_script']);
|
||||
$post_script = preg_replace( "/\%home_name\%/i", $home_info['home_name'], $post_script);
|
||||
$post_script = preg_replace( "/\%control_password\%/i", $home_info['control_password'], $post_script);
|
||||
$post_script = preg_replace( "/\%max_players\%/i", $home_info['mods'][$mod_id]['max_players'], $post_script);
|
||||
$post_script = preg_replace( "/\%ip\%/i", $home_info['ip'], $post_script);
|
||||
$post_script = preg_replace( "/\%port\%/i", $home_info['port'], $post_script);
|
||||
$post_script = preg_replace( "/\%query_port\%/i", $home_info['query_port'], $post_script);
|
||||
$post_script = preg_replace( "/\%incremental\%/i", $home_info['incremental'], $post_script);
|
||||
}
|
||||
}
|
||||
|
||||
#### end of replacements
|
||||
if ( $state == "start" AND $addon_id != "" ) {
|
||||
// Record install attempt in history before triggering download.
|
||||
$cache_mode = scm_get_cache_mode($db);
|
||||
$history_id = scm_record_install_start(
|
||||
$db,
|
||||
$home_id,
|
||||
$addon_id,
|
||||
$user_id,
|
||||
$addon_info['url'],
|
||||
$content_version,
|
||||
$install_method,
|
||||
$cache_mode
|
||||
);
|
||||
$_SESSION['scm_history_id_' . $home_id . '_' . $addon_id] = $history_id;
|
||||
$log_target_path = (string)$install_payload['path'];
|
||||
if ($install_method === 'steam_workshop') {
|
||||
$log_target_path = scm_extract_workshop_install_path($server_xml);
|
||||
if ($log_target_path === '') {
|
||||
$log_target_path = scm_get_default_workshop_target_template(scm_detect_workshop_install_strategy($home_info, $server_xml));
|
||||
}
|
||||
}
|
||||
scm_log_content_install_action(array(
|
||||
'addon_id' => (int)$addon_id,
|
||||
'addon_name' => isset($addon_info['name']) ? $addon_info['name'] : '',
|
||||
'content_type' => $install_method,
|
||||
'home_id' => (int)$home_id,
|
||||
'home_cfg_id' => (int)$home_info['home_cfg_id'],
|
||||
'workshop_id' => isset($install_payload['workshop_item_id']) ? (string)$install_payload['workshop_item_id'] : '',
|
||||
'target_path' => $log_target_path,
|
||||
'action' => 'started',
|
||||
));
|
||||
if ($install_method === 'steam_workshop') {
|
||||
scm_ensure_workshop_schema($db);
|
||||
$workshop_runtime = scm_build_workshop_runtime_context($db, $home_info, $server_xml, $install_payload, $validation_message);
|
||||
if ($workshop_runtime === false) {
|
||||
print_failure($validation_message);
|
||||
echo "<p><a href=\"?m=addonsmanager&p=user_addons&home_id=$home_id&mod_id=$mod_id&ip=$ip&port=$port\">".get_lang('back')."</a></p>";
|
||||
return;
|
||||
}
|
||||
$workshop_item_id = (string)$workshop_runtime['workshop_item_id'];
|
||||
$target_path_template = (string)$workshop_runtime['target_path_template'];
|
||||
$workshop_app_id = (string)$workshop_runtime['workshop_app_id'];
|
||||
$steam_app_id = (string)$workshop_runtime['steam_app_id'];
|
||||
$target_path_resolved = (string)$workshop_runtime['target_path_resolved'];
|
||||
$extra_manifest = array(
|
||||
'addon_id' => (int)$addon_id,
|
||||
'target_path_template' => $target_path_template,
|
||||
'target_path_resolved' => $target_path_resolved,
|
||||
'config_edit_rule' => trim((string)$addon_info['config_edit_rule']),
|
||||
'launch_param_additions' => isset($server_xml->workshop_support->startup_param_format) ? trim((string)$server_xml->workshop_support->startup_param_format) : '',
|
||||
'workshop_app_id' => $workshop_app_id,
|
||||
'steam_app_id' => $steam_app_id,
|
||||
'steamcmd_path' => isset($workshop_runtime['steamcmd_path']) ? (string)$workshop_runtime['steamcmd_path'] : '',
|
||||
'workshop_download_dir' => isset($workshop_runtime['workshop_download_dir']) ? (string)$workshop_runtime['workshop_download_dir'] : '',
|
||||
'server_root' => isset($workshop_runtime['server_root']) ? (string)$workshop_runtime['server_root'] : rtrim((string)$home_info['home_path'], '/'),
|
||||
'post_install_script' => scm_workshop_post_install_action($server_xml),
|
||||
);
|
||||
$workshop_error = '';
|
||||
$workshop_ok = scm_workshop_write_manifest_and_run($db, $home_info, $server_xml, 'install', array($workshop_item_id), $workshop_error, $extra_manifest);
|
||||
if ($workshop_ok) {
|
||||
scm_record_install_done($db, (int)$history_id, 'installed', 0, 'workshop_install_ok');
|
||||
scm_upsert_manifest($db, $home_id, $addon_id, array(
|
||||
'install_method' => $install_method,
|
||||
'content_version' => $content_version,
|
||||
'install_state' => 'installed',
|
||||
'source_url' => 'steam://workshop/' . $workshop_item_id,
|
||||
'installed_by' => $user_id,
|
||||
));
|
||||
scm_log_content_install_action(array(
|
||||
'addon_id' => (int)$addon_id,
|
||||
'addon_name' => isset($addon_info['name']) ? $addon_info['name'] : '',
|
||||
'content_type' => $install_method,
|
||||
'home_id' => (int)$home_id,
|
||||
'home_cfg_id' => (int)$home_info['home_cfg_id'],
|
||||
'workshop_id' => $workshop_item_id,
|
||||
'steam_app_id' => $steam_app_id,
|
||||
'workshop_app_id' => $workshop_app_id,
|
||||
'target_path' => $target_path_resolved,
|
||||
'final_folder_path' => $target_path_resolved,
|
||||
'action' => 'succeeded',
|
||||
));
|
||||
print_success(get_lang('addon_installed_successfully'));
|
||||
} else {
|
||||
scm_record_install_done($db, (int)$history_id, 'failed', 1, $workshop_error);
|
||||
scm_log_content_install_action(array(
|
||||
'addon_id' => (int)$addon_id,
|
||||
'addon_name' => isset($addon_info['name']) ? $addon_info['name'] : '',
|
||||
'content_type' => $install_method,
|
||||
'home_id' => (int)$home_id,
|
||||
'home_cfg_id' => (int)$home_info['home_cfg_id'],
|
||||
'workshop_id' => $workshop_item_id,
|
||||
'steam_app_id' => $steam_app_id,
|
||||
'workshop_app_id' => $workshop_app_id,
|
||||
'target_path' => $target_path_resolved,
|
||||
'action' => 'failed',
|
||||
'error' => $workshop_error,
|
||||
));
|
||||
print_failure($workshop_error);
|
||||
}
|
||||
echo "<p><a href=\"?m=addonsmanager&p=user_addons&home_id=$home_id&mod_id=$mod_id&ip=$ip&port=$port\">".get_lang('back')."</a></p>";
|
||||
return;
|
||||
}
|
||||
if ($install_method === 'post_script') {
|
||||
$script_command = "cd " . escapeshellarg($home_info['home_path']) . " && /bin/bash -lc " . escapeshellarg((string)$post_script) . " ; echo __GSP_SCRIPT_EXIT:$?";
|
||||
$script_output = $remote->exec($script_command);
|
||||
$script_ok = is_string($script_output) && preg_match('/__GSP_SCRIPT_EXIT:(\d+)/', $script_output, $sm) && (int)$sm[1] === 0;
|
||||
if ($script_ok) {
|
||||
scm_record_install_done($db, (int)$history_id, 'installed', 0, trim((string)$script_output));
|
||||
scm_upsert_manifest($db, $home_id, $addon_id, array(
|
||||
'install_method' => $install_method,
|
||||
'content_version' => $content_version,
|
||||
'install_state' => 'installed',
|
||||
'source_url' => '',
|
||||
'installed_by' => $user_id,
|
||||
));
|
||||
scm_log_content_install_action(array('addon_id' => (int)$addon_id, 'addon_name' => isset($addon_info['name']) ? $addon_info['name'] : '', 'content_type' => $install_method, 'home_id' => (int)$home_id, 'home_cfg_id' => (int)$home_info['home_cfg_id'], 'action' => 'succeeded'));
|
||||
print_success(get_lang('addon_installed_successfully'));
|
||||
} else {
|
||||
$error_msg = 'Script/action failed.';
|
||||
scm_record_install_done($db, (int)$history_id, 'failed', 1, trim((string)$script_output));
|
||||
scm_log_content_install_action(array('addon_id' => (int)$addon_id, 'addon_name' => isset($addon_info['name']) ? $addon_info['name'] : '', 'content_type' => $install_method, 'home_id' => (int)$home_id, 'home_cfg_id' => (int)$home_info['home_cfg_id'], 'action' => 'failed', 'error' => trim((string)$script_output)));
|
||||
print_failure($error_msg);
|
||||
}
|
||||
echo "<p><a href=\"?m=addonsmanager&p=user_addons&home_id=$home_id&mod_id=$mod_id&ip=$ip&port=$port\">".get_lang('back')."</a></p>";
|
||||
return;
|
||||
}
|
||||
if ($install_method === 'config_edit' || $install_method === 'create_folder') {
|
||||
$placeholder_map = scm_build_placeholder_map($home_info, array('exe_location' => (string)$server_xml->exe_location));
|
||||
$target_template = trim((string)$addon_info['path']);
|
||||
$resolved_path = scm_apply_placeholders($target_template, $placeholder_map);
|
||||
if ($resolved_path === '' || strpos($resolved_path, '/') !== 0) {
|
||||
$resolved_path = clean_path(rtrim($home_info['home_path'], '/') . '/' . ltrim($resolved_path, '/'));
|
||||
}
|
||||
$ok = false;
|
||||
if ($install_method === 'create_folder') {
|
||||
$ok = is_string($remote->exec("mkdir -p " . escapeshellarg($resolved_path) . " && echo __GSP_FOLDER_OK"));
|
||||
} else {
|
||||
$config_rule = trim((string)$addon_info['config_edit_rule']);
|
||||
$dir = dirname($resolved_path);
|
||||
$cmd = "mkdir -p " . escapeshellarg($dir) . " && touch " . escapeshellarg($resolved_path) . " && printf %s " . escapeshellarg($config_rule . PHP_EOL) . " >> " . escapeshellarg($resolved_path) . " && echo __GSP_CONFIG_OK";
|
||||
$out = $remote->exec($cmd);
|
||||
$ok = is_string($out) && strpos($out, '__GSP_CONFIG_OK') !== false;
|
||||
}
|
||||
if ($ok) {
|
||||
scm_record_install_done($db, (int)$history_id, 'installed', 0, $install_method . '_ok');
|
||||
scm_upsert_manifest($db, $home_id, $addon_id, array('install_method' => $install_method, 'content_version' => $content_version, 'install_state' => 'installed', 'source_url' => '', 'installed_by' => $user_id));
|
||||
scm_log_content_install_action(array('addon_id' => (int)$addon_id, 'addon_name' => isset($addon_info['name']) ? $addon_info['name'] : '', 'content_type' => $install_method, 'home_id' => (int)$home_id, 'home_cfg_id' => (int)$home_info['home_cfg_id'], 'target_path' => $resolved_path, 'action' => 'succeeded'));
|
||||
print_success(get_lang('addon_installed_successfully'));
|
||||
} else {
|
||||
scm_record_install_done($db, (int)$history_id, 'failed', 1, $install_method . '_failed');
|
||||
scm_log_content_install_action(array('addon_id' => (int)$addon_id, 'addon_name' => isset($addon_info['name']) ? $addon_info['name'] : '', 'content_type' => $install_method, 'home_id' => (int)$home_id, 'home_cfg_id' => (int)$home_info['home_cfg_id'], 'target_path' => $resolved_path, 'action' => 'failed', 'error' => $install_method . '_failed'));
|
||||
print_failure('Content action failed.');
|
||||
}
|
||||
echo "<p><a href=\"?m=addonsmanager&p=user_addons&home_id=$home_id&mod_id=$mod_id&ip=$ip&port=$port\">".get_lang('back')."</a></p>";
|
||||
return;
|
||||
}
|
||||
$download_action = ($install_method === 'download_file') ? "" : "uncompress";
|
||||
$pid = $remote->start_file_download( $addon_info['url'], $home_info['home_path']."/".$addon_info['path'], $filename, $download_action, $post_script);
|
||||
}
|
||||
|
||||
$headers = get_headers($url, 1);
|
||||
|
||||
$download_available = !$headers ? FALSE : TRUE;
|
||||
// Check if any error occured
|
||||
if($download_available)
|
||||
{
|
||||
$bytes = is_array($headers['Content-Length']) ? $headers['Content-Length'][1] : $headers['Content-Length'];
|
||||
// Display the File Size
|
||||
$totalsize = $bytes / 1024;
|
||||
clearstatcache();
|
||||
}
|
||||
|
||||
$kbytes = $remote->rsync_progress($home_info['home_path']."/".$addon_info['path']."/".$filename);
|
||||
list($totalsize,$mbytes,$pct) = explode(";",do_progress($kbytes,$totalsize));
|
||||
$totalmbytes = round($totalsize / 1024, 2);
|
||||
$pct = $pct > 100 ? 100 : $pct;
|
||||
echo "<h2>" . htmlentities($home_info['home_name']) . "</h2>";
|
||||
echo '<div class="dragbox bloc rounded" style="background-color:#dce9f2;" >
|
||||
<h4>'.get_lang('install')." ".$filename." ${mbytes}MB/${totalmbytes}MB</h4>
|
||||
<div style='background-color:#dce9f2;' >
|
||||
";
|
||||
$bar = '';
|
||||
for( $i = 1; $i <= $pct; $i++ )
|
||||
{
|
||||
$bar .= '<img style="width:0.92%;vertical-align:middle;" src="images/progressBar.png">';
|
||||
}
|
||||
echo "<center>$bar <b style='vertical-align:top;display:inline;font-size:1.2em;color:red;' >$pct%</b></center>
|
||||
</div>
|
||||
</div>";
|
||||
|
||||
if ( ( $pct == "100" or !$download_available ) AND $post_script != "" )
|
||||
{
|
||||
$log_retval = $remote->get_log( "post_script",
|
||||
$pid,
|
||||
clean_path($home_info['home_path']."/".$server_xml->exe_location),
|
||||
$script_log);
|
||||
if ($log_retval == 0)
|
||||
{
|
||||
print_failure(get_lang('agent_offline'));
|
||||
}
|
||||
elseif ($log_retval == 1 || $log_retval == 2)
|
||||
{
|
||||
echo "<pre class='log'>".$script_log."</pre>";
|
||||
}
|
||||
elseif( $remote->is_screen_running("post_script",$pid) == 1 )
|
||||
{
|
||||
print_failure(get_lang_f('unable_to_get_log',$log_retval));
|
||||
}
|
||||
}
|
||||
|
||||
if( $pct == "100" or !$download_available or ( $download_available and $pct == "-" and $pid > 0 ) )
|
||||
{
|
||||
if(!$download_available)
|
||||
{
|
||||
print_failure(get_lang('failed_to_start_file_download'));
|
||||
scm_log_content_install_action(array(
|
||||
'addon_id' => (int)$addon_id,
|
||||
'addon_name' => isset($addon_info['name']) ? $addon_info['name'] : '',
|
||||
'content_type' => $install_method,
|
||||
'home_id' => (int)$home_id,
|
||||
'home_cfg_id' => (int)$home_info['home_cfg_id'],
|
||||
'action' => 'failed',
|
||||
'error' => 'failed_to_start_file_download',
|
||||
));
|
||||
}
|
||||
elseif( $remote->is_file_download_in_progress($pid) === 1 )
|
||||
{
|
||||
if ($install_method === 'download_zip')
|
||||
print_success(get_lang_f('wait_while_decompressing', $filename));
|
||||
else
|
||||
print_success(get_lang('install') . " " . $filename . "...");
|
||||
echo "<p><a href=\"?m=addonsmanager&p=addons&state=refresh&home_id=$home_id&mod_id=$mod_id".
|
||||
"&ip=$ip&port=$port&addon_id=$addon_id&pid=$pid\">".get_lang('refresh')."</a></p>";
|
||||
$view->refresh("?m=addonsmanager&p=addons&state=refresh&home_id=$home_id&mod_id=$mod_id".
|
||||
"&ip=$ip&port=$port&addon_id=$addon_id&pid=$pid",5);
|
||||
}
|
||||
elseif( $remote->is_file_download_in_progress($pid) === 0 AND $remote->is_screen_running("post_script",$pid) === 0 )
|
||||
{
|
||||
print_success(get_lang('addon_installed_successfully'));
|
||||
// Update install history and manifest on successful completion.
|
||||
$history_key = 'scm_history_id_' . $home_id . '_' . $addon_id;
|
||||
if (!empty($_SESSION[$history_key])) {
|
||||
scm_record_install_done($db, (int)$_SESSION[$history_key], 'installed', 0);
|
||||
unset($_SESSION[$history_key]);
|
||||
}
|
||||
scm_upsert_manifest($db, $home_id, $addon_id, array(
|
||||
'install_method' => $install_method,
|
||||
'content_version' => $content_version,
|
||||
'install_state' => 'installed',
|
||||
'source_url' => $addon_info['url'],
|
||||
'installed_by' => $user_id,
|
||||
));
|
||||
scm_log_content_install_action(array(
|
||||
'addon_id' => (int)$addon_id,
|
||||
'addon_name' => isset($addon_info['name']) ? $addon_info['name'] : '',
|
||||
'content_type' => $install_method,
|
||||
'home_id' => (int)$home_id,
|
||||
'home_cfg_id' => (int)$home_info['home_cfg_id'],
|
||||
'target_path' => isset($addon_info['path']) ? (string)$addon_info['path'] : '',
|
||||
'action' => 'succeeded',
|
||||
));
|
||||
echo "<p><a href=\"?m=addonsmanager&p=user_addons&home_id=$home_id".
|
||||
"&mod_id=$mod_id&ip=$ip&port=$port\">".get_lang('back')."</a></p>";
|
||||
$view->refresh("?m=addonsmanager&p=user_addons&home_id=$home_id".
|
||||
"&mod_id=$mod_id&ip=$ip&port=$port",10);
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
echo "<p><a href=\"?m=addonsmanager&p=addons&state=refresh&home_id=$home_id&mod_id=$mod_id".
|
||||
"&ip=$ip&port=$port&addon_id=$addon_id&pid=$pid\">".get_lang('refresh')."</a></p>";
|
||||
$view->refresh("?m=addonsmanager&p=addons&state=refresh&home_id=$home_id&mod_id=$mod_id".
|
||||
"&ip=$ip&port=$port&addon_id=$addon_id&pid=$pid",5);
|
||||
}
|
||||
|
||||
}
|
||||
elseif( $addon_type != "" )
|
||||
{
|
||||
|
||||
if (!(is_array($addon_types) && in_array($addon_type, $addon_types))) {
|
||||
print_failure(get_lang('invalid_addon_type'));
|
||||
$view->refresh('?m=addonsmanager&p=user_addons&home_id='. $home_id .'&mod_id='. $mod_id .'&ip='. $ip .'&port='.$port);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Workshop items are managed through the dedicated workshop_content page
|
||||
// where users enter their own Workshop IDs. Redirect there immediately.
|
||||
if ($addon_type === 'workshop_item') {
|
||||
$first_addon_id = 0;
|
||||
$wk_addons = $db->resultQuery(
|
||||
"SELECT addon_id FROM OGP_DB_PREFIXaddons
|
||||
WHERE addon_type='workshop_item' AND home_cfg_id=" . (int)$home_cfg_id . $query_groups . "
|
||||
ORDER BY name ASC LIMIT 1"
|
||||
);
|
||||
if (is_array($wk_addons) && !empty($wk_addons[0]['addon_id'])) {
|
||||
$first_addon_id = (int)$wk_addons[0]['addon_id'];
|
||||
}
|
||||
$redirect = "?m=addonsmanager&p=workshop_content&home_id=" . (int)$home_id .
|
||||
"&mod_id=" . (int)$mod_id . "&ip=" . urlencode($ip) . "&port=" . urlencode($port) .
|
||||
($first_addon_id > 0 ? "&addon_id=" . $first_addon_id : '');
|
||||
$view->refresh($redirect);
|
||||
echo "<p>Redirecting to Workshop Content manager…<br>";
|
||||
echo "<a href='" . htmlspecialchars($redirect, ENT_QUOTES, 'UTF-8') . "'>Click here if not redirected.</a></p>";
|
||||
return;
|
||||
}
|
||||
?>
|
||||
<?php
|
||||
$category_labels = get_server_content_categories();
|
||||
$addon_type_lang = isset($category_labels[$addon_type]) ? $category_labels[$addon_type] : ucfirst(str_replace('_', ' ', $addon_type));
|
||||
$addons = $db->resultQuery(
|
||||
"SELECT addon_id, name, install_method, workshop_item_id, workshop_app_id, target_path_template, optional_folder_name
|
||||
FROM OGP_DB_PREFIXaddons
|
||||
WHERE addon_type='".$addon_type."' AND home_cfg_id=" . $home_cfg_id . $query_groups . "
|
||||
ORDER BY name ASC"
|
||||
);
|
||||
if (!is_array($addons)) {
|
||||
$addons = [];
|
||||
}
|
||||
$selected_addon = isset($addons[0]) ? $addons[0] : array();
|
||||
$default_install_method = isset($selected_addon['install_method']) ? scm_get_install_method_default($selected_addon['install_method']) : '';
|
||||
$is_workshop_default = ($default_install_method === 'steam_workshop');
|
||||
$default_workshop_app_id = scm_extract_workshop_app_id($server_xml);
|
||||
$default_target_template = scm_extract_workshop_install_path($server_xml);
|
||||
if ($default_target_template === '') {
|
||||
$default_target_template = scm_get_default_workshop_target_template(scm_detect_workshop_install_strategy($home_info, $server_xml));
|
||||
}
|
||||
?>
|
||||
<h2><?php echo htmlentities($home_info['home_name'])." ".$addon_type_lang ;?></h2>
|
||||
<table class='center'>
|
||||
<form method='get'>
|
||||
<input type='hidden' name='m' value='addonsmanager' />
|
||||
<input type='hidden' name='p' value='addons' />
|
||||
<input type='hidden' name='home_id' value='<?php echo $home_id; ?>' />
|
||||
<input type='hidden' name='mod_id' value='<?php echo $mod_id; ?>' />
|
||||
<input type='hidden' name='ip' value='<?php echo $ip; ?>' />
|
||||
<input type='hidden' name='port' value='<?php echo $port; ?>' />
|
||||
<input type='hidden' name='state' value='start' />
|
||||
<tr><td align='right'><?php print_lang('game_name'); ?>: </td><td align='left'><?php echo $home_info['game_name']; ?></td></tr>
|
||||
<tr><td align='right'><?php print_lang('directory'); ?>: </td><td align='left'><?php echo $home_info['home_path']; ?></td></tr>
|
||||
<tr><td align='right'><?php print_lang('remote_server'); ?>: </td>
|
||||
<td align='left'><?php echo "$home_info[remote_server_name] ($home_info[agent_ip]:$home_info[agent_port])"; ?></td></tr>
|
||||
<tr><td align='right'><?php print_lang('select_addon'); ?>: </td>
|
||||
<td align='left'>
|
||||
<select name="addon_id" id="scm-user-addon-select">
|
||||
<?php foreach ((array)$addons as $addon) { ?>
|
||||
<option
|
||||
value="<?php echo (int)$addon['addon_id']; ?>"
|
||||
data-install-method="<?php echo htmlspecialchars(scm_get_install_method_default(isset($addon['install_method']) ? $addon['install_method'] : ''), ENT_QUOTES, 'UTF-8'); ?>"
|
||||
data-workshop-item-id="<?php echo htmlspecialchars(isset($addon['workshop_item_id']) ? $addon['workshop_item_id'] : '', ENT_QUOTES, 'UTF-8'); ?>"
|
||||
><?php echo htmlspecialchars($addon['name']); ?></option>
|
||||
<?php } ?>
|
||||
</select>
|
||||
</td></tr>
|
||||
<tr class="scm-user-workshop-row" <?php echo $is_workshop_default ? '' : 'style="display:none;"'; ?>><td align='right'><strong><?php print_lang('workshop_id'); ?></strong></td><td align='left'>
|
||||
<input type="text" id="scm-user-workshop-id" name="workshop_item_id" size="50" value="<?php echo htmlspecialchars(isset($selected_addon['workshop_item_id']) ? $selected_addon['workshop_item_id'] : '', ENT_QUOTES, 'UTF-8'); ?>" placeholder="Example Arma 3 Workshop ID: 450814997" />
|
||||
<div class="info" style="margin-top:4px;">Install a Steam Workshop mod using Workshop ID. URL is not required.</div>
|
||||
</td></tr>
|
||||
<tr class="scm-user-workshop-row" <?php echo $is_workshop_default ? '' : 'style="display:none;"'; ?>><td align='right'><strong>Target Path Preview</strong></td><td align='left'>
|
||||
<code id="scm-user-target-path-preview"
|
||||
data-server-root="<?php echo htmlspecialchars(rtrim((string)$home_info['home_path'], '/'), ENT_QUOTES, 'UTF-8'); ?>"
|
||||
data-game-root="<?php echo htmlspecialchars(rtrim((string)$home_info['home_path'], '/'), ENT_QUOTES, 'UTF-8'); ?>"
|
||||
data-steam-app-id="<?php echo htmlspecialchars(scm_extract_workshop_steam_app_id($server_xml), ENT_QUOTES, 'UTF-8'); ?>"
|
||||
data-workshop-app-id="<?php echo htmlspecialchars($default_workshop_app_id, ENT_QUOTES, 'UTF-8'); ?>"
|
||||
data-target-template="<?php echo htmlspecialchars($default_target_template, ENT_QUOTES, 'UTF-8'); ?>"
|
||||
><?php echo htmlspecialchars($default_target_template, ENT_QUOTES, 'UTF-8'); ?></code>
|
||||
<div class="info" style="margin-top:4px;">Workshop App ID, install path, and launch parameter format are defined in the game XML.</div>
|
||||
</td></tr>
|
||||
<tr><td colspan='2' class='info'> </td></tr>
|
||||
<td align='left'>
|
||||
|
||||
</td></tr><tr><td align="right">
|
||||
<input type="submit" name="update" value="<?php print_lang('install'); ?>" />
|
||||
</form></td><td>
|
||||
<form method="get">
|
||||
<input type="hidden" name="m" value="addonsmanager" />
|
||||
<input type="hidden" name="p" value="user_addons" />
|
||||
<input type="hidden" name="home_id" value="<?php echo $home_id; ?>" />
|
||||
<input type="hidden" name="mod_id" value="<?php echo $mod_id; ?>" />
|
||||
<input type="hidden" name="ip" value="<?php echo $ip; ?>" />
|
||||
<input type="hidden" name="port" value="<?php echo $port; ?>" />
|
||||
<input type="submit" value="<?php print_lang('back'); ?>" />
|
||||
</form>
|
||||
</td></tr>
|
||||
</table>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
|
@ -30,6 +30,12 @@ require_once("modules/config_games/server_config_parser.php");
|
|||
|
||||
function exec_ogp_module() {
|
||||
|
||||
if (isset($install_method) && $install_method === 'steam_workshop') {
|
||||
print_failure('Steam Workshop has been removed from Server Content. Use the Steam Workshop module instead.');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
global $db;
|
||||
|
||||
// Ensure Phase 2 schema is present (idempotent).
|
||||
|
|
|
|||
|
|
@ -0,0 +1,532 @@
|
|||
<script type="text/javascript" src="js/modules/addonsmanager.js"></script>
|
||||
<?php
|
||||
/*
|
||||
*
|
||||
* GSP - Game Server Panel (a heavily customized fork of OGP maintained by WDS)
|
||||
*
|
||||
* Admin page: Server Content Manager (module: addonsmanager)
|
||||
* ─────────────────────────────────────────────────────────────────────────────
|
||||
* This page lets admins create, edit, and remove Server Content items.
|
||||
*
|
||||
* A "Server Content item" is anything that can be pushed to a game server:
|
||||
* 1. A zip/file package extracted into the server directory.
|
||||
* 2. A downloaded file placed into the server directory.
|
||||
* 3. A script-driven installer (post_script only, no download required).
|
||||
* 4. A Minecraft server jar / version switcher (future: install_method=minecraft_jar).
|
||||
* 5. A DayZ/Epoch/Arma profile copy (future: install_method=profile_copy).
|
||||
* 6. A Steam Workshop entry point (install behavior comes from game XML).
|
||||
* 7. A config preset (type=config).
|
||||
* 8. A full server profile built from multiple actions (type=profile).
|
||||
*
|
||||
* DB table: OGP_DB_PREFIXaddons (unchanged for backward compatibility).
|
||||
* See SERVER_CONTENT_ROADMAP.md for the full migration plan.
|
||||
*
|
||||
*/
|
||||
|
||||
// Central category map — defines all valid addon_type values and their labels.
|
||||
require_once(dirname(__FILE__) . '/server_content_categories.php');
|
||||
require_once(dirname(__FILE__) . '/server_content_helpers.php');
|
||||
require_once("modules/config_games/server_config_parser.php");
|
||||
|
||||
function exec_ogp_module() {
|
||||
|
||||
global $db;
|
||||
|
||||
// Ensure Phase 2 schema is present (idempotent).
|
||||
scm_ensure_phase2_schema($db);
|
||||
|
||||
// Build the complete list of allowed content types from the category map.
|
||||
// Admins can create items of any registered type; the original three types
|
||||
// (plugin, mappack, config) are always included.
|
||||
$addon_types = get_server_content_type_keys(); // all keys
|
||||
$addon_type_labels = get_server_content_categories(); // key => label
|
||||
$install_methods = scm_get_install_methods(); // install_method keys => labels
|
||||
|
||||
if (isset($_POST['create_addon']))
|
||||
{
|
||||
$valid_install_methods = array_keys($install_methods);
|
||||
$fields['name'] = isset($_POST['name']) ? trim((string)$_POST['name']) : '';
|
||||
$fields['url'] = isset($_POST['url']) ? trim((string)$_POST['url']) : '';
|
||||
$fields['path'] = isset($_POST['path']) ? trim((string)$_POST['path']) : '';
|
||||
$fields['addon_type'] = '';
|
||||
$fields['home_cfg_id'] = isset($_POST['home_cfg_id']) ? (int)$_POST['home_cfg_id'] : 0;
|
||||
$fields['post_script'] = isset($_POST['post_script']) ? trim((string)$_POST['post_script']) : '';
|
||||
$fields['group_id'] = isset($_POST['group_id']) ? (int)$_POST['group_id'] : 0;
|
||||
$posted_install_method = isset($_POST['install_method']) ? $_POST['install_method'] : '';
|
||||
$fields['install_method'] = in_array($posted_install_method, $valid_install_methods) ? $posted_install_method : 'download_zip';
|
||||
$fields['content_version'] = isset($_POST['content_version']) ? $_POST['content_version'] : '';
|
||||
$fields['requires_stop'] = !empty($_POST['requires_stop']) ? 1 : 0;
|
||||
$fields['backup_before_install'] = !empty($_POST['backup_before_install']) ? 1 : 0;
|
||||
$fields['restart_after_install'] = !empty($_POST['restart_after_install']) ? 1 : 0;
|
||||
$fields['is_cacheable'] = !empty($_POST['is_cacheable']) ? 1 : 0;
|
||||
$fields['description'] = isset($_POST['description']) ? $_POST['description'] : '';
|
||||
$fields['workshop_item_id'] = '';
|
||||
$fields['workshop_app_id'] = '';
|
||||
$fields['target_path_template']= '';
|
||||
$fields['optional_folder_name']= '';
|
||||
$fields['config_edit_rule'] = isset($_POST['config_edit_rule']) ? trim((string)$_POST['config_edit_rule']) : '';
|
||||
$fields['launch_param_additions'] = '';
|
||||
$fields['addon_type'] = scm_get_addon_type_from_install_method($fields['install_method']);
|
||||
if ($fields['install_method'] === 'steam_workshop') {
|
||||
$fields['url'] = '';
|
||||
$fields['path'] = '';
|
||||
$fields['post_script'] = '';
|
||||
$fields['config_edit_rule'] = '';
|
||||
}
|
||||
|
||||
if ($fields['name'] === '')
|
||||
{
|
||||
print_failure(get_lang("fill_the_addon_name"));
|
||||
}
|
||||
elseif (empty($fields['home_cfg_id']))
|
||||
{
|
||||
print_failure(get_lang("select_a_game_type"));
|
||||
}
|
||||
elseif ($fields['install_method'] === 'steam_workshop' && (!($game_cfg = $db->getGameCfg($fields['home_cfg_id'])) || !($server_xml = read_server_config(SERVER_CONFIG_LOCATION . "/" . $game_cfg['home_cfg_file'])) || !scm_workshop_is_supported($server_xml)))
|
||||
{
|
||||
print_failure('Steam Workshop support must be configured in the selected game XML before creating a Workshop content entry.');
|
||||
}
|
||||
else
|
||||
{
|
||||
$validation_payload = array(
|
||||
'url' => $fields['url'],
|
||||
'path' => $fields['path'],
|
||||
'workshop_item_id' => $fields['workshop_item_id'],
|
||||
'workshop_app_id' => $fields['workshop_app_id'],
|
||||
'target_path_template' => $fields['target_path_template'],
|
||||
'post_script' => $fields['post_script'],
|
||||
'config_edit_rule' => $fields['config_edit_rule'],
|
||||
);
|
||||
$validation_message = '';
|
||||
if (!scm_validate_install_method_payload($fields['install_method'], $validation_payload, $validation_message))
|
||||
{
|
||||
print_failure($validation_message);
|
||||
}
|
||||
elseif (is_numeric($db->resultInsertId('addons', $fields)))
|
||||
{
|
||||
print_success(get_lang_f("addon_has_been_created", $fields['name']));
|
||||
if (isset($_POST['addon_id']) && (int)$_POST['addon_id'] > 0 && isset($_POST['edit']))
|
||||
$db->query("DELETE FROM OGP_DB_PREFIXaddons WHERE addon_id=" . (int)$_POST['addon_id']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
echo "<h2>".get_lang('addons_manager')."</h2>";
|
||||
$name = isset($_POST['name']) ? $_POST['name'] : "";
|
||||
$url = isset($_POST['url']) ? $_POST['url'] : "";
|
||||
$path = isset($_POST['path']) ? $_POST['path'] : "";
|
||||
$post_script = isset($_POST['post_script']) ? $_POST['post_script'] : "";
|
||||
$home_cfg_id = isset($_POST['home_cfg_id']) ? $_POST['home_cfg_id'] : "";
|
||||
$addon_type = isset($_POST['addon_type']) ? $_POST['addon_type'] : "";
|
||||
$group_id = isset($_POST['group_id']) ? $_POST['group_id'] : "";
|
||||
$install_method = isset($_POST['install_method']) ? $_POST['install_method'] : "download_zip";
|
||||
$content_version = isset($_POST['content_version']) ? $_POST['content_version'] : "";
|
||||
$requires_stop = isset($_POST['requires_stop']) ? (int)$_POST['requires_stop'] : 1;
|
||||
$backup_before_install = isset($_POST['backup_before_install']) ? (int)$_POST['backup_before_install'] : 1;
|
||||
$restart_after_install = isset($_POST['restart_after_install']) ? (int)$_POST['restart_after_install'] : 0;
|
||||
$is_cacheable = isset($_POST['is_cacheable']) ? (int)$_POST['is_cacheable'] : 0;
|
||||
$description = isset($_POST['description']) ? $_POST['description'] : "";
|
||||
$config_edit_rule = isset($_POST['config_edit_rule']) ? $_POST['config_edit_rule'] : "";
|
||||
|
||||
if (isset($_POST['addon_id']) && (int)$_POST['addon_id'] > 0 && isset($_POST['edit']))
|
||||
{
|
||||
$addons_rows = $db->resultQuery("SELECT * FROM OGP_DB_PREFIXaddons WHERE addon_id=".(int)$_POST['addon_id']);
|
||||
if (!is_array($addons_rows)) {
|
||||
$addons_rows = [];
|
||||
}
|
||||
$addon_info = $addons_rows[0];
|
||||
$name = isset($addon_info['name']) ? $addon_info['name'] : "";
|
||||
$url = isset($addon_info['url']) ? $addon_info['url'] : "";
|
||||
$path = isset($addon_info['path']) ? $addon_info['path'] : "";
|
||||
$post_script = isset($addon_info['post_script']) ? $addon_info['post_script'] : "";
|
||||
$home_cfg_id = isset($addon_info['home_cfg_id']) ? $addon_info['home_cfg_id'] : "";
|
||||
$addon_type = scm_normalize_addon_type(isset($addon_info['addon_type']) ? $addon_info['addon_type'] : "", $install_method);
|
||||
$group_id = isset($addon_info['group_id']) ? $addon_info['group_id'] : "";
|
||||
$install_method = isset($addon_info['install_method']) ? $addon_info['install_method'] : "download_zip";
|
||||
$content_version = isset($addon_info['content_version']) ? $addon_info['content_version'] : "";
|
||||
$requires_stop = isset($addon_info['requires_stop']) ? (int)$addon_info['requires_stop'] : 1;
|
||||
$backup_before_install = isset($addon_info['backup_before_install']) ? (int)$addon_info['backup_before_install'] : 1;
|
||||
$restart_after_install = isset($addon_info['restart_after_install']) ? (int)$addon_info['restart_after_install'] : 0;
|
||||
$is_cacheable = isset($addon_info['is_cacheable']) ? (int)$addon_info['is_cacheable'] : 0;
|
||||
$description = isset($addon_info['description']) ? $addon_info['description'] : "";
|
||||
$config_edit_rule = isset($addon_info['config_edit_rule']) ? $addon_info['config_edit_rule'] : "";
|
||||
}
|
||||
?>
|
||||
<form action="" method="post">
|
||||
<table class="center">
|
||||
<tr>
|
||||
<td align="right">
|
||||
<b><?php print_lang('addon_name'); ?></b>
|
||||
</td>
|
||||
<td align="left">
|
||||
<input type="text" value="<?php echo $name; ?>" name="name" size="85" title="<?php print_lang('addon_name_info'); ?>" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr id="scm-row-install-method">
|
||||
<td align="right">
|
||||
<b><?php print_lang('content_type'); ?></b>
|
||||
</td>
|
||||
<td align="left">
|
||||
<select name="install_method" id="scm-install-method">
|
||||
<?php
|
||||
$install_help = scm_get_install_method_help_text();
|
||||
foreach ((array)$install_methods as $method_key => $method_label) {
|
||||
$sel = ($method_key == $install_method) ? 'selected="selected"' : '';
|
||||
$help = isset($install_help[$method_key]) ? $install_help[$method_key] : '';
|
||||
echo '<option value="'.htmlspecialchars($method_key).'" data-help="'.htmlspecialchars($help, ENT_QUOTES, 'UTF-8').'" '.$sel.'>'.htmlspecialchars($method_label).'</option>'."\n";
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
<div id="scm-install-method-help" style="color:#666;margin-top:4px;"></div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr id="scm-row-url">
|
||||
<td align="right">
|
||||
<b><?php print_lang('url'); ?></b>
|
||||
</td>
|
||||
<td align="left">
|
||||
<input type="text" value="<?php echo $url; ?>" name="url" size="85" title="<?php print_lang('url_info'); ?>" />
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Destination path — must be relative to the game server home directory.
|
||||
Path traversal (../) is not allowed; the agent enforces this. -->
|
||||
<tr id="scm-row-path">
|
||||
<td align="right">
|
||||
<b id="scm-path-label"><?php print_lang('path'); ?></b>
|
||||
</td>
|
||||
<td align="left">
|
||||
<input type="text" value="<?php echo $path; ?>" name="path" size="85" title="<?php print_lang('path_info'); ?>" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr id="scm-row-workshop-xml-info">
|
||||
<td align="right">
|
||||
<b>Steam Workshop</b>
|
||||
</td>
|
||||
<td align="left">
|
||||
<div class="info">Steam Workshop behavior is configured by the selected game's XML <code>workshop_support</code> block. Users install Workshop items from their server management page.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr id="scm-row-post-script">
|
||||
<td align="right">
|
||||
<b>Post-Install Script / Action</b><br>
|
||||
<u><?php print_lang('replacements'); ?></u><br>
|
||||
%home_path%<br>
|
||||
%home_name%<br>
|
||||
%control_password%<br>
|
||||
%max_players%<br>
|
||||
%ip%<br>
|
||||
%port%<br>
|
||||
%query_port%<br>
|
||||
%incremental%<br>
|
||||
</td>
|
||||
<td align="left">
|
||||
<textarea name="post_script" style="width:99%;height:175px;" title="<?php print_lang('post-script_info'); ?>" ><?php echo strip_real_escape_string($post_script); ?></textarea>
|
||||
</td>
|
||||
</tr>
|
||||
<tr id="scm-row-config-edit-rule">
|
||||
<td align="right">
|
||||
<b><?php print_lang('config_edit_rule'); ?></b>
|
||||
</td>
|
||||
<td align="left">
|
||||
<textarea name="config_edit_rule" style="width:99%;height:90px;" placeholder="Text/rules to append or apply to the target config."><?php echo htmlspecialchars($config_edit_rule, ENT_QUOTES, 'UTF-8'); ?></textarea>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="right">
|
||||
<b><?php print_lang('select_game_type'); ?></b>
|
||||
</td>
|
||||
<td align="left">
|
||||
<select name='home_cfg_id'>
|
||||
<?php
|
||||
$game_cfgs = $db->getGameCfgs();
|
||||
if (!is_array($game_cfgs)) {
|
||||
$game_cfgs = [];
|
||||
}
|
||||
echo "<option style='background:black;color:white;' value=''>".get_lang('linux_games')."</option>\n";
|
||||
|
||||
foreach ((array)$game_cfgs as $row)
|
||||
{
|
||||
if ( preg_match("/linux/", $row['game_key']) )
|
||||
{
|
||||
$selected = (isset($home_cfg_id) AND $row['home_cfg_id'] == $home_cfg_id) ? 'selected="selected"' : '';
|
||||
echo "<option $selected value='".$row['home_cfg_id']."'>".$row['game_name'];
|
||||
if ( preg_match("/64/", $row['game_key']) ) echo " (64bit)";
|
||||
echo "</option>\n";
|
||||
}
|
||||
}
|
||||
echo "<option style='background:black;color:white;' value=''>".get_lang('windows_games')."</option>\n";
|
||||
foreach ((array)$game_cfgs as $row)
|
||||
{
|
||||
if ( preg_match("/win/", $row['game_key']) )
|
||||
{
|
||||
$selected = (isset($home_cfg_id) AND $row['home_cfg_id'] == $home_cfg_id) ? 'selected=selected' : '';
|
||||
echo "<option $selected value='".$row['home_cfg_id']."'>".$row['game_name'];
|
||||
if ( preg_match("/64/", $row['game_key']) ) echo " (64bit)";
|
||||
echo "</option>\n";
|
||||
}
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="right">
|
||||
<b><?php print_lang('show_to_group'); ?></b>
|
||||
</td>
|
||||
<td align="left">
|
||||
<select name='group_id'>
|
||||
<option value="0"><?php print_lang('all_groups'); ?></option>
|
||||
<?php
|
||||
$groups = $db->getGroupList();
|
||||
if (!is_array($groups)) {
|
||||
$groups = [];
|
||||
}
|
||||
foreach ((array)$groups as $group)
|
||||
{
|
||||
$selected = (isset($group_id) AND $group['group_id'] == $group_id) ? 'selected=selected' : '';
|
||||
echo "<option value='".$group['group_id']."' $selected>".$group['group_name']."</option>\n";
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="right">
|
||||
<b>Content Version</b>
|
||||
</td>
|
||||
<td align="left">
|
||||
<input type="text" value="<?php echo htmlspecialchars($content_version, ENT_QUOTES, 'UTF-8'); ?>" name="content_version" size="40" placeholder="e.g. 1.21.1 or 2024-05-01" />
|
||||
<small style="color:#666;"> Optional version tag shown in the installed-content list.</small>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="right">
|
||||
<b>Description</b>
|
||||
</td>
|
||||
<td align="left">
|
||||
<textarea name="description" style="width:99%;height:60px;" placeholder="Short description shown to users."><?php echo htmlspecialchars($description, ENT_QUOTES, 'UTF-8'); ?></textarea>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="right">
|
||||
<b>Behaviour Options</b>
|
||||
</td>
|
||||
<td align="left">
|
||||
<label>
|
||||
<input type="checkbox" name="requires_stop" value="1" <?php echo $requires_stop ? 'checked' : ''; ?> />
|
||||
Stop server before installing
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<input type="checkbox" name="backup_before_install" value="1" <?php echo $backup_before_install ? 'checked' : ''; ?> />
|
||||
Backup target path before installing
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<input type="checkbox" name="restart_after_install" value="1" <?php echo $restart_after_install ? 'checked' : ''; ?> />
|
||||
Restart server after successful install
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="right">
|
||||
<b>Content Reuse</b>
|
||||
</td>
|
||||
<td align="left">
|
||||
<label>
|
||||
<input type="checkbox" name="is_cacheable" value="1" <?php echo $is_cacheable ? 'checked' : ''; ?> />
|
||||
Mark as cacheable / reusable
|
||||
</label>
|
||||
<small style="color:#666;">
|
||||
Only check this for public, non-sensitive content (maps, mods, jars).
|
||||
<strong>Never</strong> check for configs, saves, credentials, or user-edited files.
|
||||
Caching only activates when the <em>Server Content Cache Mode</em> panel
|
||||
setting (in Panel Settings) is set to something other than <em>Disabled</em>.
|
||||
</small>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" align="center">
|
||||
<?php
|
||||
if (isset($_POST['addon_id']) && isset($_POST['edit']))
|
||||
{
|
||||
echo '<input type="hidden" name="addon_id" value="'.$_POST['addon_id'].'" >';
|
||||
echo '<input type="hidden" name="edit" value="'.$_POST['edit'].'" >';
|
||||
?>
|
||||
<button name="create_addon" type="submit">
|
||||
<?php print_lang('edit_addon'); ?>
|
||||
</button>
|
||||
<?php
|
||||
}
|
||||
else
|
||||
{
|
||||
?>
|
||||
<button name="create_addon" type="submit">
|
||||
<?php print_lang('create_addon'); ?>
|
||||
</button>
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</form>
|
||||
<br>
|
||||
<h2><?php print_lang('addons_db'); ?></h2>
|
||||
<table class="center">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<form name="remove" action="" method="get">
|
||||
<input name="m" type="hidden" value="addonsmanager"/>
|
||||
<input name="p" type="hidden" value="addons_manager"/>
|
||||
<b><?php print_lang('game'); ?></b> <select name='home_cfg_id'>
|
||||
<?php
|
||||
echo "<option style='background:black;color:white;' value=''>".get_lang('linux_games')."</option>\n";
|
||||
foreach ((array)$game_cfgs as $row)
|
||||
{
|
||||
if ( preg_match("/linux/", $row['game_key']) )
|
||||
{
|
||||
if(isset($_GET['home_cfg_id']) AND $row['home_cfg_id'] == $_GET['home_cfg_id'])
|
||||
$selected = "selected='selected'";
|
||||
else
|
||||
$selected = "";
|
||||
echo "<option value='".$row['home_cfg_id']."' $selected >".$row['game_name'];
|
||||
if ( preg_match("/64/", $row['game_key']) ) echo " (64bit)";
|
||||
echo "</option>\n";
|
||||
}
|
||||
}
|
||||
echo "<option style='background:black;color:white;' value=''>".get_lang('windows_games')."</option>\n";
|
||||
foreach ((array)$game_cfgs as $row)
|
||||
{
|
||||
if(isset($_GET['home_cfg_id']) AND $row['home_cfg_id'] == $_GET['home_cfg_id'])
|
||||
$selected = "selected='selected'";
|
||||
else
|
||||
$selected = "";
|
||||
if ( preg_match("/win/", $row['game_key']) )
|
||||
{
|
||||
echo "<option value='".$row['home_cfg_id']."' $selected >".$row['game_name'];
|
||||
if ( preg_match("/64/", $row['game_key']) ) echo " (64bit)";
|
||||
echo "</option>\n";
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
</select>
|
||||
<b><?php print_lang('type'); ?></b>
|
||||
<select name="addon_type">
|
||||
<?php
|
||||
$option = '';
|
||||
|
||||
foreach ((array)$addon_type_labels as $k => $label) {
|
||||
$option .= '<option';
|
||||
|
||||
if (isset($_GET['addon_type']) && $_GET['addon_type'] == $k) {
|
||||
$option .= ' selected';
|
||||
}
|
||||
|
||||
$option .= ' value="'. htmlspecialchars($k) .'">'.htmlspecialchars($label).'</option>';
|
||||
}
|
||||
|
||||
echo $option;
|
||||
?>
|
||||
|
||||
</select>
|
||||
<b><?php print_lang('group'); ?></b>
|
||||
<select name='group_id'>
|
||||
<option value="0"><?php print_lang('all_groups'); ?></option>
|
||||
<?php
|
||||
foreach ((array)$groups as $group)
|
||||
{
|
||||
$selected = (isset($_GET['group_id']) AND $group['group_id'] == $_GET['group_id']) ? 'selected=selected' : '';
|
||||
echo "<option value='".$group['group_id']."' $selected>".$group['group_name']."</option>\n";
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
<button name="show" type="submit">
|
||||
<?php print_lang('show'); ?>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<input name="show_game" type="submit" value="<?php print_lang('show_addons_for_selected_game'); ?>"/>
|
||||
<input name="show_type" type="submit" value="<?php print_lang('show_addons_for_selected_type'); ?>"/>
|
||||
<input name="show_group" type="submit" value="<?php print_lang('show_addons_for_selected_group'); ?>"/>
|
||||
<input name="show_all" type="submit" value="<?php print_lang('show_all_addons'); ?>"/>
|
||||
</td>
|
||||
</tr>
|
||||
</form>
|
||||
</table>
|
||||
<?php
|
||||
if (isset($_POST['addon_id']) && (int)$_POST['addon_id'] > 0 && isset($_POST['remove']))
|
||||
{
|
||||
if (!$db->query("DELETE FROM OGP_DB_PREFIXaddons WHERE addon_id=" . (int)$_POST['addon_id']))
|
||||
print_lang('can_not_remove_addon');
|
||||
}
|
||||
|
||||
$home_cfg_id = !empty($_GET['home_cfg_id']) && (int)$_GET['home_cfg_id'] > 0 ? (int)$_GET['home_cfg_id'] : 0;
|
||||
$addon_type = !empty($_GET['addon_type']) && in_array($_GET['addon_type'], $addon_types) ? $_GET['addon_type'] : "";
|
||||
$group_id = isset($_GET['group_id']) && is_numeric($_GET['group_id']) ? (int)$_GET['group_id'] : 0;
|
||||
|
||||
if ( isset($_GET['show']) )
|
||||
{
|
||||
$result = $db->resultQuery("SELECT DISTINCT addon_id, name, game_name, url, path, group_id FROM OGP_DB_PREFIXaddons NATURAL JOIN OGP_DB_PREFIXconfig_homes WHERE addon_type='".$addon_type."' AND home_cfg_id=".$home_cfg_id);
|
||||
}
|
||||
elseif ( isset($_GET['show_all']) )
|
||||
{
|
||||
$result = $db->resultQuery("SELECT DISTINCT addon_id, name, game_name, url, path, group_id FROM OGP_DB_PREFIXaddons NATURAL JOIN OGP_DB_PREFIXconfig_homes");
|
||||
}
|
||||
elseif ( isset($_GET['show_type']))
|
||||
{
|
||||
$result = $db->resultQuery("SELECT DISTINCT addon_id, name, game_name, url, path, group_id FROM OGP_DB_PREFIXaddons NATURAL JOIN OGP_DB_PREFIXconfig_homes WHERE addon_type='".$addon_type."'");
|
||||
}
|
||||
elseif ( isset($_GET['show_game']))
|
||||
{
|
||||
$result = $db->resultQuery("SELECT DISTINCT addon_id, name, game_name, url, path, group_id FROM OGP_DB_PREFIXaddons NATURAL JOIN OGP_DB_PREFIXconfig_homes WHERE home_cfg_id=".$home_cfg_id);
|
||||
}
|
||||
elseif ( isset($_GET['show_group']))
|
||||
{
|
||||
$group_id = $group_id == '0' ? $group_id." OR group_id IS NULL" : $group_id;
|
||||
$result = $db->resultQuery("SELECT DISTINCT addon_id, name, game_name, url, path, group_id FROM OGP_DB_PREFIXaddons NATURAL JOIN OGP_DB_PREFIXconfig_homes WHERE group_id=".$group_id);
|
||||
}
|
||||
if (isset($result) && !is_array($result)) {
|
||||
$result = [];
|
||||
}
|
||||
?>
|
||||
<table class="center">
|
||||
<?php
|
||||
$group_names = array();
|
||||
foreach ((array)$groups as $group)
|
||||
$group_names[$group['group_id']] = $group['group_name'];
|
||||
|
||||
if (isset($result) and is_array($result) and (is_array($result) ? count((array)$result) : 0) > 0)
|
||||
{
|
||||
foreach ((array)$result as $row)
|
||||
{
|
||||
?>
|
||||
<tr>
|
||||
<form action="" method="post">
|
||||
<td class='left'>
|
||||
<b><?php echo $row['game_name']; ?></b>
|
||||
</td>
|
||||
<td>
|
||||
<?php echo $row['name'];?>
|
||||
</td>
|
||||
<td>
|
||||
<?php echo "[".get_lang('group').": ". (isset($group_names[$row['group_id']])?$group_names[$row['group_id']]:get_lang('all_groups')) ."]";?>
|
||||
</td>
|
||||
<td>
|
||||
<input name="addon_id" type="hidden" value="<?php echo $row['addon_id'];?>"/>
|
||||
<input name="edit" type="submit" value="<?php print_lang('edit_addon'); ?>"/>
|
||||
<input name="remove" type="submit" value="<?php print_lang('remove_addon'); ?>"/>
|
||||
</td>
|
||||
</form>
|
||||
</tr>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
?>
|
||||
</table>
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
293
Panel/modules/addonsmanager/module.php.bak.20260609-111046
Normal file
293
Panel/modules/addonsmanager/module.php.bak.20260609-111046
Normal file
|
|
@ -0,0 +1,293 @@
|
|||
<?php
|
||||
/*
|
||||
*
|
||||
* GSP - Game Server Panel (a heavily customized fork of OGP maintained by WDS)
|
||||
*
|
||||
* Module: addonsmanager → Server Content Manager
|
||||
* ─────────────────────────────────────────────────────────────────────────────
|
||||
* The module folder and DB table names are intentionally unchanged for
|
||||
* backward compatibility. Only UI labels have been updated to the new
|
||||
* "Server Content" terminology.
|
||||
*
|
||||
* db_version history:
|
||||
* 1 – initial schema (addons table, addon_type VARCHAR(7))
|
||||
* 2 – expand addon_type to VARCHAR(32) to support extended content types
|
||||
* (workshop=8 chars, and any future type up to 32 chars)
|
||||
* 3 – add server_content_workshop table for per-server Workshop item selections
|
||||
* 4 – Phase 2: add install_method / content_version / requires_stop /
|
||||
* backup_before_install / restart_after_install / is_cacheable /
|
||||
* description columns to addons table; add server_content_manifest
|
||||
* and server_content_install_history tables
|
||||
* 5 – add workshop_item_id / workshop_app_id / target_path_template /
|
||||
* optional_folder_name / config_edit_rule / launch_param_additions
|
||||
* columns to addons table
|
||||
* 6 – add admin template policy columns to addons table
|
||||
* (allow_user_workshop_ids, max_workshop_ids, required_workshop_ids,
|
||||
* blocked_workshop_ids); add content_id column to
|
||||
* server_content_workshop so user installs link to their template
|
||||
* 7 – add Phase 1 Workshop runtime tracking columns to
|
||||
* server_content_workshop (install_path, install_strategy, enabled,
|
||||
* load_order)
|
||||
*
|
||||
*/
|
||||
|
||||
// Module general information
|
||||
$module_title = "Server Content Manager";
|
||||
$module_version = "2.5";
|
||||
$db_version = 7;
|
||||
$module_required = TRUE;
|
||||
$module_menus = array(
|
||||
array( 'subpage' => 'addons_manager', 'name' => 'Server Content Manager', 'group' => 'admin' )
|
||||
);
|
||||
|
||||
// ── db_version 1 : initial install ───────────────────────────────────────────
|
||||
$install_queries = array();
|
||||
$install_queries[0] = array(
|
||||
"CREATE TABLE IF NOT EXISTS `".OGP_DB_PREFIX."addons` (
|
||||
`addon_id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
`name` VARCHAR(80) NOT NULL,
|
||||
`url` VARCHAR(200) NOT NULL,
|
||||
`path` VARCHAR(80) NOT NULL,
|
||||
`addon_type` VARCHAR(7) NOT NULL,
|
||||
`home_cfg_id` VARCHAR(7) NOT NULL,
|
||||
`post_script` longtext NOT NULL,
|
||||
`group_id` int(11) NULL
|
||||
) ENGINE=MyISAM;"
|
||||
);
|
||||
|
||||
// ── db_version 2 : expand addon_type to VARCHAR(32) ──────────────────────────
|
||||
// Required so extended content types such as 'workshop' (8 chars) can be stored.
|
||||
// MODIFY is safe on existing installs; existing 'plugin'/'mappack'/'config'
|
||||
// values are preserved without alteration.
|
||||
$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;"
|
||||
);
|
||||
|
||||
// ── db_version 4 : Phase 2 – install_method, per-server manifest, install history ──
|
||||
//
|
||||
// Uses a PHP callable so each ALTER is applied only when the column does not
|
||||
// already exist (safe for repeated runs, compatible with all MySQL versions).
|
||||
//
|
||||
$install_queries[3] = array(
|
||||
function ($db) {
|
||||
$prefix = OGP_DB_PREFIX;
|
||||
|
||||
// ── Extend the addons table with Phase 2 columns ──────────────────────
|
||||
$new_columns = array(
|
||||
'install_method' => "VARCHAR(32) NOT NULL DEFAULT 'download_zip' AFTER `group_id`",
|
||||
'content_version' => "VARCHAR(64) NULL AFTER `install_method`",
|
||||
'requires_stop' => "TINYINT(1) NOT NULL DEFAULT 1 AFTER `content_version`",
|
||||
'backup_before_install' => "TINYINT(1) NOT NULL DEFAULT 1 AFTER `requires_stop`",
|
||||
'restart_after_install' => "TINYINT(1) NOT NULL DEFAULT 0 AFTER `backup_before_install`",
|
||||
'is_cacheable' => "TINYINT(1) NOT NULL DEFAULT 0 AFTER `restart_after_install`",
|
||||
'description' => "TEXT NULL AFTER `is_cacheable`",
|
||||
);
|
||||
|
||||
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)) {
|
||||
if (!$db->query("ALTER TABLE `{$prefix}addons` ADD COLUMN `{$col}` {$definition}")) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Per-server installed-content manifest ─────────────────────────────
|
||||
if (!$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"
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// ── Install history (one row per install attempt) ─────────────────────
|
||||
if (!$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 false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
);
|
||||
|
||||
// ── db_version 5 : content-type specific metadata for Workshop/config/folder actions ──
|
||||
$install_queries[4] = array(
|
||||
function ($db) {
|
||||
$prefix = OGP_DB_PREFIX;
|
||||
$new_columns = array(
|
||||
'workshop_item_id' => "VARCHAR(64) NULL AFTER `description`",
|
||||
'workshop_app_id' => "VARCHAR(32) NULL AFTER `workshop_item_id`",
|
||||
'target_path_template' => "VARCHAR(255) NULL AFTER `workshop_app_id`",
|
||||
'optional_folder_name' => "VARCHAR(255) NULL AFTER `target_path_template`",
|
||||
'config_edit_rule' => "TEXT NULL AFTER `optional_folder_name`",
|
||||
'launch_param_additions' => "VARCHAR(255) NULL AFTER `config_edit_rule`",
|
||||
);
|
||||
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)) {
|
||||
if (!$db->query("ALTER TABLE `{$prefix}addons` ADD COLUMN `{$col}` {$definition}")) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
);
|
||||
// ── db_version 6 : admin template policy columns + content_id on workshop rows ──
|
||||
//
|
||||
// allow_user_workshop_ids – whether users may enter their own IDs (default 1)
|
||||
// max_workshop_ids – optional cap on how many IDs a user may install
|
||||
// required_workshop_ids – JSON list of IDs that must always be installed
|
||||
// blocked_workshop_ids – JSON list of IDs that must not be installed
|
||||
// content_id on server_content_workshop – links a user install row back to
|
||||
// the admin content template so the correct workshop_app_id is used.
|
||||
//
|
||||
$install_queries[5] = array(
|
||||
function ($db) {
|
||||
$prefix = OGP_DB_PREFIX;
|
||||
|
||||
// ── New policy columns on the addons (content template) table ─────────
|
||||
$addon_columns = array(
|
||||
'allow_user_workshop_ids' => "TINYINT(1) NOT NULL DEFAULT 1 AFTER `blocked_workshop_ids`",
|
||||
'max_workshop_ids' => "INT NULL AFTER `allow_user_workshop_ids`",
|
||||
'required_workshop_ids' => "TEXT NULL AFTER `max_workshop_ids`",
|
||||
'blocked_workshop_ids' => "TEXT NULL AFTER `launch_param_additions`",
|
||||
);
|
||||
foreach ($addon_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)) {
|
||||
if (!$db->query("ALTER TABLE `{$prefix}addons` ADD COLUMN `{$col}` {$definition}")) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── content_id on server_content_workshop ─────────────────────────────
|
||||
$wk_table = $db->realEscapeSingle($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)) {
|
||||
if (!$db->query(
|
||||
"ALTER TABLE `{$prefix}server_content_workshop`
|
||||
ADD COLUMN `content_id` INT NULL AFTER `id`,
|
||||
ADD KEY `idx_content_id` (`content_id`)"
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
);
|
||||
// ── db_version 7 : Workshop Phase 1 runtime tracking columns ────────────────
|
||||
$install_queries[6] = array(
|
||||
function ($db) {
|
||||
$prefix = OGP_DB_PREFIX;
|
||||
$table = $db->realEscapeSingle($prefix . 'server_content_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`",
|
||||
);
|
||||
foreach ($columns as $col => $definition) {
|
||||
$check = $db->resultQuery(
|
||||
"SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = '{$table}'
|
||||
AND COLUMN_NAME = '" . $db->realEscapeSingle($col) . "'"
|
||||
);
|
||||
if (empty($check)) {
|
||||
if (!$db->query("ALTER TABLE `{$prefix}server_content_workshop` ADD COLUMN `{$col}` {$definition}")) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
);
|
||||
?>
|
||||
|
|
@ -2,5 +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" />
|
||||
<page key="server_content_workshop_disabled" file="server_content_workshop_disabled.php" access="admin,user" />
|
||||
</navigation>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
<navigation>
|
||||
<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>
|
||||
|
|
@ -1,246 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
MANIFEST_PATH="${1:-}"
|
||||
if [[ -z "$MANIFEST_PATH" ]]; then
|
||||
echo "Usage: $0 <manifest_path>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
python3 - "$MANIFEST_PATH" <<'PY'
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from datetime import datetime
|
||||
|
||||
manifest_path = os.path.abspath(sys.argv[1])
|
||||
if not os.path.isfile(manifest_path):
|
||||
print(f"Manifest not found: {manifest_path}")
|
||||
sys.exit(1)
|
||||
|
||||
manifest_dir = os.path.dirname(manifest_path)
|
||||
home_root = os.path.dirname(manifest_dir)
|
||||
log_file = os.path.join(manifest_dir, 'workshop_install.log')
|
||||
removed_dir = os.path.join(manifest_dir, 'workshop', 'removed')
|
||||
os.makedirs(removed_dir, exist_ok=True)
|
||||
|
||||
|
||||
def log(message, status=None):
|
||||
line = f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}]"
|
||||
if status:
|
||||
line += f" [{status}]"
|
||||
line += f" {message}"
|
||||
print(line)
|
||||
with open(log_file, 'a', encoding='utf-8') as handle:
|
||||
handle.write(line + "\n")
|
||||
|
||||
|
||||
def fail(message):
|
||||
log(message, 'Failed')
|
||||
raise RuntimeError(message)
|
||||
|
||||
|
||||
def uniq_numeric_items(raw_items):
|
||||
seen = []
|
||||
for value in raw_items:
|
||||
text = str(value).strip()
|
||||
if text.isdigit() and text not in seen:
|
||||
seen.append(text)
|
||||
return seen
|
||||
|
||||
|
||||
def render_template(template, values):
|
||||
rendered = str(template or '')
|
||||
for key, value in values.items():
|
||||
rendered = rendered.replace('{' + key + '}', str(value))
|
||||
return rendered
|
||||
|
||||
|
||||
def ensure_under_home(path_value):
|
||||
target = os.path.abspath(path_value)
|
||||
try:
|
||||
common = os.path.commonpath([home_root, target])
|
||||
except ValueError:
|
||||
common = ''
|
||||
if common != os.path.abspath(home_root):
|
||||
fail(f"Refusing to write outside server home: {target}")
|
||||
return target
|
||||
|
||||
|
||||
def safe_folder_name(value, fallback):
|
||||
text = str(value or '').strip()
|
||||
if not text or '..' in text or '/' in text or '\\' in text or '\x00' in text:
|
||||
return fallback
|
||||
return text
|
||||
|
||||
|
||||
def truthy(value):
|
||||
if isinstance(value, bool):
|
||||
return value
|
||||
return str(value).strip().lower() in ('1', 'yes', 'true', 'on')
|
||||
|
||||
|
||||
def resolve_steamcmd(explicit_path=''):
|
||||
candidates = []
|
||||
explicit_path = str(explicit_path or '').strip()
|
||||
if explicit_path:
|
||||
candidates.append(explicit_path)
|
||||
env_value = os.environ.get('STEAMCMD_PATH', '').strip()
|
||||
if env_value:
|
||||
candidates.append(env_value)
|
||||
for path_value in (
|
||||
'/home/gameserver/steamcmd/steamcmd.sh',
|
||||
shutil.which('steamcmd'),
|
||||
shutil.which('steamcmd.exe'),
|
||||
):
|
||||
if path_value:
|
||||
candidates.append(path_value)
|
||||
for candidate in candidates:
|
||||
if candidate and os.path.isfile(candidate):
|
||||
return candidate
|
||||
fail('SteamCMD is missing on the agent host.')
|
||||
|
||||
|
||||
def sync_copy(src, dst):
|
||||
if not os.path.isdir(src):
|
||||
fail(f"Workshop download source was not found: {src}")
|
||||
os.makedirs(dst, exist_ok=True)
|
||||
for entry in os.listdir(src):
|
||||
source_entry = os.path.join(src, entry)
|
||||
target_entry = os.path.join(dst, entry)
|
||||
if os.path.isdir(source_entry):
|
||||
sync_copy(source_entry, target_entry)
|
||||
else:
|
||||
os.makedirs(os.path.dirname(target_entry), exist_ok=True)
|
||||
shutil.copy2(source_entry, target_entry)
|
||||
|
||||
|
||||
def copy_bikeys(mod_path, keys_target, workshop_id):
|
||||
if not os.path.isdir(mod_path):
|
||||
return 0
|
||||
keys_target = ensure_under_home(keys_target)
|
||||
os.makedirs(keys_target, exist_ok=True)
|
||||
copied = 0
|
||||
for root, dirs, files in os.walk(mod_path):
|
||||
for filename in files:
|
||||
if not filename.lower().endswith('.bikey'):
|
||||
continue
|
||||
source_file = os.path.join(root, filename)
|
||||
target_file = os.path.join(keys_target, filename)
|
||||
shutil.copy2(source_file, target_file)
|
||||
copied += 1
|
||||
log(f"workshop_id={workshop_id} key={target_file}", 'Copying Key')
|
||||
if copied == 0:
|
||||
log(f"workshop_id={workshop_id} no .bikey files found; continuing", 'Copying Key')
|
||||
return copied
|
||||
|
||||
|
||||
try:
|
||||
with open(manifest_path, 'r', encoding='utf-8') as handle:
|
||||
manifest = json.load(handle)
|
||||
|
||||
extra = manifest.get('extra') or {}
|
||||
action = str(manifest.get('action', '')).strip()
|
||||
raw_items = manifest.get('items', [])
|
||||
if isinstance(raw_items, dict):
|
||||
raw_items = raw_items.get('workshop_item_ids', [])
|
||||
items = uniq_numeric_items(raw_items)
|
||||
if not items:
|
||||
fail('No Workshop IDs were found in the manifest.')
|
||||
|
||||
workshop_app_id = str(extra.get('workshop_app_id') or manifest.get('workshop_app_id') or '').strip()
|
||||
steam_app_id = str(extra.get('steam_app_id') or manifest.get('steam_app_id') or '').strip()
|
||||
server_root = ensure_under_home(extra.get('server_root') or home_root)
|
||||
steamcmd_path = ''
|
||||
if action in ('install', 'update', 'check_updates', 'download_only', 'validate_files'):
|
||||
steamcmd_path = resolve_steamcmd(extra.get('steamcmd_path') or '')
|
||||
post_install_script = str(extra.get('post_install_script') or '').strip()
|
||||
item_details = manifest.get('item_details') or extra.get('item_details') or {}
|
||||
default_install_strategy = str(manifest.get('install_strategy') or extra.get('install_strategy') or 'copy_to_mod_folder').strip()
|
||||
default_download_dir = extra.get('workshop_download_dir') or os.path.join(server_root, 'steamapps', 'workshop', 'content', workshop_app_id or steam_app_id)
|
||||
|
||||
action_label = 'Queued' if action in ('install', 'update', 'check_updates', 'download_only', 'validate_files') else action
|
||||
log(f"action={action} manifest={manifest_path} steam_app_id={steam_app_id or 'n/a'} workshop_app_id={workshop_app_id or 'n/a'}", action_label)
|
||||
|
||||
for workshop_id in items:
|
||||
detail = item_details.get(workshop_id) or item_details.get(str(workshop_id)) or {}
|
||||
folder_name = safe_folder_name(detail.get('folder_name') or extra.get('optional_folder_name') or '', '@' + workshop_id)
|
||||
install_strategy = str(detail.get('install_strategy') or default_install_strategy).strip() or 'copy_to_mod_folder'
|
||||
template_values = {
|
||||
'HOME_ID': manifest.get('home_id', ''),
|
||||
'SERVER_ROOT': server_root,
|
||||
'GAME_ROOT': server_root,
|
||||
'WORKSHOP_ID': workshop_id,
|
||||
'WORKSHOP_APP_ID': workshop_app_id,
|
||||
'STEAM_APP_ID': steam_app_id,
|
||||
'FOLDER_NAME': folder_name,
|
||||
'MOD_FOLDER': folder_name,
|
||||
}
|
||||
target_template = str(detail.get('target_path_template') or extra.get('target_path_template') or '{SERVER_ROOT}/workshop/{MOD_FOLDER}')
|
||||
target_path = str(detail.get('target_path_resolved') or extra.get('target_path_resolved') or '').strip()
|
||||
if len(items) != 1 or not target_path:
|
||||
target_path = render_template(target_template, template_values)
|
||||
target_path = ensure_under_home(target_path)
|
||||
keys_target_path = str(detail.get('keys_target_path') or extra.get('keys_target_path') or os.path.join(server_root, 'keys'))
|
||||
should_copy_keys = truthy(detail.get('copy_keys', extra.get('copy_keys', install_strategy in ('dayz_mod_folder', 'arma_mod_folder'))))
|
||||
|
||||
download_dir = ensure_under_home(render_template(default_download_dir, template_values))
|
||||
source_path = os.path.join(download_dir, workshop_id)
|
||||
|
||||
if action in ('install', 'update', 'check_updates', 'download_only', 'validate_files'):
|
||||
if not workshop_app_id:
|
||||
fail(f"Workshop App ID is missing for Workshop item {workshop_id}.")
|
||||
fd, runscript_path = tempfile.mkstemp(prefix=f"steamcmd_workshop_{workshop_id}_", suffix=".txt", dir=manifest_dir, text=True)
|
||||
with os.fdopen(fd, 'w', encoding='utf-8') as script_handle:
|
||||
script_handle.write("@ShutdownOnFailedCommand 0\n")
|
||||
script_handle.write("@NoPromptForPassword 1\n")
|
||||
script_handle.write(f"force_install_dir {server_root}\n")
|
||||
script_handle.write("login anonymous\n")
|
||||
script_handle.write(f"workshop_download_item {workshop_app_id} {workshop_id} validate\n")
|
||||
script_handle.write("quit\n")
|
||||
command = [steamcmd_path, '+runscript', runscript_path]
|
||||
log(f"workshop_id={workshop_id} runscript={runscript_path}", 'Downloading Workshop Item')
|
||||
result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, cwd=server_root)
|
||||
if result.stdout:
|
||||
for line in result.stdout.splitlines():
|
||||
log(f"steamcmd[{workshop_id}] {line}")
|
||||
if result.returncode != 0:
|
||||
fail(f"SteamCMD failed for Workshop item {workshop_id} with exit code {result.returncode}.")
|
||||
if not os.path.isdir(source_path):
|
||||
fail(f"SteamCMD did not create the expected Workshop cache path: {source_path}")
|
||||
|
||||
if action not in ('check_updates', 'download_only', 'validate_files'):
|
||||
log(f"workshop_id={workshop_id} strategy={install_strategy} install_path={target_path}", 'Extracting/Copying')
|
||||
sync_copy(source_path, target_path)
|
||||
log(f"workshop_id={workshop_id} final_folder_path={target_path}", 'Applying Folder Name')
|
||||
if should_copy_keys:
|
||||
copy_bikeys(target_path, keys_target_path, workshop_id)
|
||||
if post_install_script:
|
||||
log(f"workshop_id={workshop_id} cwd={server_root}", 'Running Post-install Script')
|
||||
post_result = subprocess.run(['bash', '-lc', post_install_script], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, cwd=server_root)
|
||||
if post_result.stdout:
|
||||
for line in post_result.stdout.splitlines():
|
||||
log(f"post_install[{workshop_id}] {line}")
|
||||
if post_result.returncode != 0:
|
||||
fail(f"Post-install script failed for Workshop item {workshop_id} with exit code {post_result.returncode}.")
|
||||
elif action == 'download_only':
|
||||
log(f"workshop_id={workshop_id} cached_path={source_path}", 'Downloaded Only')
|
||||
elif action == 'validate_files':
|
||||
log(f"workshop_id={workshop_id} cached_path={source_path}", 'Validated')
|
||||
log(f"workshop_id={workshop_id} install_path={target_path}", 'Completed')
|
||||
elif action == 'remove':
|
||||
if os.path.exists(target_path):
|
||||
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||
removed_path = os.path.join(removed_dir, f"{workshop_id}_{timestamp}")
|
||||
shutil.move(target_path, removed_path)
|
||||
log(f"workshop_id={workshop_id} removed_path={removed_path}", 'Completed')
|
||||
else:
|
||||
log(f"workshop_id={workshop_id} target_path_missing={target_path}", 'Completed')
|
||||
else:
|
||||
fail(f"Unknown workshop action: {action}")
|
||||
except RuntimeError:
|
||||
sys.exit(1)
|
||||
PY
|
||||
|
|
@ -1,247 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
MANIFEST_PATH="${1:-}"
|
||||
if [[ -z "$MANIFEST_PATH" ]]; then
|
||||
echo "Usage: $0 <manifest_path>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
python3 - "$MANIFEST_PATH" <<'PY'
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from datetime import datetime
|
||||
|
||||
manifest_path = os.path.abspath(sys.argv[1])
|
||||
if not os.path.isfile(manifest_path):
|
||||
print(f"Manifest not found: {manifest_path}")
|
||||
sys.exit(1)
|
||||
|
||||
manifest_dir = os.path.dirname(manifest_path)
|
||||
home_root = os.path.dirname(manifest_dir)
|
||||
log_file = os.path.join(manifest_dir, 'workshop_install_windows.log')
|
||||
removed_dir = os.path.join(manifest_dir, 'workshop', 'removed')
|
||||
os.makedirs(removed_dir, exist_ok=True)
|
||||
|
||||
|
||||
def log(message, status=None):
|
||||
line = f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}]"
|
||||
if status:
|
||||
line += f" [{status}]"
|
||||
line += f" {message}"
|
||||
print(line)
|
||||
with open(log_file, 'a', encoding='utf-8') as handle:
|
||||
handle.write(line + "\n")
|
||||
|
||||
|
||||
def fail(message):
|
||||
log(message, 'Failed')
|
||||
raise RuntimeError(message)
|
||||
|
||||
|
||||
def uniq_numeric_items(raw_items):
|
||||
seen = []
|
||||
for value in raw_items:
|
||||
text = str(value).strip()
|
||||
if text.isdigit() and text not in seen:
|
||||
seen.append(text)
|
||||
return seen
|
||||
|
||||
|
||||
def render_template(template, values):
|
||||
rendered = str(template or '')
|
||||
for key, value in values.items():
|
||||
rendered = rendered.replace('{' + key + '}', str(value))
|
||||
return rendered
|
||||
|
||||
|
||||
def ensure_under_home(path_value):
|
||||
target = os.path.abspath(path_value)
|
||||
try:
|
||||
common = os.path.commonpath([home_root, target])
|
||||
except ValueError:
|
||||
common = ''
|
||||
if common != os.path.abspath(home_root):
|
||||
fail(f"Refusing to write outside server home: {target}")
|
||||
return target
|
||||
|
||||
|
||||
def safe_folder_name(value, fallback):
|
||||
text = str(value or '').strip()
|
||||
if not text or '..' in text or '/' in text or '\\' in text or '\x00' in text:
|
||||
return fallback
|
||||
return text
|
||||
|
||||
|
||||
def truthy(value):
|
||||
if isinstance(value, bool):
|
||||
return value
|
||||
return str(value).strip().lower() in ('1', 'yes', 'true', 'on')
|
||||
|
||||
|
||||
def resolve_steamcmd(explicit_path=''):
|
||||
candidates = []
|
||||
explicit_path = str(explicit_path or '').strip()
|
||||
if explicit_path:
|
||||
candidates.append(explicit_path)
|
||||
env_value = os.environ.get('STEAMCMD_PATH', '').strip()
|
||||
if env_value:
|
||||
candidates.append(env_value)
|
||||
for path_value in (
|
||||
'/home/gameserver/steamcmd/steamcmd.sh',
|
||||
shutil.which('steamcmd'),
|
||||
shutil.which('steamcmd.exe'),
|
||||
shutil.which('steamcmd.sh'),
|
||||
):
|
||||
if path_value:
|
||||
candidates.append(path_value)
|
||||
for candidate in candidates:
|
||||
if candidate and os.path.isfile(candidate):
|
||||
return candidate
|
||||
fail('SteamCMD is missing on the agent host.')
|
||||
|
||||
|
||||
def sync_copy(src, dst):
|
||||
if not os.path.isdir(src):
|
||||
fail(f"Workshop download source was not found: {src}")
|
||||
os.makedirs(dst, exist_ok=True)
|
||||
for entry in os.listdir(src):
|
||||
source_entry = os.path.join(src, entry)
|
||||
target_entry = os.path.join(dst, entry)
|
||||
if os.path.isdir(source_entry):
|
||||
sync_copy(source_entry, target_entry)
|
||||
else:
|
||||
os.makedirs(os.path.dirname(target_entry), exist_ok=True)
|
||||
shutil.copy2(source_entry, target_entry)
|
||||
|
||||
|
||||
def copy_bikeys(mod_path, keys_target, workshop_id):
|
||||
if not os.path.isdir(mod_path):
|
||||
return 0
|
||||
keys_target = ensure_under_home(keys_target)
|
||||
os.makedirs(keys_target, exist_ok=True)
|
||||
copied = 0
|
||||
for root, dirs, files in os.walk(mod_path):
|
||||
for filename in files:
|
||||
if not filename.lower().endswith('.bikey'):
|
||||
continue
|
||||
source_file = os.path.join(root, filename)
|
||||
target_file = os.path.join(keys_target, filename)
|
||||
shutil.copy2(source_file, target_file)
|
||||
copied += 1
|
||||
log(f"workshop_id={workshop_id} key={target_file}", 'Copying Key')
|
||||
if copied == 0:
|
||||
log(f"workshop_id={workshop_id} no .bikey files found; continuing", 'Copying Key')
|
||||
return copied
|
||||
|
||||
|
||||
try:
|
||||
with open(manifest_path, 'r', encoding='utf-8') as handle:
|
||||
manifest = json.load(handle)
|
||||
|
||||
extra = manifest.get('extra') or {}
|
||||
action = str(manifest.get('action', '')).strip()
|
||||
raw_items = manifest.get('items', [])
|
||||
if isinstance(raw_items, dict):
|
||||
raw_items = raw_items.get('workshop_item_ids', [])
|
||||
items = uniq_numeric_items(raw_items)
|
||||
if not items:
|
||||
fail('No Workshop IDs were found in the manifest.')
|
||||
|
||||
workshop_app_id = str(extra.get('workshop_app_id') or manifest.get('workshop_app_id') or '').strip()
|
||||
steam_app_id = str(extra.get('steam_app_id') or manifest.get('steam_app_id') or '').strip()
|
||||
server_root = ensure_under_home(extra.get('server_root') or home_root)
|
||||
steamcmd_path = ''
|
||||
if action in ('install', 'update', 'check_updates', 'download_only', 'validate_files'):
|
||||
steamcmd_path = resolve_steamcmd(extra.get('steamcmd_path') or '')
|
||||
post_install_script = str(extra.get('post_install_script') or '').strip()
|
||||
item_details = manifest.get('item_details') or extra.get('item_details') or {}
|
||||
default_install_strategy = str(manifest.get('install_strategy') or extra.get('install_strategy') or 'copy_to_mod_folder').strip()
|
||||
default_download_dir = extra.get('workshop_download_dir') or os.path.join(server_root, 'steamapps', 'workshop', 'content', workshop_app_id or steam_app_id)
|
||||
|
||||
action_label = 'Queued' if action in ('install', 'update', 'check_updates', 'download_only', 'validate_files') else action
|
||||
log(f"action={action} manifest={manifest_path} steam_app_id={steam_app_id or 'n/a'} workshop_app_id={workshop_app_id or 'n/a'}", action_label)
|
||||
|
||||
for workshop_id in items:
|
||||
detail = item_details.get(workshop_id) or item_details.get(str(workshop_id)) or {}
|
||||
folder_name = safe_folder_name(detail.get('folder_name') or extra.get('optional_folder_name') or '', '@' + workshop_id)
|
||||
install_strategy = str(detail.get('install_strategy') or default_install_strategy).strip() or 'copy_to_mod_folder'
|
||||
template_values = {
|
||||
'HOME_ID': manifest.get('home_id', ''),
|
||||
'SERVER_ROOT': server_root,
|
||||
'GAME_ROOT': server_root,
|
||||
'WORKSHOP_ID': workshop_id,
|
||||
'WORKSHOP_APP_ID': workshop_app_id,
|
||||
'STEAM_APP_ID': steam_app_id,
|
||||
'FOLDER_NAME': folder_name,
|
||||
'MOD_FOLDER': folder_name,
|
||||
}
|
||||
target_template = str(detail.get('target_path_template') or extra.get('target_path_template') or '{SERVER_ROOT}/workshop/{MOD_FOLDER}')
|
||||
target_path = str(detail.get('target_path_resolved') or extra.get('target_path_resolved') or '').strip()
|
||||
if len(items) != 1 or not target_path:
|
||||
target_path = render_template(target_template, template_values)
|
||||
target_path = ensure_under_home(target_path)
|
||||
keys_target_path = str(detail.get('keys_target_path') or extra.get('keys_target_path') or os.path.join(server_root, 'keys'))
|
||||
should_copy_keys = truthy(detail.get('copy_keys', extra.get('copy_keys', install_strategy in ('dayz_mod_folder', 'arma_mod_folder'))))
|
||||
|
||||
download_dir = ensure_under_home(render_template(default_download_dir, template_values))
|
||||
source_path = os.path.join(download_dir, workshop_id)
|
||||
|
||||
if action in ('install', 'update', 'check_updates', 'download_only', 'validate_files'):
|
||||
if not workshop_app_id:
|
||||
fail(f"Workshop App ID is missing for Workshop item {workshop_id}.")
|
||||
fd, runscript_path = tempfile.mkstemp(prefix=f"steamcmd_workshop_{workshop_id}_", suffix=".txt", dir=manifest_dir, text=True)
|
||||
with os.fdopen(fd, 'w', encoding='utf-8') as script_handle:
|
||||
script_handle.write("@ShutdownOnFailedCommand 0\n")
|
||||
script_handle.write("@NoPromptForPassword 1\n")
|
||||
script_handle.write(f"force_install_dir {server_root}\n")
|
||||
script_handle.write("login anonymous\n")
|
||||
script_handle.write(f"workshop_download_item {workshop_app_id} {workshop_id} validate\n")
|
||||
script_handle.write("quit\n")
|
||||
command = [steamcmd_path, '+runscript', runscript_path]
|
||||
log(f"workshop_id={workshop_id} runscript={runscript_path}", 'Downloading Workshop Item')
|
||||
result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, cwd=server_root)
|
||||
if result.stdout:
|
||||
for line in result.stdout.splitlines():
|
||||
log(f"steamcmd[{workshop_id}] {line}")
|
||||
if result.returncode != 0:
|
||||
fail(f"SteamCMD failed for Workshop item {workshop_id} with exit code {result.returncode}.")
|
||||
if not os.path.isdir(source_path):
|
||||
fail(f"SteamCMD did not create the expected Workshop cache path: {source_path}")
|
||||
|
||||
if action not in ('check_updates', 'download_only', 'validate_files'):
|
||||
log(f"workshop_id={workshop_id} strategy={install_strategy} install_path={target_path}", 'Extracting/Copying')
|
||||
sync_copy(source_path, target_path)
|
||||
log(f"workshop_id={workshop_id} final_folder_path={target_path}", 'Applying Folder Name')
|
||||
if should_copy_keys:
|
||||
copy_bikeys(target_path, keys_target_path, workshop_id)
|
||||
if post_install_script:
|
||||
log(f"workshop_id={workshop_id} cwd={server_root}", 'Running Post-install Script')
|
||||
post_result = subprocess.run(['bash', '-lc', post_install_script], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, cwd=server_root)
|
||||
if post_result.stdout:
|
||||
for line in post_result.stdout.splitlines():
|
||||
log(f"post_install[{workshop_id}] {line}")
|
||||
if post_result.returncode != 0:
|
||||
fail(f"Post-install script failed for Workshop item {workshop_id} with exit code {post_result.returncode}.")
|
||||
elif action == 'download_only':
|
||||
log(f"workshop_id={workshop_id} cached_path={source_path}", 'Downloaded Only')
|
||||
elif action == 'validate_files':
|
||||
log(f"workshop_id={workshop_id} cached_path={source_path}", 'Validated')
|
||||
log(f"workshop_id={workshop_id} install_path={target_path}", 'Completed')
|
||||
elif action == 'remove':
|
||||
if os.path.exists(target_path):
|
||||
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||
removed_path = os.path.join(removed_dir, f"{workshop_id}_{timestamp}")
|
||||
shutil.move(target_path, removed_path)
|
||||
log(f"workshop_id={workshop_id} removed_path={removed_path}", 'Completed')
|
||||
else:
|
||||
log(f"workshop_id={workshop_id} target_path_missing={target_path}", 'Completed')
|
||||
else:
|
||||
fail(f"Unknown workshop action: {action}")
|
||||
except RuntimeError:
|
||||
sys.exit(1)
|
||||
PY
|
||||
|
|
@ -1,137 +0,0 @@
|
|||
#!/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_value() {
|
||||
local key="$1"
|
||||
KEY="$key" MANIFEST_FILE="$MANIFEST" perl -0777 -e '
|
||||
my $k = $ENV{"KEY"};
|
||||
my $f = $ENV{"MANIFEST_FILE"};
|
||||
open my $fh, "<", $f or exit 1;
|
||||
local $/;
|
||||
my $json = <$fh>;
|
||||
if ($json =~ /"\Q$k\E"\s*:\s*"([^"]*)"/s) {
|
||||
my $v = $1;
|
||||
$v =~ s/\\\//\//g;
|
||||
print $v;
|
||||
exit 0;
|
||||
}
|
||||
if ($json =~ /"\Q$k\E"\s*:\s*([0-9]+)/s) {
|
||||
print $1;
|
||||
exit 0;
|
||||
}
|
||||
exit 0;
|
||||
'
|
||||
}
|
||||
|
||||
json_items() {
|
||||
MANIFEST_FILE="$MANIFEST" perl -0777 -e '
|
||||
my $f = $ENV{"MANIFEST_FILE"};
|
||||
open my $fh, "<", $f or exit 1;
|
||||
local $/;
|
||||
my $json = <$fh>;
|
||||
|
||||
my %seen;
|
||||
|
||||
if ($json =~ /"items"\s*:\s*\[(.*?)\]/s) {
|
||||
my $x = $1;
|
||||
while ($x =~ /"([0-9]{3,20})"/g) {
|
||||
print "$1\n" unless $seen{$1}++;
|
||||
}
|
||||
while ($x =~ /(?<![0-9])([0-9]{3,20})(?![0-9])/g) {
|
||||
print "$1\n" unless $seen{$1}++;
|
||||
}
|
||||
}
|
||||
'
|
||||
}
|
||||
|
||||
ACTION="$(json_value action)"
|
||||
APPID="$(json_value workshop_app_id)"
|
||||
STEAM_APPID="$(json_value steam_app_id)"
|
||||
SERVER_PATH="$(json_value server_path)"
|
||||
[ -z "$ACTION" ] && ACTION="install"
|
||||
[ -z "$APPID" ] && APPID="$STEAM_APPID"
|
||||
[ -z "$SERVER_PATH" ] && SERVER_PATH="$(pwd)"
|
||||
if [ -z "$APPID" ]; then
|
||||
log "ERROR: workshop_app_id missing from manifest."
|
||||
echo "Manifest debug:"
|
||||
sed -n '1,80p' "$MANIFEST"
|
||||
exit 3
|
||||
fi
|
||||
ITEMS="$(json_items)"
|
||||
if [ -z "$ITEMS" ]; then
|
||||
log "ERROR: no Workshop item IDs found in manifest."
|
||||
echo "Manifest debug:"
|
||||
sed -n '1,80p' "$MANIFEST"
|
||||
exit 4
|
||||
fi
|
||||
find_steamcmd() {
|
||||
for c in "${STEAMCMD_PATH:-}" steamcmd steamcmd.sh steamcmd.exe \
|
||||
/OGP/steamcmd/steamcmd.exe /OGP/steamcmd/steamcmd.sh \
|
||||
"$SERVER_PATH/steamcmd/steamcmd.exe" "$SERVER_PATH/steamcmd/steamcmd.sh" \
|
||||
/home/gameserver/steamcmd/steamcmd.sh "$HOME/steamcmd/steamcmd.sh" "$HOME/steamcmd/steamcmd.exe"
|
||||
do
|
||||
[ -z "$c" ] && continue
|
||||
if command -v "$c" >/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
|
||||
|
|
@ -694,7 +694,6 @@ function scm_get_install_methods()
|
|||
{
|
||||
return array(
|
||||
'download_zip' => 'Downloadable Mod',
|
||||
'steam_workshop' => 'Steam Workshop Mods',
|
||||
'config_edit' => 'Configuration Package',
|
||||
'post_script' => 'Scripted Installer',
|
||||
);
|
||||
|
|
@ -704,7 +703,6 @@ 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.',
|
||||
);
|
||||
|
|
@ -724,7 +722,6 @@ 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.',
|
||||
);
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,487 +1,11 @@
|
|||
<?php
|
||||
/*
|
||||
*
|
||||
* GSP - Workshop Content actions (Phase 1)
|
||||
*
|
||||
/**
|
||||
* Steam Workshop was removed from Server Content.
|
||||
* Use Panel/modules/steam_workshop instead.
|
||||
*/
|
||||
|
||||
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)
|
||||
function scm_workshop_disabled_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_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);
|
||||
$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'] : scm_extract_workshop_steam_app_id($server_xml);
|
||||
if ($workshop_app_id === '' && $steam_app_id !== '') {
|
||||
$workshop_app_id = $steam_app_id;
|
||||
}
|
||||
if ($steam_app_id === '' && $workshop_app_id !== '') {
|
||||
$steam_app_id = $workshop_app_id;
|
||||
}
|
||||
if ($workshop_app_id === '' || !preg_match('/^[0-9]+$/', $workshop_app_id)) {
|
||||
$error = 'Workshop App ID is missing from the game XML workshop_support block.';
|
||||
return false;
|
||||
}
|
||||
$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' => $workshop_app_id,
|
||||
'steam_app_id' => $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;
|
||||
return 'Steam Workshop has been removed from Server Content. Use the Steam Workshop module instead.';
|
||||
}
|
||||
?>
|
||||
|
|
|
|||
|
|
@ -1,308 +1,16 @@
|
|||
<?php
|
||||
/*
|
||||
*
|
||||
* GSP - Server Content Workshop page
|
||||
*
|
||||
* Users enter Steam Workshop IDs or URLs to install on their server.
|
||||
* Game-specific Workshop behavior comes from the game XML workshop_support block.
|
||||
*
|
||||
/**
|
||||
* Steam Workshop was removed from Server Content.
|
||||
* Use Panel/modules/steam_workshop instead.
|
||||
*/
|
||||
|
||||
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'] : '';
|
||||
$addon_id = isset($_REQUEST['addon_id']) ? (int)$_REQUEST['addon_id'] : 0;
|
||||
|
||||
if ($home_id <= 0 || $user_id <= 0) {
|
||||
print_failure(get_lang('no_rights'));
|
||||
echo create_back_button("addonsmanager","user_addons");
|
||||
return;
|
||||
if (!function_exists('print_failure')) {
|
||||
function print_failure($msg) {
|
||||
echo '<div class="failure">'.htmlspecialchars($msg, ENT_QUOTES, 'UTF-8').'</div>';
|
||||
}
|
||||
|
||||
$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;
|
||||
}
|
||||
scm_ensure_phase2_schema($db);
|
||||
$server_xml = read_server_config(SERVER_CONFIG_LOCATION . "/" . $home_info['home_cfg_file']);
|
||||
if ($server_xml === false || !scm_workshop_is_supported($server_xml)) {
|
||||
print_failure('Steam Workshop is not enabled for this game XML. Add a valid workshop_support block before using Workshop management.');
|
||||
echo create_back_button("addonsmanager","user_addons");
|
||||
return;
|
||||
}
|
||||
|
||||
// Template records are optional and used only as labels/history anchors.
|
||||
$addon_template = null;
|
||||
if ($addon_id > 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 "<h2>Workshop Mods: " . scm_h($home_info['home_name']) . "</h2>";
|
||||
if ($addon_template !== null) {
|
||||
echo "<p class='info'>Content template: <strong>" . scm_h($addon_template['name']) . "</strong>";
|
||||
if (!empty($addon_template['description'])) {
|
||||
echo " – " . scm_h($addon_template['description']);
|
||||
}
|
||||
echo "</p>";
|
||||
}
|
||||
echo "<p class='info'>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.</p>";
|
||||
|
||||
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>
|
||||
<tr><td align='right'><strong>Workshop App ID:</strong></td><td align='left'><?php echo scm_h($catalog_app_id); ?></td></tr>
|
||||
<tr><td align='right'><strong>Steam App ID:</strong></td><td align='left'><?php echo scm_h($steam_app_id); ?></td></tr>
|
||||
<tr><td align='right'><strong>Install Strategy:</strong></td><td align='left'><?php echo scm_h($install_strategy); ?></td></tr>
|
||||
</table>
|
||||
|
||||
<h3>Search Workshop</h3>
|
||||
<form method='get' 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='addon_id' value='<?php echo (int)$addon_id; ?>' />
|
||||
<table class='center'>
|
||||
<tr>
|
||||
<td align='right'><strong>Keyword / ID / URL</strong></td>
|
||||
<td align='left'><input type='text' name='workshop_search' size='42' value='<?php echo scm_h($catalog_query); ?>' placeholder='ACE, CBA, Zombies, Maps, or Workshop ID' /></td>
|
||||
<td align='right'><strong>Tag</strong></td>
|
||||
<td align='left'><input type='text' name='workshop_tag' size='24' value='<?php echo scm_h($catalog_tag); ?>' placeholder='Weapons, Missions, Maps' /></td>
|
||||
<td><button type='submit'>Search</button></td>
|
||||
</tr>
|
||||
<?php if ($catalog_app_id !== '' && ($catalog_query !== '' || $catalog_tag !== '')): ?>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td colspan='4' class='info'>
|
||||
<a target='_blank' rel='noopener' href='https://steamcommunity.com/workshop/browse/?appid=<?php echo scm_h($catalog_app_id); ?>&searchtext=<?php echo scm_h(urlencode(trim($catalog_query . ' ' . $catalog_tag))); ?>'>Open matching Steam Workshop search</a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</table>
|
||||
</form>
|
||||
|
||||
<?php if ($catalog_query !== '' || $catalog_tag !== ''): ?>
|
||||
<h3>Workshop Search Results</h3>
|
||||
<p class='info'>
|
||||
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.
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
|
||||
<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='addon_id' value='<?php echo (int)$addon_id; ?>' />
|
||||
<input type='hidden' name='workshop_csrf' value='<?php echo scm_h($csrf_token); ?>' />
|
||||
|
||||
<table class='center'>
|
||||
<tr>
|
||||
<td align='right'><strong>Workshop URLs / IDs</strong></td>
|
||||
<td align='left'>
|
||||
<textarea name='workshop_ids' rows='4' cols='72' placeholder='https://steamcommunity.com/sharedfiles/filedetails/?id=450814997 463939057'><?php echo scm_h($entered_ids); ?></textarea>
|
||||
<br><small style="color:#666;">Enter one or more Steam Workshop URLs or numeric IDs, one per line, comma-separated, or space-separated.<br>Example for Arma 3 CBA_A3: <code>https://steamcommunity.com/sharedfiles/filedetails/?id=450814997</code></small>
|
||||
</td>
|
||||
<td align='left' style='vertical-align:top;padding-top:4px;'>
|
||||
<button type='submit' name='workshop_action' value='install_new'>Install / Queue</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<br>
|
||||
<table class='center'>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Workshop ID</th>
|
||||
<th>Title</th>
|
||||
<th>Enabled</th>
|
||||
<th>Order</th>
|
||||
<th>Update Policy</th>
|
||||
<th>State</th>
|
||||
<th>Install Path</th>
|
||||
<th>Last Installed</th>
|
||||
<th>Last Updated</th>
|
||||
<th>Last Error</th>
|
||||
</tr>
|
||||
<?php if (empty($rows)): ?>
|
||||
<tr><td colspan='11' class='info'>No Workshop items 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 !empty($row['enabled']) ? 'Yes' : 'No'; ?></td>
|
||||
<td><?php echo scm_h(isset($row['load_order']) ? $row['load_order'] : ''); ?></td>
|
||||
<td><?php echo scm_h(isset($row['update_policy']) ? $row['update_policy'] : 'manual'); ?></td>
|
||||
<td><?php echo scm_h($row['install_state']); ?></td>
|
||||
<td><small><?php echo scm_h(isset($row['install_path']) ? $row['install_path'] : ''); ?></small></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>
|
||||
<select name='update_policy'>
|
||||
<?php foreach (scm_get_workshop_update_policies() as $policy_key => $policy_label): ?>
|
||||
<option value='<?php echo scm_h($policy_key); ?>'><?php echo scm_h($policy_label); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</td>
|
||||
<td><button type='submit' name='workshop_action' value='save_update_policy'>Save Policy</button></td>
|
||||
<td><button type='submit' name='workshop_action' value='update_selected'>Update Selected</button></td>
|
||||
<td><button type='submit' name='workshop_action' value='download_selected'>Download 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='enable_selected'>Enable Selected</button></td>
|
||||
<td><button type='submit' name='workshop_action' value='disable_selected'>Disable Selected</button></td>
|
||||
<td><button type='submit' name='workshop_action' value='update_all'>Update All</button></td>
|
||||
</tr>
|
||||
</table>
|
||||
</form>
|
||||
|
||||
<h3>Known Workshop Items</h3>
|
||||
<p class='info'>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.</p>
|
||||
<table class='center'>
|
||||
<tr>
|
||||
<th>Workshop ID</th>
|
||||
<th><a href='?<?php echo scm_h($base_query); ?>&catalog_sort=name'>Name</a></th>
|
||||
<th>Author</th>
|
||||
<th>Thumbnail</th>
|
||||
<th><a href='?<?php echo scm_h($base_query); ?>&catalog_sort=install_count'>Install Count</a></th>
|
||||
<th><a href='?<?php echo scm_h($base_query); ?>&catalog_sort=published_date'>Published</a></th>
|
||||
<th><a href='?<?php echo scm_h($base_query); ?>&catalog_sort=last_updated'>Last Updated</a></th>
|
||||
<th><a href='?<?php echo scm_h($base_query); ?>&catalog_sort=last_installed'>Last Installed</a></th>
|
||||
<th><a href='?<?php echo scm_h($base_query); ?>&catalog_sort=workshop_id'>Sort ID</a></th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
<?php if (empty($catalog_rows)): ?>
|
||||
<tr><td colspan='10' class='info'>No known Workshop items have been installed for this app yet.</td></tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ((array)$catalog_rows as $catalog_row): ?>
|
||||
<tr>
|
||||
<td><?php echo scm_h($catalog_row['workshop_id']); ?></td>
|
||||
<td><?php echo scm_h($catalog_row['title']); ?></td>
|
||||
<td><?php echo scm_h(isset($catalog_row['author']) ? $catalog_row['author'] : ''); ?></td>
|
||||
<td>
|
||||
<?php if (!empty($catalog_row['thumbnail_url'])): ?>
|
||||
<img src='<?php echo scm_h($catalog_row['thumbnail_url']); ?>' alt='' style='max-width:72px;max-height:48px;' />
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?php echo scm_h($catalog_row['install_count']); ?></td>
|
||||
<td><?php echo scm_h($catalog_row['published_date']); ?></td>
|
||||
<td><?php echo scm_h($catalog_row['last_updated']); ?></td>
|
||||
<td><?php echo scm_h($catalog_row['last_installed']); ?></td>
|
||||
<td><?php echo scm_h($catalog_row['workshop_id']); ?></td>
|
||||
<td>
|
||||
<form method='post' action='' style='margin:0;'>
|
||||
<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='addon_id' value='<?php echo (int)$addon_id; ?>' />
|
||||
<input type='hidden' name='workshop_csrf' value='<?php echo scm_h($csrf_token); ?>' />
|
||||
<input type='hidden' name='workshop_ids' value='<?php echo scm_h($catalog_row['workshop_id']); ?>' />
|
||||
<button type='submit' name='workshop_action' value='install_new'>Install</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</table>
|
||||
|
||||
<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
|
||||
}
|
||||
|
||||
echo '<h2>Steam Workshop</h2>';
|
||||
print_failure('Steam Workshop has been removed from Server Content. Use the Steam Workshop module instead.');
|
||||
echo '<p>The working legacy Steam Workshop module handles Workshop installs. Server Content will continue to manage normal downloadable content/addons only.</p>';
|
||||
?>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue