Implement workshop install normalization

Agent-Logs-Url: https://github.com/GameServerPanel/GSP/sessions/b08aa54e-755c-4869-ba97-0f817a05b3c6

Co-authored-by: iaretechnician <2749183+iaretechnician@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2026-05-19 22:36:34 +00:00 committed by GitHub
parent 68d06253fe
commit c471c4b3f8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 827 additions and 236 deletions

View file

@ -1,7 +1,15 @@
$(function() { $(function() {
function replaceTemplate(template, values) {
var output = String(template || '');
$.each(values, function(key, value) {
output = output.split(key).join(value);
});
return output;
}
var methodToRows = { var methodToRows = {
download_zip: ['#scm-row-url', '#scm-row-path'], download_zip: ['#scm-row-url', '#scm-row-path'],
steam_workshop: ['#scm-row-workshop-id', '#scm-row-workshop-app-id', '#scm-row-target-path-template', '#scm-row-optional-folder-name'], steam_workshop: ['#scm-row-workshop-id', '#scm-row-workshop-app-id', '#scm-row-target-path-template', '#scm-row-optional-folder-name', '#scm-row-post-script', '#scm-row-launch-param-additions'],
post_script: ['#scm-row-post-script'], post_script: ['#scm-row-post-script'],
config_edit: ['#scm-row-path', '#scm-row-config-edit-rule'] config_edit: ['#scm-row-path', '#scm-row-config-edit-rule']
}; };
@ -37,4 +45,66 @@ $(function() {
$method.on('change', applyContentTypeUi); $method.on('change', applyContentTypeUi);
applyContentTypeUi(); applyContentTypeUi();
var $userSelect = $('#scm-user-addon-select');
var $userWorkshopRows = $('.scm-user-workshop-row');
var $userWorkshopId = $('#scm-user-workshop-id');
var $userWorkshopAppId = $('#scm-user-workshop-app-id');
var $userTargetTemplate = $('#scm-user-target-path-template');
var $userOptionalFolderName = $('#scm-user-optional-folder-name');
var $userPreview = $('#scm-user-target-path-preview');
function updateUserWorkshopUi() {
if ($userSelect.length === 0) return;
var $selected = $userSelect.find('option:selected');
var installMethod = String($selected.data('installMethod') || '');
var isWorkshop = installMethod === 'steam_workshop';
$userWorkshopRows.toggle(isWorkshop);
if (!isWorkshop) {
return;
}
if (!$userWorkshopId.val()) {
$userWorkshopId.val(String($selected.data('workshopItemId') || ''));
}
if (!$userWorkshopAppId.val()) {
$userWorkshopAppId.val(String($selected.data('workshopAppId') || $userWorkshopAppId.data('defaultAppId') || ''));
}
if (!$userTargetTemplate.val()) {
$userTargetTemplate.val(String($selected.data('targetPathTemplate') || $userTargetTemplate.data('defaultTemplate') || ''));
}
if (!$userOptionalFolderName.val()) {
$userOptionalFolderName.val(String($selected.data('optionalFolderName') || ''));
}
var workshopId = $.trim($userWorkshopId.val());
var workshopAppId = $.trim($userWorkshopAppId.val()) || String($selected.data('workshopAppId') || $userWorkshopAppId.data('defaultAppId') || '');
var folderName = $.trim($userOptionalFolderName.val()) || (workshopId ? '@' + workshopId : '@{WORKSHOP_ID}');
var targetTemplate = $.trim($userTargetTemplate.val()) || String($selected.data('targetPathTemplate') || $userTargetTemplate.data('defaultTemplate') || '');
var previewValues = {
'{SERVER_ROOT}': String($userPreview.data('serverRoot') || ''),
'{GAME_ROOT}': String($userPreview.data('gameRoot') || ''),
'{WORKSHOP_ID}': workshopId || '{WORKSHOP_ID}',
'{WORKSHOP_APP_ID}': workshopAppId || '{WORKSHOP_APP_ID}',
'{STEAM_APP_ID}': String($userPreview.data('steamAppId') || '{STEAM_APP_ID}'),
'{FOLDER_NAME}': folderName,
'{MOD_FOLDER}': folderName
};
$userPreview.text(replaceTemplate(targetTemplate, previewValues));
}
if ($userSelect.length) {
$userSelect.on('change', function() {
$userWorkshopId.val(String($(this).find('option:selected').data('workshopItemId') || ''));
$userWorkshopAppId.val(String($(this).find('option:selected').data('workshopAppId') || ''));
$userTargetTemplate.val(String($(this).find('option:selected').data('targetPathTemplate') || ''));
$userOptionalFolderName.val(String($(this).find('option:selected').data('optionalFolderName') || ''));
updateUserWorkshopUi();
});
$userWorkshopId.on('input', updateUserWorkshopUi);
$userWorkshopAppId.on('input', updateUserWorkshopUi);
$userTargetTemplate.on('input', updateUserWorkshopUi);
$userOptionalFolderName.on('input', updateUserWorkshopUi);
updateUserWorkshopUi();
}
}); });

View file

@ -42,9 +42,9 @@ define('LANG_wait_while_decompressing', "Wait while the file %s is decompressed.
define('LANG_addon_name', "Content Item Name"); define('LANG_addon_name', "Content Item Name");
define('LANG_url', "URL"); define('LANG_url', "URL");
define('LANG_select_game_type', "Select Game Type"); define('LANG_select_game_type', "Select Game Type");
define('LANG_plugin', "Plugins / Mods"); define('LANG_plugin', "Downloadable Mod");
define('LANG_mappack', "Map Packs"); define('LANG_mappack', "Steam Workshop Item");
define('LANG_config', "Config Packs"); define('LANG_config', "Configuration Package");
if (!defined('LANG_version')) { if (!defined('LANG_version')) {
define('LANG_version', "Version"); define('LANG_version', "Version");
} }
@ -93,8 +93,8 @@ define('LANG_target_path_template', "Target Path");
define('LANG_optional_folder_name', "Optional Folder Name"); define('LANG_optional_folder_name', "Optional Folder Name");
define('LANG_config_edit_rule', "Config Edit Rule"); define('LANG_config_edit_rule', "Config Edit Rule");
define('LANG_launch_param_additions', "Launch Parameter Additions"); define('LANG_launch_param_additions', "Launch Parameter Additions");
define('LANG_content_type_help_download_zip', "Downloads an archive/file from URL; extract path is optional."); define('LANG_content_type_help_download_zip', "Download and extract a ZIP, RAR, or archive file.");
define('LANG_content_type_help_steam_workshop', "Installs/updates a Workshop item with Workshop ID (no URL required)."); define('LANG_content_type_help_steam_workshop', "Install a Steam Workshop mod using Workshop ID.");
define('LANG_content_type_help_post_script', "Runs a scripted installer action (no URL required)."); define('LANG_content_type_help_post_script', "Run a custom scripted installation process.");
define('LANG_content_type_help_config_edit', "Edits config at target path using provided action/rules (no URL required)."); define('LANG_content_type_help_config_edit', "Install configuration files, profiles, or templates.");
?> ?>

View file

@ -1,3 +1,4 @@
<script type="text/javascript" src="js/modules/addonsmanager.js"></script>
<?php <?php
/* /*
* *
@ -182,17 +183,13 @@ function exec_ogp_module() {
$install_method = scm_get_install_method_default(isset($addon_info['install_method']) ? $addon_info['install_method'] : 'download_zip'); $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'] : ''; $content_version = isset($addon_info['content_version']) ? $addon_info['content_version'] : '';
$requires_stop = !empty($addon_info['requires_stop']) ? 1 : 0; $requires_stop = !empty($addon_info['requires_stop']) ? 1 : 0;
$user_override_keys = ($install_method === 'steam_workshop')
? array('workshop_item_id', 'workshop_app_id', 'target_path_template', 'optional_folder_name')
: array();
$install_payload = scm_collect_install_payload($addon_info, $_REQUEST, $user_override_keys);
$post_script = ''; $post_script = '';
$validation_payload = array(
'url' => isset($addon_info['url']) ? $addon_info['url'] : '',
'path' => isset($addon_info['path']) ? $addon_info['path'] : '',
'workshop_item_id' => isset($addon_info['workshop_item_id']) ? $addon_info['workshop_item_id'] : '',
'target_path_template' => isset($addon_info['target_path_template']) ? $addon_info['target_path_template'] : '',
'post_script' => isset($addon_info['post_script']) ? $addon_info['post_script'] : '',
'config_edit_rule' => isset($addon_info['config_edit_rule']) ? $addon_info['config_edit_rule'] : '',
);
$validation_message = ''; $validation_message = '';
if ($state == "start" && !scm_validate_install_method_payload($install_method, $validation_payload, $validation_message)) { if ($state == "start" && !scm_validate_install_method_payload($install_method, $install_payload, $validation_message)) {
print_failure($validation_message); print_failure($validation_message);
return; return;
} }
@ -209,7 +206,7 @@ function exec_ogp_module() {
return; return;
} }
} }
$url = $addon_info['url']; $url = $install_payload['url'];
$filename = basename($url); $filename = basename($url);
#### Replace template variables in the post-install script with #### Replace template variables in the post-install script with
#### live server data before sending to the agent. #### live server data before sending to the agent.
@ -285,52 +282,38 @@ function exec_ogp_module() {
'content_type' => $install_method, 'content_type' => $install_method,
'home_id' => (int)$home_id, 'home_id' => (int)$home_id,
'home_cfg_id' => (int)$home_info['home_cfg_id'], 'home_cfg_id' => (int)$home_info['home_cfg_id'],
'workshop_id' => isset($addon_info['workshop_item_id']) ? (string)$addon_info['workshop_item_id'] : '', 'workshop_id' => isset($install_payload['workshop_item_id']) ? (string)$install_payload['workshop_item_id'] : '',
'target_path' => isset($addon_info['target_path_template']) ? (string)$addon_info['target_path_template'] : (string)$addon_info['path'], 'target_path' => ($install_method === 'steam_workshop')
? (string)$install_payload['target_path_template']
: (string)$install_payload['path'],
'action' => 'started', 'action' => 'started',
)); ));
if ($install_method === 'steam_workshop') { if ($install_method === 'steam_workshop') {
scm_ensure_workshop_schema($db); scm_ensure_workshop_schema($db);
$workshop_item_id = trim((string)$addon_info['workshop_item_id']); $workshop_runtime = scm_build_workshop_runtime_context($db, $home_info, $server_xml, $install_payload, $validation_message);
$target_path_template = trim((string)$addon_info['target_path_template']); if ($workshop_runtime === false) {
$resolved = function_exists('steam_workshop_install_item_to_home') print_failure($validation_message);
? steam_workshop_install_item_to_home($db, $home_info, $workshop_item_id, $target_path_template, array( echo "<p><a href=\"?m=addonsmanager&amp;p=user_addons&amp;home_id=$home_id&amp;mod_id=$mod_id&amp;ip=$ip&amp;port=$port\">".get_lang('back')."</a></p>";
'optional_folder_name' => trim((string)$addon_info['optional_folder_name']), return;
'workshop_app_id' => trim((string)$addon_info['workshop_app_id']),
))
: array('ok' => false);
if (empty($resolved['ok'])) {
$fallback_profile = function_exists('sw_get_profile_for_home') ? sw_get_profile_for_home($db, (int)$home_id) : false;
$fallback_workshop_app_id = trim((string)$addon_info['workshop_app_id']);
if ($fallback_workshop_app_id === '' && is_array($fallback_profile) && !empty($fallback_profile['workshop_app_id'])) {
$fallback_workshop_app_id = (string)$fallback_profile['workshop_app_id'];
} }
if ($fallback_workshop_app_id === '') { $workshop_item_id = (string)$workshop_runtime['workshop_item_id'];
$fallback_workshop_app_id = scm_extract_workshop_app_id($server_xml); $target_path_template = (string)$workshop_runtime['target_path_template'];
} $workshop_app_id = (string)$workshop_runtime['workshop_app_id'];
$placeholder_map = scm_build_placeholder_map($home_info, array('exe_location' => (string)$server_xml->exe_location), array( $steam_app_id = (string)$workshop_runtime['steam_app_id'];
'WORKSHOP_ID' => $workshop_item_id, $target_path_resolved = (string)$workshop_runtime['target_path_resolved'];
'WORKSHOP_APP_ID' => $fallback_workshop_app_id,
'STEAM_APP_ID' => (is_array($fallback_profile) && !empty($fallback_profile['steam_app_id'])) ? (string)$fallback_profile['steam_app_id'] : '',
));
$resolved = array(
'workshop_app_id' => $fallback_workshop_app_id,
'steam_app_id' => (is_array($fallback_profile) && !empty($fallback_profile['steam_app_id'])) ? (string)$fallback_profile['steam_app_id'] : '',
'target_path_resolved' => scm_apply_placeholders($target_path_template, $placeholder_map),
);
}
$workshop_app_id = isset($resolved['workshop_app_id']) ? (string)$resolved['workshop_app_id'] : '';
$steam_app_id = isset($resolved['steam_app_id']) ? (string)$resolved['steam_app_id'] : '';
$target_path_resolved = isset($resolved['target_path_resolved']) ? (string)$resolved['target_path_resolved'] : '';
$extra_manifest = array( $extra_manifest = array(
'addon_id' => (int)$addon_id, 'addon_id' => (int)$addon_id,
'target_path_template' => $target_path_template, 'target_path_template' => $target_path_template,
'target_path_resolved' => $target_path_resolved, 'target_path_resolved' => $target_path_resolved,
'optional_folder_name' => trim((string)$addon_info['optional_folder_name']), 'optional_folder_name' => trim((string)$install_payload['optional_folder_name']),
'config_edit_rule' => trim((string)$addon_info['config_edit_rule']), 'config_edit_rule' => trim((string)$addon_info['config_edit_rule']),
'launch_param_additions' => trim((string)$addon_info['launch_param_additions']), 'launch_param_additions' => trim((string)$addon_info['launch_param_additions']),
'workshop_app_id' => $workshop_app_id, 'workshop_app_id' => $workshop_app_id,
'steam_app_id' => $steam_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' => trim((string)$post_script),
); );
$workshop_error = ''; $workshop_error = '';
$workshop_ok = scm_workshop_write_manifest_and_run($db, $home_info, $server_xml, 'install', array($workshop_item_id), $workshop_error, $extra_manifest); $workshop_ok = scm_workshop_write_manifest_and_run($db, $home_info, $server_xml, 'install', array($workshop_item_id), $workshop_error, $extra_manifest);
@ -350,7 +333,10 @@ function exec_ogp_module() {
'home_id' => (int)$home_id, 'home_id' => (int)$home_id,
'home_cfg_id' => (int)$home_info['home_cfg_id'], 'home_cfg_id' => (int)$home_info['home_cfg_id'],
'workshop_id' => $workshop_item_id, 'workshop_id' => $workshop_item_id,
'steam_app_id' => $steam_app_id,
'workshop_app_id' => $workshop_app_id,
'target_path' => $target_path_resolved, 'target_path' => $target_path_resolved,
'final_folder_path' => $target_path_resolved,
'action' => 'succeeded', 'action' => 'succeeded',
)); ));
print_success(get_lang('addon_installed_successfully')); print_success(get_lang('addon_installed_successfully'));
@ -363,6 +349,8 @@ function exec_ogp_module() {
'home_id' => (int)$home_id, 'home_id' => (int)$home_id,
'home_cfg_id' => (int)$home_info['home_cfg_id'], 'home_cfg_id' => (int)$home_info['home_cfg_id'],
'workshop_id' => $workshop_item_id, 'workshop_id' => $workshop_item_id,
'steam_app_id' => $steam_app_id,
'workshop_app_id' => $workshop_app_id,
'target_path' => $target_path_resolved, 'target_path' => $target_path_resolved,
'action' => 'failed', 'action' => 'failed',
'error' => $workshop_error, 'error' => $workshop_error,
@ -558,10 +546,28 @@ function exec_ogp_module() {
} }
?> ?>
<?php <?php
$addon_type_lang_key = "server_content_".$addon_type; $category_labels = get_server_content_categories();
$addon_type_lang = get_lang($addon_type_lang_key); $addon_type_lang = isset($category_labels[$addon_type]) ? $category_labels[$addon_type] : ucfirst(str_replace('_', ' ', $addon_type));
if($addon_type_lang === "_".$addon_type_lang_key."_") $addons = $db->resultQuery(
$addon_type_lang = get_lang($addon_type); "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');
$workshop_profile = function_exists('sw_get_profile_for_home') ? sw_get_profile_for_home($db, (int)$home_id) : false;
$default_workshop_app_id = !empty($selected_addon['workshop_app_id'])
? trim((string)$selected_addon['workshop_app_id'])
: ((is_array($workshop_profile) && !empty($workshop_profile['workshop_app_id'])) ? (string)$workshop_profile['workshop_app_id'] : scm_extract_workshop_app_id($server_xml));
$default_target_template = !empty($selected_addon['target_path_template'])
? trim((string)$selected_addon['target_path_template'])
: ((is_array($workshop_profile) && !empty($workshop_profile['install_path_template'])) ? (string)$workshop_profile['install_path_template'] : '{SERVER_ROOT}/{MOD_FOLDER}');
$default_optional_folder_name = !empty($selected_addon['optional_folder_name']) ? trim((string)$selected_addon['optional_folder_name']) : '';
?> ?>
<h2><?php echo htmlentities($home_info['home_name'])."&nbsp;".$addon_type_lang ;?></h2> <h2><?php echo htmlentities($home_info['home_name'])."&nbsp;".$addon_type_lang ;?></h2>
<table class='center'> <table class='center'>
@ -579,21 +585,40 @@ function exec_ogp_module() {
<td align='left'><?php echo "$home_info[remote_server_name] ($home_info[agent_ip]:$home_info[agent_port])"; ?></td></tr> <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> <tr><td align='right'><?php print_lang('select_addon'); ?>: </td>
<td align='left'> <td align='left'>
<select name="addon_id"> <select name="addon_id" id="scm-user-addon-select">
<?php <?php foreach ((array)$addons as $addon) { ?>
$addons = $db->resultQuery("SELECT addon_id, name FROM OGP_DB_PREFIXaddons WHERE addon_type='".$addon_type."' AND home_cfg_id=" . $home_cfg_id . $query_groups . " ORDER BY name ASC"); <option
if (!is_array($addons)) { value="<?php echo (int)$addon['addon_id']; ?>"
$addons = []; 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'); ?>"
foreach ((array)$addons as $addon) data-workshop-app-id="<?php echo htmlspecialchars(isset($addon['workshop_app_id']) ? $addon['workshop_app_id'] : '', ENT_QUOTES, 'UTF-8'); ?>"
{ data-target-path-template="<?php echo htmlspecialchars(isset($addon['target_path_template']) ? $addon['target_path_template'] : '', ENT_QUOTES, 'UTF-8'); ?>"
?> data-optional-folder-name="<?php echo htmlspecialchars(isset($addon['optional_folder_name']) ? $addon['optional_folder_name'] : '', ENT_QUOTES, 'UTF-8'); ?>"
<option value="<?php echo $addon['addon_id']; ?>"><?php echo $addon['name']; ?></option> ><?php echo htmlspecialchars($addon['name']); ?></option>
<?php <?php } ?>
}
?>
</select> </select>
</td></tr> </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>Workshop App ID Override</strong></td><td align='left'>
<input type="text" id="scm-user-workshop-app-id" name="workshop_app_id" size="50" value="<?php echo htmlspecialchars($default_workshop_app_id, ENT_QUOTES, 'UTF-8'); ?>" data-default-app-id="<?php echo htmlspecialchars($default_workshop_app_id, ENT_QUOTES, 'UTF-8'); ?>" placeholder="Optional App ID override" />
</td></tr>
<tr class="scm-user-workshop-row" <?php echo $is_workshop_default ? '' : 'style="display:none;"'; ?>><td align='right'><strong><?php print_lang('optional_folder_name'); ?></strong></td><td align='left'>
<input type="text" id="scm-user-optional-folder-name" name="optional_folder_name" size="50" value="<?php echo htmlspecialchars($default_optional_folder_name, ENT_QUOTES, 'UTF-8'); ?>" placeholder="@myWorkshopMod (optional)" />
</td></tr>
<tr class="scm-user-workshop-row" <?php echo $is_workshop_default ? '' : 'style="display:none;"'; ?>><td align='right'><strong><?php print_lang('target_path_template'); ?></strong></td><td align='left'>
<input type="text" id="scm-user-target-path-template" name="target_path_template" size="85" value="<?php echo htmlspecialchars($default_target_template, ENT_QUOTES, 'UTF-8'); ?>" data-default-template="<?php echo htmlspecialchars($default_target_template, ENT_QUOTES, 'UTF-8'); ?>" placeholder="{SERVER_ROOT}/{MOD_FOLDER}" />
<div class="info" style="margin-top:4px;">Supported placeholders: {SERVER_ROOT}, {GAME_ROOT}, {WORKSHOP_ID}, {WORKSHOP_APP_ID}, {STEAM_APP_ID}, {FOLDER_NAME}, {MOD_FOLDER}</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((is_array($workshop_profile) && !empty($workshop_profile['steam_app_id'])) ? (string)$workshop_profile['steam_app_id'] : '', ENT_QUOTES, 'UTF-8'); ?>"
><?php echo htmlspecialchars($default_target_template, ENT_QUOTES, 'UTF-8'); ?></code>
</td></tr>
<tr><td colspan='2' class='info'>&nbsp;</td></tr> <tr><td colspan='2' class='info'>&nbsp;</td></tr>
<td align='left'> <td align='left'>
&nbsp; &nbsp;

View file

@ -202,6 +202,7 @@ function exec_ogp_module() {
</td> </td>
<td align="left"> <td align="left">
<input type="text" value="<?php echo htmlspecialchars($workshop_item_id, ENT_QUOTES, 'UTF-8'); ?>" name="workshop_item_id" size="85" placeholder="e.g. 450814997" /> <input type="text" value="<?php echo htmlspecialchars($workshop_item_id, ENT_QUOTES, 'UTF-8'); ?>" name="workshop_item_id" size="85" placeholder="e.g. 450814997" />
<small style="color:#666;">Example Arma 3 Workshop ID: 450814997</small>
</td> </td>
</tr> </tr>
<tr id="scm-row-workshop-app-id"> <tr id="scm-row-workshop-app-id">
@ -217,8 +218,8 @@ function exec_ogp_module() {
<b><?php print_lang('target_path_template'); ?></b> <b><?php print_lang('target_path_template'); ?></b>
</td> </td>
<td align="left"> <td align="left">
<input type="text" value="<?php echo htmlspecialchars($target_path_template, ENT_QUOTES, 'UTF-8'); ?>" name="target_path_template" size="85" placeholder="{SERVER_ROOT}/mods/{WORKSHOP_ID}" /> <input type="text" value="<?php echo htmlspecialchars($target_path_template, ENT_QUOTES, 'UTF-8'); ?>" name="target_path_template" size="85" placeholder="{SERVER_ROOT}/{MOD_FOLDER}" />
<small style="color:#666;">Supported placeholders: {HOME_ID}, {SERVER_ROOT}, {GAME_ROOT}, {WORKSHOP_ID}, {WORKSHOP_APP_ID}, {STEAM_APP_ID}</small> <small style="color:#666;">Supported placeholders: {HOME_ID}, {SERVER_ROOT}, {GAME_ROOT}, {WORKSHOP_ID}, {WORKSHOP_APP_ID}, {STEAM_APP_ID}, {FOLDER_NAME}, {MOD_FOLDER}</small>
</td> </td>
</tr> </tr>
<tr id="scm-row-optional-folder-name"> <tr id="scm-row-optional-folder-name">

View file

@ -7,57 +7,190 @@ if [[ -z "$MANIFEST_PATH" ]]; then
exit 1 exit 1
fi fi
if [[ ! -f "$MANIFEST_PATH" ]]; then python3 - "$MANIFEST_PATH" <<'PY'
echo "Manifest not found: $MANIFEST_PATH" import json
exit 1 import os
fi import shutil
import subprocess
import sys
from datetime import datetime
MANIFEST_DIR="$(dirname "$MANIFEST_PATH")" manifest_path = os.path.abspath(sys.argv[1])
WORKSHOP_DIR="${MANIFEST_DIR}/workshop" if not os.path.isfile(manifest_path):
REMOVED_DIR="${WORKSHOP_DIR}/removed" print(f"Manifest not found: {manifest_path}")
LOG_FILE="${MANIFEST_DIR}/workshop_phase1.log" sys.exit(1)
mkdir -p "$WORKSHOP_DIR" "$REMOVED_DIR" 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)
ACTION="$(python3 - <<'PY' "$MANIFEST_PATH"
import json,sys def log(message, status=None):
with open(sys.argv[1], "r", encoding="utf-8") as f: line = f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}]"
data=json.load(f) if status:
print(data.get("action","")) 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 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)
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 = resolve_steamcmd(extra.get('steamcmd_path') or '')
post_install_script = str(extra.get('post_install_script') or '').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') 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:
folder_name = str(extra.get('optional_folder_name') or '').strip() or ('@' + workshop_id)
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(extra.get('target_path_template') or '{SERVER_ROOT}/{MOD_FOLDER}')
target_path = str(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)
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'):
if not workshop_app_id:
fail(f"Workshop App ID is missing for Workshop item {workshop_id}.")
command = [
steamcmd_path,
'+force_install_dir', server_root,
'+login', 'anonymous',
'+workshop_download_item', workshop_app_id, workshop_id, 'validate',
'+quit',
]
log(f"workshop_id={workshop_id} steamcmd={' '.join(command)}", '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 != 'check_updates':
log(f"workshop_id={workshop_id} 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 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}.")
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 PY
)"
ITEMS="$(python3 - <<'PY' "$MANIFEST_PATH"
import json,sys
with open(sys.argv[1], "r", encoding="utf-8") as f:
data=json.load(f)
items=data.get("items",[])
print(",".join(str(x) for x in items if str(x).isdigit()))
PY
)"
{
echo "[$(date '+%Y-%m-%d %H:%M:%S')] workshop_phase1 action=${ACTION} manifest=${MANIFEST_PATH}"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] workshop_phase1 items=${ITEMS}"
} >> "$LOG_FILE"
case "$ACTION" in
install|update)
# TODO: Replace with game-specific SteamCMD workshop install/update logic.
# Example flow:
# 1) Use workshop_app_id + item IDs from the manifest.
# 2) Download/refresh content into a controlled staging folder.
# 3) Copy/sync approved files into the game server content path.
;;
remove)
# Phase 1 safety behavior: avoid destructive delete.
# TODO: move/disable per-item content using game-specific mapping rules.
echo "[$(date '+%Y-%m-%d %H:%M:%S')] remove requested; preserving files (non-destructive phase 1)." >> "$LOG_FILE"
;;
*)
echo "Unknown workshop action: ${ACTION}" >> "$LOG_FILE"
exit 1
;;
esac
exit 0

View file

@ -7,52 +7,191 @@ if [[ -z "$MANIFEST_PATH" ]]; then
exit 1 exit 1
fi fi
if [[ ! -f "$MANIFEST_PATH" ]]; then python3 - "$MANIFEST_PATH" <<'PY'
echo "Manifest not found: $MANIFEST_PATH" import json
exit 1 import os
fi import shutil
import subprocess
import sys
from datetime import datetime
MANIFEST_DIR="$(dirname "$MANIFEST_PATH")" manifest_path = os.path.abspath(sys.argv[1])
WORKSHOP_DIR="${MANIFEST_DIR}/workshop" if not os.path.isfile(manifest_path):
REMOVED_DIR="${WORKSHOP_DIR}/removed" print(f"Manifest not found: {manifest_path}")
LOG_FILE="${MANIFEST_DIR}/workshop_phase1_windows.log" sys.exit(1)
mkdir -p "$WORKSHOP_DIR" "$REMOVED_DIR" 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)
ACTION="$(python3 - <<'PY' "$MANIFEST_PATH"
import json,sys def log(message, status=None):
with open(sys.argv[1], "r", encoding="utf-8") as f: line = f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}]"
data=json.load(f) if status:
print(data.get("action","")) 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 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)
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 = resolve_steamcmd(extra.get('steamcmd_path') or '')
post_install_script = str(extra.get('post_install_script') or '').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') 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:
folder_name = str(extra.get('optional_folder_name') or '').strip() or ('@' + workshop_id)
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(extra.get('target_path_template') or '{SERVER_ROOT}/{MOD_FOLDER}')
target_path = str(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)
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'):
if not workshop_app_id:
fail(f"Workshop App ID is missing for Workshop item {workshop_id}.")
command = [
steamcmd_path,
'+force_install_dir', server_root,
'+login', 'anonymous',
'+workshop_download_item', workshop_app_id, workshop_id, 'validate',
'+quit',
]
log(f"workshop_id={workshop_id} steamcmd={' '.join(command)}", '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 != 'check_updates':
log(f"workshop_id={workshop_id} 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 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}.")
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 PY
)"
ITEMS="$(python3 - <<'PY' "$MANIFEST_PATH"
import json,sys
with open(sys.argv[1], "r", encoding="utf-8") as f:
data=json.load(f)
items=data.get("items",[])
print(",".join(str(x) for x in items if str(x).isdigit()))
PY
)"
{
echo "[$(date '+%Y-%m-%d %H:%M:%S')] workshop_phase1_windows action=${ACTION} manifest=${MANIFEST_PATH}"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] workshop_phase1_windows items=${ITEMS}"
} >> "$LOG_FILE"
case "$ACTION" in
install|update)
# TODO: Replace with game-specific SteamCMD workshop install/update logic for Cygwin environments.
;;
remove)
# Phase 1 safety behavior: avoid destructive delete.
echo "[$(date '+%Y-%m-%d %H:%M:%S')] remove requested; preserving files (non-destructive phase 1)." >> "$LOG_FILE"
;;
*)
echo "Unknown workshop action: ${ACTION}" >> "$LOG_FILE"
exit 1
;;
esac
exit 0

View file

@ -176,7 +176,15 @@ function server_content_execute_manifest($home_id, $manifest_path, $script_key,
if ($remote->status_chk() !== 1) { if ($remote->status_chk() !== 1) {
return server_content_result('failed', 'Agent is offline.', array('remote_server_id' => (int)$home_info['remote_server_id'])); return server_content_result('failed', 'Agent is offline.', array('remote_server_id' => (int)$home_info['remote_server_id']));
} }
if ((int)$remote->rfile_exists($script_path) !== 1) { if ($script_key === 'workshop') {
$prepare_error = '';
$prepared_path = scm_prepare_workshop_script_for_agent($remote, $home_info, $server_xml, $prepare_error);
if ($prepared_path === false) {
return server_content_result('failed', $prepare_error, array('script_key' => (string)$script_key));
}
$script_path = $prepared_path;
}
elseif ((int)$remote->rfile_exists($script_path) !== 1) {
return server_content_result('failed', 'Server content script was not found on agent host.', array('script_path' => $script_path)); return server_content_result('failed', 'Server content script was not found on agent host.', array('script_path' => $script_path));
} }
$command = "bash " . escapeshellarg($script_path) . " " . escapeshellarg((string)$manifest_path) . " ; echo __GSP_SERVER_CONTENT_EXIT:$?"; $command = "bash " . escapeshellarg($script_path) . " " . escapeshellarg((string)$manifest_path) . " ; echo __GSP_SERVER_CONTENT_EXIT:$?";
@ -469,4 +477,3 @@ function server_content_run_scheduled_action($home_id, $action, $options = array
server_content_log_action($home_id, $action, $result['status'], $result['message'], $result['details']); server_content_log_action($home_id, $action, $result['status'], $result['message'], $result['details']);
return $result; return $result;
} }

View file

@ -40,9 +40,9 @@
function get_server_content_categories() function get_server_content_categories()
{ {
return array( return array(
'file_download' => 'File Download / Archive', 'file_download' => 'Downloadable Mod',
'workshop_item' => 'Steam Workshop Item', 'workshop_item' => 'Steam Workshop Item',
'config_edit' => 'Config Edit', 'config_edit' => 'Configuration Package',
'scripted_installer' => 'Scripted Installer', 'scripted_installer' => 'Scripted Installer',
); );
} }

View file

@ -6,10 +6,10 @@
*/ */
if (!defined('SCM_WORKSHOP_SCRIPT_LINUX_DEFAULT')) { if (!defined('SCM_WORKSHOP_SCRIPT_LINUX_DEFAULT')) {
define('SCM_WORKSHOP_SCRIPT_LINUX_DEFAULT', '/var/www/html/GSP/Panel/modules/addonsmanager/scripts/workshop/generic_steam_workshop_linux.sh'); define('SCM_WORKSHOP_SCRIPT_LINUX_DEFAULT', 'generic_steam_workshop_linux.sh');
} }
if (!defined('SCM_WORKSHOP_SCRIPT_WINDOWS_DEFAULT')) { if (!defined('SCM_WORKSHOP_SCRIPT_WINDOWS_DEFAULT')) {
define('SCM_WORKSHOP_SCRIPT_WINDOWS_DEFAULT', '/var/www/html/GSP/Panel/modules/addonsmanager/scripts/workshop/generic_steam_workshop_windows_cygwin.sh'); define('SCM_WORKSHOP_SCRIPT_WINDOWS_DEFAULT', 'generic_steam_workshop_windows_cygwin.sh');
} }
function scm_ensure_workshop_schema($db) function scm_ensure_workshop_schema($db)
@ -182,6 +182,73 @@ function scm_get_workshop_script_path(array $home_info, $server_xml)
return scm_is_windows_home($home_info) ? SCM_WORKSHOP_SCRIPT_WINDOWS_DEFAULT : SCM_WORKSHOP_SCRIPT_LINUX_DEFAULT; return scm_is_windows_home($home_info) ? SCM_WORKSHOP_SCRIPT_WINDOWS_DEFAULT : SCM_WORKSHOP_SCRIPT_LINUX_DEFAULT;
} }
function scm_get_bundled_workshop_script_source(array $home_info)
{
$filename = scm_is_windows_home($home_info) ? SCM_WORKSHOP_SCRIPT_WINDOWS_DEFAULT : SCM_WORKSHOP_SCRIPT_LINUX_DEFAULT;
return dirname(__FILE__) . '/scripts/workshop/' . $filename;
}
function scm_is_legacy_panel_workshop_script_path($script_path)
{
$script_path = trim((string)$script_path);
if ($script_path === '') {
return false;
}
return (strpos($script_path, '/var/www/html/') === 0 && strpos($script_path, '/Panel/modules/addonsmanager/scripts/workshop/') !== false)
|| (strpos($script_path, '/OGP_User_Files/modules/addonsmanager/scripts/') !== false);
}
function scm_get_agent_managed_workshop_script_path(array $home_info)
{
$home_path = rtrim(clean_path((string)$home_info['home_path']), '/');
$filename = scm_is_windows_home($home_info) ? SCM_WORKSHOP_SCRIPT_WINDOWS_DEFAULT : SCM_WORKSHOP_SCRIPT_LINUX_DEFAULT;
$remote_path = clean_path($home_path . '/gsp_server_content/scripts/workshop/' . $filename);
if (!scm_path_is_under_home($home_path, $remote_path)) {
return false;
}
return $remote_path;
}
function scm_prepare_workshop_script_for_agent($remote, array $home_info, $server_xml, &$error = '')
{
$error = '';
$configured_path = trim((string)scm_get_workshop_script_path($home_info, $server_xml));
if ($configured_path !== '' && !scm_is_legacy_panel_workshop_script_path($configured_path) && preg_match('/^[^\\r\\n\\0]+$/', $configured_path)) {
if ((int)$remote->rfile_exists($configured_path) === 1) {
return $configured_path;
}
$error = 'Configured workshop script not found on agent host: ' . $configured_path;
return false;
}
$source_path = scm_get_bundled_workshop_script_source($home_info);
if (!is_file($source_path)) {
$error = 'Bundled workshop script is missing from the panel: ' . $source_path;
return false;
}
$remote_path = scm_get_agent_managed_workshop_script_path($home_info);
if ($remote_path === false) {
$error = 'Unable to resolve an agent-managed workshop script path for this server.';
return false;
}
$script_body = @file_get_contents($source_path);
if ($script_body === false || $script_body === '') {
$error = 'Failed to read bundled workshop script: ' . $source_path;
return false;
}
$remote_dir = dirname($remote_path);
$remote->exec("mkdir -p " . escapeshellarg($remote_dir));
if ((int)$remote->remote_writefile($remote_path, $script_body) !== 1) {
$error = 'Failed to sync workshop script to agent host.';
return false;
}
$remote->exec("chmod 755 " . escapeshellarg($remote_path) . " >/dev/null 2>&1 || true");
return $remote_path;
}
function scm_get_csrf_token() function scm_get_csrf_token()
{ {
if (empty($_SESSION['addonsmanager_workshop_csrf'])) { if (empty($_SESSION['addonsmanager_workshop_csrf'])) {
@ -255,9 +322,9 @@ function scm_get_cache_mode($db)
function scm_get_install_methods() function scm_get_install_methods()
{ {
return array( return array(
'download_zip' => 'File Download / Archive', 'download_zip' => 'Downloadable Mod',
'steam_workshop' => 'Steam Workshop Item', 'steam_workshop' => 'Steam Workshop Item',
'config_edit' => 'Config Edit', 'config_edit' => 'Configuration Package',
'post_script' => 'Scripted Installer', 'post_script' => 'Scripted Installer',
); );
} }
@ -265,10 +332,10 @@ function scm_get_install_methods()
function scm_get_install_method_help_text() function scm_get_install_method_help_text()
{ {
return array( return array(
'download_zip' => 'Downloads an archive or file from URL; extract path is optional.', 'download_zip' => 'Download and extract a ZIP, RAR, or archive file.',
'steam_workshop' => 'Installs a Steam Workshop item by Workshop ID without requiring URL.', 'steam_workshop' => 'Install a Steam Workshop mod using Workshop ID.',
'config_edit' => 'Applies config edits to the target file/path without requiring URL.', 'config_edit' => 'Install configuration files, profiles, or templates.',
'post_script' => 'Runs an installer script/action body without requiring URL.', 'post_script' => 'Run a custom scripted installation process.',
); );
} }
@ -305,52 +372,201 @@ function scm_get_install_method_default($value = '')
return isset($methods[$value]) ? $value : 'download_zip'; return isset($methods[$value]) ? $value : 'download_zip';
} }
function scm_validate_install_method_payload($install_method, array $payload, &$message = '') function scm_get_install_payload_keys()
{ {
$install_method = scm_get_install_method_default($install_method); return array(
$required = scm_get_install_method_required_fields(); 'url',
$errors = scm_get_install_method_validation_errors(); 'path',
if (!isset($required[$install_method])) { 'workshop_item_id',
$message = 'Invalid install/content type selected.'; 'workshop_app_id',
'target_path_template',
'optional_folder_name',
'post_script',
'config_edit_rule',
'launch_param_additions',
);
}
function scm_collect_install_payload(array $defaults = array(), array $request = array(), array $override_keys = array())
{
$payload = array();
foreach (scm_get_install_payload_keys() as $key) {
$payload[$key] = isset($defaults[$key]) ? trim((string)$defaults[$key]) : '';
}
foreach ($override_keys as $key) {
if (array_key_exists($key, $request)) {
$payload[$key] = trim((string)$request[$key]);
}
}
return $payload;
}
function scm_validate_numeric_content_value($value, $error_message, &$message, $allow_blank = false)
{
$value = trim((string)$value);
if ($value === '') {
if ($allow_blank) {
$message = '';
return true;
}
$message = $error_message;
return false; return false;
} }
if ($install_method === 'config_edit') { if (!preg_match('/^[0-9]+$/', $value)) {
$message = $error_message;
return false;
}
$message = '';
return true;
}
function scm_validate_download_content(array $payload, &$message = '')
{
$url = isset($payload['url']) ? trim((string)$payload['url']) : '';
if ($url === '') {
$message = 'Please enter a download URL.';
return false;
}
$message = '';
return true;
}
function scm_validate_workshop_content(array $payload, &$message = '')
{
if (!scm_validate_numeric_content_value(isset($payload['workshop_item_id']) ? $payload['workshop_item_id'] : '', 'Please enter a Workshop ID.', $message, false)) {
return false;
}
if (!scm_validate_numeric_content_value(isset($payload['workshop_app_id']) ? $payload['workshop_app_id'] : '', 'Workshop App ID must be numeric.', $message, true)) {
return false;
}
$folder_name = isset($payload['optional_folder_name']) ? trim((string)$payload['optional_folder_name']) : '';
if ($folder_name !== '' && (strpos($folder_name, '..') !== false || preg_match('/[\\\\\\/]/', $folder_name))) {
$message = 'Optional folder name must be a single folder name.';
return false;
}
$message = '';
return true;
}
function scm_validate_scripted_installer(array $payload, &$message = '')
{
$script = isset($payload['post_script']) ? trim((string)$payload['post_script']) : '';
if ($script === '') {
$message = 'Please enter the installer script/action.';
return false;
}
$message = '';
return true;
}
function scm_validate_configuration_package(array $payload, &$message = '')
{
$path = isset($payload['path']) ? trim((string)$payload['path']) : ''; $path = isset($payload['path']) ? trim((string)$payload['path']) : '';
$rule = isset($payload['config_edit_rule']) ? trim((string)$payload['config_edit_rule']) : ''; $rule = isset($payload['config_edit_rule']) ? trim((string)$payload['config_edit_rule']) : '';
if ($path === '' || $rule === '') { if ($path === '' || $rule === '') {
$message = $errors['config_edit']; $message = 'Please enter the config target and edit action.';
return false; return false;
} }
$message = ''; $message = '';
return true; return true;
} }
if ($install_method === 'post_script') {
$script = isset($payload['post_script']) ? trim((string)$payload['post_script']) : ''; function scm_validate_install_method_payload($install_method, array $payload, &$message = '')
if ($script === '') { {
$message = $errors['post_script']; $install_method = scm_get_install_method_default($install_method);
return false; if (!isset(scm_get_install_method_required_fields()[$install_method])) {
} $message = 'Invalid install/content type selected.';
$message = '';
return true;
}
foreach ($required[$install_method] as $field) {
$value = isset($payload[$field]) ? trim((string)$payload[$field]) : '';
if ($value === '') {
$message = isset($errors[$install_method]) ? $errors[$install_method] : 'Missing required field.';
return false; return false;
} }
if ($install_method === 'download_zip') {
return scm_validate_download_content($payload, $message);
} }
if ($install_method === 'steam_workshop') { if ($install_method === 'steam_workshop') {
$wid = isset($payload['workshop_item_id']) ? trim((string)$payload['workshop_item_id']) : ''; return scm_validate_workshop_content($payload, $message);
if ($wid === '' || !preg_match('/^[0-9]+$/', $wid)) {
$message = 'Please enter a Workshop ID.';
return false;
} }
if ($install_method === 'post_script') {
return scm_validate_scripted_installer($payload, $message);
}
if ($install_method === 'config_edit') {
return scm_validate_configuration_package($payload, $message);
} }
$message = ''; $message = '';
return true; return true;
} }
function scm_build_workshop_runtime_context($db, array $home_info, $server_xml, array $payload, &$message = '')
{
if (!scm_validate_workshop_content($payload, $message)) {
return false;
}
$workshop_item_id = trim((string)$payload['workshop_item_id']);
$target_path_template = trim((string)$payload['target_path_template']);
$optional_folder_name = trim((string)$payload['optional_folder_name']);
$workshop_app_id_override = trim((string)$payload['workshop_app_id']);
$fallback_profile = function_exists('sw_get_profile_for_home') ? sw_get_profile_for_home($db, (int)$home_info['home_id']) : false;
$resolved = function_exists('steam_workshop_install_item_to_home')
? steam_workshop_install_item_to_home($db, $home_info, $workshop_item_id, $target_path_template, array(
'optional_folder_name' => $optional_folder_name,
'workshop_app_id' => $workshop_app_id_override,
))
: array('ok' => false);
if (!empty($resolved['ok'])) {
$profile = (isset($resolved['profile']) && is_array($resolved['profile'])) ? $resolved['profile'] : array();
$message = '';
return array(
'workshop_item_id' => $workshop_item_id,
'workshop_app_id' => isset($resolved['workshop_app_id']) ? (string)$resolved['workshop_app_id'] : '',
'steam_app_id' => isset($resolved['steam_app_id']) ? (string)$resolved['steam_app_id'] : '',
'folder_name' => isset($resolved['folder_name']) ? (string)$resolved['folder_name'] : ($optional_folder_name !== '' ? $optional_folder_name : '@' . $workshop_item_id),
'target_path_template' => isset($resolved['target_path_template']) ? (string)$resolved['target_path_template'] : $target_path_template,
'target_path_resolved' => isset($resolved['target_path_resolved']) ? (string)$resolved['target_path_resolved'] : '',
'server_root' => rtrim((string)$home_info['home_path'], '/'),
'steamcmd_path' => isset($profile['steamcmd_path']) ? trim((string)$profile['steamcmd_path']) : '',
'workshop_download_dir' => (isset($profile['workshop_download_dir_template']) && trim((string)$profile['workshop_download_dir_template']) !== '')
? sw_apply_template((string)$profile['workshop_download_dir_template'], (array)$resolved['vars'])
: '',
);
}
$fallback_workshop_app_id = $workshop_app_id_override;
if ($fallback_workshop_app_id === '' && is_array($fallback_profile) && !empty($fallback_profile['workshop_app_id'])) {
$fallback_workshop_app_id = (string)$fallback_profile['workshop_app_id'];
}
if ($fallback_workshop_app_id === '') {
$fallback_workshop_app_id = scm_extract_workshop_app_id($server_xml);
}
$steam_app_id = (is_array($fallback_profile) && !empty($fallback_profile['steam_app_id'])) ? (string)$fallback_profile['steam_app_id'] : '';
$folder_name = ($optional_folder_name !== '') ? $optional_folder_name : '@' . $workshop_item_id;
$effective_template = $target_path_template;
if ($effective_template === '') {
$effective_template = (is_array($fallback_profile) && !empty($fallback_profile['install_path_template']))
? (string)$fallback_profile['install_path_template']
: '{SERVER_ROOT}/{MOD_FOLDER}';
}
$placeholder_map = scm_build_placeholder_map($home_info, array('exe_location' => isset($server_xml->exe_location) ? (string)$server_xml->exe_location : ''), array(
'WORKSHOP_ID' => $workshop_item_id,
'WORKSHOP_APP_ID' => $fallback_workshop_app_id,
'STEAM_APP_ID' => $steam_app_id,
'FOLDER_NAME' => $folder_name,
'MOD_FOLDER' => $folder_name,
));
$message = '';
return array(
'workshop_item_id' => $workshop_item_id,
'workshop_app_id' => $fallback_workshop_app_id,
'steam_app_id' => $steam_app_id,
'folder_name' => $folder_name,
'target_path_template' => $effective_template,
'target_path_resolved' => scm_apply_placeholders($effective_template, $placeholder_map),
'server_root' => rtrim((string)$home_info['home_path'], '/'),
'steamcmd_path' => (is_array($fallback_profile) && !empty($fallback_profile['steamcmd_path'])) ? (string)$fallback_profile['steamcmd_path'] : '',
'workshop_download_dir' => '',
);
}
function scm_build_placeholder_map(array $home_info, array $server_context = array(), array $overrides = array()) function scm_build_placeholder_map(array $home_info, array $server_context = array(), array $overrides = array())
{ {
$home_id = (int)(isset($home_info['home_id']) ? $home_info['home_id'] : 0); $home_id = (int)(isset($home_info['home_id']) ? $home_info['home_id'] : 0);
@ -370,6 +586,8 @@ function scm_build_placeholder_map(array $home_info, array $server_context = arr
'{WORKSHOP_ID}' => '', '{WORKSHOP_ID}' => '',
'{WORKSHOP_APP_ID}' => '', '{WORKSHOP_APP_ID}' => '',
'{STEAM_APP_ID}' => '', '{STEAM_APP_ID}' => '',
'{FOLDER_NAME}' => '',
'{MOD_FOLDER}' => '',
); );
foreach ($overrides as $key => $value) { foreach ($overrides as $key => $value) {
$token = '{' . strtoupper(trim((string)$key, '{}')) . '}'; $token = '{' . strtoupper(trim((string)$key, '{}')) . '}';

View file

@ -81,13 +81,6 @@ function scm_workshop_write_manifest_and_run($db, array $home_info, $server_xml,
return false; return false;
} }
$script_path = scm_get_workshop_script_path($home_info, $server_xml);
$script_path = trim((string)$script_path);
if ($script_path === '' || !preg_match('/^[^\\r\\n\\0]+$/', $script_path)) {
$error = 'Workshop script path is invalid.';
return false;
}
$home_path = rtrim(clean_path((string)$home_info['home_path']), '/'); $home_path = rtrim(clean_path((string)$home_info['home_path']), '/');
if (!scm_path_is_under_home($home_path, $manifest_path)) { if (!scm_path_is_under_home($home_path, $manifest_path)) {
$error = 'Manifest path is outside of the server home.'; $error = 'Manifest path is outside of the server home.';
@ -99,8 +92,10 @@ function scm_workshop_write_manifest_and_run($db, array $home_info, $server_xml,
'action' => (string)$action, 'action' => (string)$action,
'home_id' => (int)$home_info['home_id'], 'home_id' => (int)$home_info['home_id'],
'home_cfg_id' => (int)$home_info['home_cfg_id'], 'home_cfg_id' => (int)$home_info['home_cfg_id'],
'workshop_app_id' => scm_extract_workshop_app_id($server_xml), 'workshop_app_id' => (!empty($extra_manifest['workshop_app_id']) ? (string)$extra_manifest['workshop_app_id'] : scm_extract_workshop_app_id($server_xml)),
'steam_app_id' => !empty($extra_manifest['steam_app_id']) ? (string)$extra_manifest['steam_app_id'] : '',
'items' => array_values($item_ids), 'items' => array_values($item_ids),
'generated_at' => date('Y-m-d H:i:s'),
); );
if (!empty($extra_manifest)) { if (!empty($extra_manifest)) {
$manifest['extra'] = $extra_manifest; $manifest['extra'] = $extra_manifest;
@ -123,8 +118,8 @@ function scm_workshop_write_manifest_and_run($db, array $home_info, $server_xml,
$error = 'Failed to write workshop manifest to remote server.'; $error = 'Failed to write workshop manifest to remote server.';
return false; return false;
} }
if ((int)$remote->rfile_exists($script_path) !== 1) { $script_path = scm_prepare_workshop_script_for_agent($remote, $home_info, $server_xml, $error);
$error = 'Configured workshop script not found on agent host: ' . $script_path; if ($script_path === false) {
return false; return false;
} }

View file

@ -1432,14 +1432,7 @@ function api_addonsmanager()
$addon_info = $addons_rows[0]; $addon_info = $addons_rows[0];
$install_method = scm_get_install_method_default(isset($addon_info['install_method']) ? $addon_info['install_method'] : 'download_zip'); $install_method = scm_get_install_method_default(isset($addon_info['install_method']) ? $addon_info['install_method'] : 'download_zip');
$validation_payload = array( $validation_payload = scm_collect_install_payload($addon_info);
'url' => isset($addon_info['url']) ? $addon_info['url'] : '',
'path' => isset($addon_info['path']) ? $addon_info['path'] : '',
'workshop_item_id' => isset($addon_info['workshop_item_id']) ? $addon_info['workshop_item_id'] : '',
'target_path_template' => isset($addon_info['target_path_template']) ? $addon_info['target_path_template'] : '',
'post_script' => isset($addon_info['post_script']) ? $addon_info['post_script'] : '',
'config_edit_rule' => isset($addon_info['config_edit_rule']) ? $addon_info['config_edit_rule'] : '',
);
$validation_message = ''; $validation_message = '';
if (!scm_validate_install_method_payload($install_method, $validation_payload, $validation_message)) if (!scm_validate_install_method_payload($install_method, $validation_payload, $validation_message))
return array("status" => '422', "message" => $validation_message); return array("status" => '422', "message" => $validation_message);
@ -1480,14 +1473,24 @@ function api_addonsmanager()
if ($install_method === 'steam_workshop') { if ($install_method === 'steam_workshop') {
scm_ensure_workshop_schema($db); scm_ensure_workshop_schema($db);
$workshop_item_id = trim((string)$addon_info['workshop_item_id']); $workshop_runtime = scm_build_workshop_runtime_context($db, $home_info, $server_xml, $validation_payload, $validation_message);
if ($workshop_runtime === false)
return array("status" => '422', "message" => $validation_message);
$workshop_item_id = (string)$workshop_runtime['workshop_item_id'];
$workshop_error = ''; $workshop_error = '';
$extra_manifest = array( $extra_manifest = array(
'addon_id' => (int)$addon_id, 'addon_id' => (int)$addon_id,
'target_path_template' => isset($addon_info['target_path_template']) ? (string)$addon_info['target_path_template'] : '', 'target_path_template' => (string)$workshop_runtime['target_path_template'],
'optional_folder_name' => isset($addon_info['optional_folder_name']) ? (string)$addon_info['optional_folder_name'] : '', 'target_path_resolved' => (string)$workshop_runtime['target_path_resolved'],
'optional_folder_name' => isset($validation_payload['optional_folder_name']) ? (string)$validation_payload['optional_folder_name'] : '',
'config_edit_rule' => isset($addon_info['config_edit_rule']) ? (string)$addon_info['config_edit_rule'] : '', 'config_edit_rule' => isset($addon_info['config_edit_rule']) ? (string)$addon_info['config_edit_rule'] : '',
'launch_param_additions' => isset($addon_info['launch_param_additions']) ? (string)$addon_info['launch_param_additions'] : '', 'launch_param_additions' => isset($addon_info['launch_param_additions']) ? (string)$addon_info['launch_param_additions'] : '',
'workshop_app_id' => (string)$workshop_runtime['workshop_app_id'],
'steam_app_id' => (string)$workshop_runtime['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' => trim((string)$post_script),
); );
$ok = scm_workshop_write_manifest_and_run($db, $home_info, $server_xml, 'install', array($workshop_item_id), $workshop_error, $extra_manifest); $ok = scm_workshop_write_manifest_and_run($db, $home_info, $server_xml, 'install', array($workshop_item_id), $workshop_error, $extra_manifest);
if ($ok) if ($ok)