diff --git a/Panel/modules/addonsmanager/addons_installer.php b/Panel/modules/addonsmanager/addons_installer.php
index b114d154..34e3bdb7 100644
--- a/Panel/modules/addonsmanager/addons_installer.php
+++ b/Panel/modules/addonsmanager/addons_installer.php
@@ -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;
diff --git a/Panel/modules/addonsmanager/addons_installer.php.bak.20260609-111046 b/Panel/modules/addonsmanager/addons_installer.php.bak.20260609-111046
new file mode 100644
index 00000000..b114d154
--- /dev/null
+++ b/Panel/modules/addonsmanager/addons_installer.php.bak.20260609-111046
@@ -0,0 +1,654 @@
+
+.
+ * 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 "
".get_lang('back')."
";
+ 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 "".get_lang('back')."
";
+ 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 "".get_lang('back')."
";
+ 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 "".get_lang('back')."
";
+ 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 "".get_lang('back')."
";
+ 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 "" . htmlentities($home_info['home_name']) . " ";
+ echo '
+
'.get_lang('install')." ".$filename." ${mbytes}MB/${totalmbytes}MB
+
+ ";
+ $bar = '';
+ for( $i = 1; $i <= $pct; $i++ )
+ {
+ $bar .= '
';
+ }
+ echo "
$bar $pct%
+
+
";
+
+ 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 "".$script_log." ";
+ }
+ 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 "".get_lang('refresh')."
";
+ $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 "".get_lang('back')."
";
+ $view->refresh("?m=addonsmanager&p=user_addons&home_id=$home_id".
+ "&mod_id=$mod_id&ip=$ip&port=$port",10);
+ return;
+ }
+ }
+ else
+ {
+ echo "".get_lang('refresh')."
";
+ $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 "Redirecting to Workshop Content manager… ";
+ echo "Click here if not redirected.
";
+ return;
+ }
+ ?>
+ 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));
+ }
+ ?>
+
+
+
diff --git a/Panel/modules/addonsmanager/addons_manager.php b/Panel/modules/addonsmanager/addons_manager.php
index 8fb52f30..114df221 100644
--- a/Panel/modules/addonsmanager/addons_manager.php
+++ b/Panel/modules/addonsmanager/addons_manager.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).
diff --git a/Panel/modules/addonsmanager/addons_manager.php.bak.20260609-111046 b/Panel/modules/addonsmanager/addons_manager.php.bak.20260609-111046
new file mode 100644
index 00000000..8fb52f30
--- /dev/null
+++ b/Panel/modules/addonsmanager/addons_manager.php.bak.20260609-111046
@@ -0,0 +1,532 @@
+
+ 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 "".get_lang('addons_manager')." ";
+ $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'] : "";
+ }
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ $method_label) {
+ $sel = ($method_key == $install_method) ? 'selected="selected"' : '';
+ $help = isset($install_help[$method_key]) ? $install_help[$method_key] : '';
+ echo ''.htmlspecialchars($method_label).' '."\n";
+ }
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Steam Workshop
+
+
+ Steam Workshop behavior is configured by the selected game's XML workshop_support block. Users install Workshop items from their server management page.
+
+
+
+
+ Post-Install Script / Action
+
+ %home_path%
+ %home_name%
+ %control_password%
+ %max_players%
+ %ip%
+ %port%
+ %query_port%
+ %incremental%
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ getGameCfgs();
+ if (!is_array($game_cfgs)) {
+ $game_cfgs = [];
+ }
+ echo "".get_lang('linux_games')." \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 "".$row['game_name'];
+ if ( preg_match("/64/", $row['game_key']) ) echo " (64bit)";
+ echo " \n";
+ }
+ }
+ echo "".get_lang('windows_games')." \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 "".$row['game_name'];
+ if ( preg_match("/64/", $row['game_key']) ) echo " (64bit)";
+ echo " \n";
+ }
+ }
+ ?>
+
+
+
+
+
+
+
+
+
+
+ getGroupList();
+ if (!is_array($groups)) {
+ $groups = [];
+ }
+ foreach ((array)$groups as $group)
+ {
+ $selected = (isset($group_id) AND $group['group_id'] == $group_id) ? 'selected=selected' : '';
+ echo "".$group['group_name']." \n";
+ }
+ ?>
+
+
+
+
+
+ Content Version
+
+
+
+ Optional version tag shown in the installed-content list.
+
+
+
+
+ Description
+
+
+
+
+
+
+
+ Behaviour Options
+
+
+
+ />
+ Stop server before installing
+
+
+
+ />
+ Backup target path before installing
+
+
+
+ />
+ Restart server after successful install
+
+
+
+
+
+ Content Reuse
+
+
+
+ />
+ Mark as cacheable / reusable
+
+
+ Only check this for public, non-sensitive content (maps, mods, jars).
+ Never check for configs, saves, credentials, or user-edited files.
+ Caching only activates when the Server Content Cache Mode panel
+ setting (in Panel Settings) is set to something other than Disabled .
+
+
+
+
+
+ ';
+ echo ' ';
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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 = [];
+ }
+ ?>
+
+
diff --git a/Panel/modules/addonsmanager/module.php.bak.20260609-111046 b/Panel/modules/addonsmanager/module.php.bak.20260609-111046
new file mode 100644
index 00000000..9b2161e0
--- /dev/null
+++ b/Panel/modules/addonsmanager/module.php.bak.20260609-111046
@@ -0,0 +1,293 @@
+ '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;
+ },
+);
+?>
diff --git a/Panel/modules/addonsmanager/navigation.xml b/Panel/modules/addonsmanager/navigation.xml
index da0b4cc5..d0a30067 100644
--- a/Panel/modules/addonsmanager/navigation.xml
+++ b/Panel/modules/addonsmanager/navigation.xml
@@ -2,5 +2,5 @@
-
+
diff --git a/Panel/modules/addonsmanager/navigation.xml.bak.20260609-111046 b/Panel/modules/addonsmanager/navigation.xml.bak.20260609-111046
new file mode 100644
index 00000000..da0b4cc5
--- /dev/null
+++ b/Panel/modules/addonsmanager/navigation.xml.bak.20260609-111046
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/Panel/modules/addonsmanager/scripts/workshop/generic_steam_workshop_linux.sh b/Panel/modules/addonsmanager/scripts/workshop/generic_steam_workshop_linux.sh
deleted file mode 100755
index 93259dd7..00000000
--- a/Panel/modules/addonsmanager/scripts/workshop/generic_steam_workshop_linux.sh
+++ /dev/null
@@ -1,246 +0,0 @@
-#!/usr/bin/env bash
-set -euo pipefail
-
-MANIFEST_PATH="${1:-}"
-if [[ -z "$MANIFEST_PATH" ]]; then
- echo "Usage: $0 "
- 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
diff --git a/Panel/modules/addonsmanager/scripts/workshop/generic_steam_workshop_windows_cygwin.sh b/Panel/modules/addonsmanager/scripts/workshop/generic_steam_workshop_windows_cygwin.sh
deleted file mode 100755
index 21f7de40..00000000
--- a/Panel/modules/addonsmanager/scripts/workshop/generic_steam_workshop_windows_cygwin.sh
+++ /dev/null
@@ -1,247 +0,0 @@
-#!/usr/bin/env bash
-set -euo pipefail
-
-MANIFEST_PATH="${1:-}"
-if [[ -z "$MANIFEST_PATH" ]]; then
- echo "Usage: $0 "
- 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
diff --git a/Panel/modules/addonsmanager/scripts/workshop/panel_generated_steamcmd_job b/Panel/modules/addonsmanager/scripts/workshop/panel_generated_steamcmd_job
deleted file mode 100755
index f8791e3a..00000000
--- a/Panel/modules/addonsmanager/scripts/workshop/panel_generated_steamcmd_job
+++ /dev/null
@@ -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 =~ /(?/dev/null 2>&1; then command -v "$c"; return 0; fi
- if [ -f "$c" ]; then echo "$c"; return 0; fi
- done
- return 1
-}
-log "GSP Workshop job starting. action=$ACTION appid=$APPID server_path=$SERVER_PATH"
-if [ "$ACTION" = "remove" ]; then
- for id in $ITEMS; do
- log "Removing Workshop item files for $id if present."
- rm -rf "$SERVER_PATH/@$id" "$SERVER_PATH/workshop/@$id" "$SERVER_PATH/steamapps/workshop/content/$APPID/$id"
- done
- log "Remove job complete."
- exit 0
-fi
-STEAMCMD="$(find_steamcmd)" || {
- log "ERROR: steamcmd was not found. Install SteamCMD on the agent host or set STEAMCMD_PATH."
- exit 127
-}
-RUNSCRIPT="$MANIFEST_DIR/steamcmd_workshop_$$.txt"
-{
- echo "@ShutdownOnFailedCommand 0"
- echo "@NoPromptForPassword 1"
- echo "login anonymous"
- echo "force_install_dir $SERVER_PATH"
- for id in $ITEMS; do
- echo "workshop_download_item $APPID $id validate"
- done
- echo "quit"
-} > "$RUNSCRIPT"
-log "Using SteamCMD: $STEAMCMD"
-log "Running SteamCMD runscript: $RUNSCRIPT"
-"$STEAMCMD" +runscript "$RUNSCRIPT" 2>&1 | tee -a "$LOG"
-rc=${PIPESTATUS[0]}
-if [ "$rc" -ne 0 ]; then
- log "ERROR: SteamCMD failed with exit $rc"
- exit "$rc"
-fi
-for id in $ITEMS; do
- SRC="$SERVER_PATH/steamapps/workshop/content/$APPID/$id"
- DST="$SERVER_PATH/@$id"
- if [ ! -d "$SRC" ]; then
- log "ERROR: downloaded Workshop source not found: $SRC"
- exit 5
- fi
- log "Installing Workshop item $id to $DST"
- rm -rf "$DST"
- cp -a "$SRC" "$DST"
- if [ -d "$DST/keys" ]; then
- mkdir -p "$SERVER_PATH/keys"
- find "$DST/keys" -type f -iname '*.bikey' -exec cp -f {} "$SERVER_PATH/keys/" \;
- fi
-done
-log "Workshop job complete."
-exit 0
diff --git a/Panel/modules/addonsmanager/server_content_helpers.php b/Panel/modules/addonsmanager/server_content_helpers.php
index fed5465c..711c25de 100644
--- a/Panel/modules/addonsmanager/server_content_helpers.php
+++ b/Panel/modules/addonsmanager/server_content_helpers.php
@@ -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.',
);
diff --git a/Panel/modules/addonsmanager/server_content_helpers.php.bak.20260609-111046 b/Panel/modules/addonsmanager/server_content_helpers.php.bak.20260609-111046
new file mode 100644
index 00000000..fed5465c
--- /dev/null
+++ b/Panel/modules/addonsmanager/server_content_helpers.php.bak.20260609-111046
@@ -0,0 +1,1301 @@
+query("ALTER TABLE `".OGP_DB_PREFIX."addons` MODIFY `addon_type` VARCHAR(32) NOT NULL");
+ $ok = (bool)$db->query(
+ "CREATE TABLE IF NOT EXISTS `".OGP_DB_PREFIX."server_content_workshop` (
+ `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ `home_id` INT NOT NULL,
+ `home_cfg_id` INT NOT NULL,
+ `remote_server_id` INT NULL,
+ `workshop_app_id` VARCHAR(32) NULL,
+ `workshop_item_id` VARCHAR(64) NOT NULL,
+ `title` VARCHAR(255) NULL,
+ `install_path` VARCHAR(512) NULL,
+ `install_strategy` VARCHAR(64) NULL,
+ `enabled` TINYINT(1) NOT NULL DEFAULT 1,
+ `load_order` INT NOT NULL DEFAULT 0,
+ `install_state` VARCHAR(32) NOT NULL DEFAULT 'selected',
+ `last_installed_at` DATETIME NULL,
+ `last_updated_at` DATETIME NULL,
+ `last_error` TEXT NULL,
+ `created_by` INT NULL,
+ `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `updated_at` DATETIME NULL,
+ UNIQUE KEY `uniq_home_workshop_item` (`home_id`, `workshop_item_id`),
+ KEY `idx_home_id` (`home_id`),
+ KEY `idx_home_cfg_id` (`home_cfg_id`),
+ KEY `idx_install_state` (`install_state`)
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
+ );
+
+ // Idempotently add content_id column (db_version 6).
+ $wk_table = $db->realEscapeSingle(OGP_DB_PREFIX . 'server_content_workshop');
+ $col_check = $db->resultQuery(
+ "SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
+ WHERE TABLE_SCHEMA = DATABASE()
+ AND TABLE_NAME = '{$wk_table}'
+ AND COLUMN_NAME = 'content_id'"
+ );
+ if (empty($col_check)) {
+ $db->query(
+ "ALTER TABLE `".OGP_DB_PREFIX."server_content_workshop`
+ ADD COLUMN `content_id` INT NULL AFTER `id`,
+ ADD KEY `idx_content_id` (`content_id`)"
+ );
+ }
+
+ $workshop_columns = array(
+ 'install_path' => "VARCHAR(512) NULL AFTER `title`",
+ 'install_strategy' => "VARCHAR(64) NULL AFTER `install_path`",
+ 'enabled' => "TINYINT(1) NOT NULL DEFAULT 1 AFTER `install_strategy`",
+ 'load_order' => "INT NOT NULL DEFAULT 0 AFTER `enabled`",
+ 'update_policy' => "VARCHAR(64) NOT NULL DEFAULT 'manual' AFTER `load_order`",
+ 'pending_action' => "VARCHAR(64) NULL AFTER `update_policy`",
+ );
+ foreach ($workshop_columns as $col => $definition) {
+ $escaped_col = $db->realEscapeSingle($col);
+ $col_check = $db->resultQuery(
+ "SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
+ WHERE TABLE_SCHEMA = DATABASE()
+ AND TABLE_NAME = '{$wk_table}'
+ AND COLUMN_NAME = '{$escaped_col}'"
+ );
+ if (empty($col_check)) {
+ $db->query("ALTER TABLE `".OGP_DB_PREFIX."server_content_workshop` ADD COLUMN `{$col}` {$definition}");
+ }
+ }
+
+ $db->query(
+ "CREATE TABLE IF NOT EXISTS `".OGP_DB_PREFIX."server_content_workshop_catalog` (
+ `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ `workshop_id` VARCHAR(64) NOT NULL,
+ `app_id` VARCHAR(32) NOT NULL DEFAULT '',
+ `title` VARCHAR(255) NULL,
+ `install_count` INT NOT NULL DEFAULT 0,
+ `first_seen` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `last_installed` DATETIME NULL,
+ `last_updated` DATETIME NULL,
+ `published_date` DATETIME NULL,
+ `tags` TEXT NULL,
+ `game_key` VARCHAR(128) NULL,
+ `local_cache_path` VARCHAR(512) NULL,
+ `author` VARCHAR(255) NULL,
+ `thumbnail_url` VARCHAR(512) NULL,
+ UNIQUE KEY `uniq_workshop_app` (`workshop_id`, `app_id`),
+ KEY `idx_app_id` (`app_id`),
+ KEY `idx_install_count` (`install_count`),
+ KEY `idx_last_installed` (`last_installed`)
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
+ );
+ $catalog_table = $db->realEscapeSingle(OGP_DB_PREFIX . 'server_content_workshop_catalog');
+ foreach (array(
+ 'author' => "VARCHAR(255) NULL AFTER `title`",
+ 'thumbnail_url' => "VARCHAR(512) NULL AFTER `author`",
+ ) as $col => $definition) {
+ $escaped_col = $db->realEscapeSingle($col);
+ $col_check = $db->resultQuery(
+ "SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
+ WHERE TABLE_SCHEMA = DATABASE()
+ AND TABLE_NAME = '{$catalog_table}'
+ AND COLUMN_NAME = '{$escaped_col}'"
+ );
+ if (empty($col_check)) {
+ $db->query("ALTER TABLE `".OGP_DB_PREFIX."server_content_workshop_catalog` ADD COLUMN `{$col}` {$definition}");
+ }
+ }
+
+ return $ok;
+}
+
+function scm_get_home_for_user($db, $home_id, $user_id)
+{
+ $home_id = (int)$home_id;
+ $user_id = (int)$user_id;
+ if ($home_id <= 0 || $user_id <= 0) {
+ return false;
+ }
+ if ($db->isAdmin($user_id)) {
+ return $db->getGameHome($home_id);
+ }
+ return $db->getUserGameHome($user_id, $home_id);
+}
+
+function scm_get_workshop_saved_count($db, $home_id)
+{
+ $home_id = (int)$home_id;
+ if ($home_id <= 0 || !scm_ensure_workshop_schema($db)) {
+ return 0;
+ }
+ $rows = $db->resultQuery(
+ "SELECT COUNT(*) AS cnt FROM `".OGP_DB_PREFIX."server_content_workshop` WHERE home_id=".$home_id." AND install_state<>'removed'"
+ );
+ if (!is_array($rows) || !isset($rows[0]['cnt'])) {
+ return 0;
+ }
+ return (int)$rows[0]['cnt'];
+}
+
+function scm_get_workshop_rows($db, $home_id)
+{
+ $home_id = (int)$home_id;
+ if ($home_id <= 0 || !scm_ensure_workshop_schema($db)) {
+ return array();
+ }
+ $rows = $db->resultQuery(
+ "SELECT * FROM `".OGP_DB_PREFIX."server_content_workshop` WHERE home_id=".$home_id." ORDER BY load_order ASC, created_at DESC, workshop_item_id ASC"
+ );
+ return is_array($rows) ? $rows : array();
+}
+
+function scm_get_workshop_update_policies()
+{
+ return array(
+ 'manual' => 'Manual update only',
+ 'scheduled' => 'Update on scheduled task',
+ 'update_now' => 'Update immediately',
+ 'update_and_restart' => 'Update immediately and restart',
+ 'download_only' => 'Download now, install later',
+ 'install_on_restart' => 'Install during next restart',
+ );
+}
+
+function scm_normalize_workshop_update_policy($policy)
+{
+ $policy = trim((string)$policy);
+ $policies = scm_get_workshop_update_policies();
+ return isset($policies[$policy]) ? $policy : 'manual';
+}
+
+function scm_workshop_set_enabled($db, $home_id, array $item_ids, $enabled)
+{
+ if (empty($item_ids)) {
+ return false;
+ }
+ $escaped_ids = array();
+ foreach ($item_ids as $item_id) {
+ $escaped_ids[] = "'" . $db->realEscapeSingle((string)$item_id) . "'";
+ }
+ return (bool)$db->query(
+ "UPDATE `".OGP_DB_PREFIX."server_content_workshop`
+ SET enabled=" . ((int)$enabled ? 1 : 0) . ", updated_at=NOW()
+ WHERE home_id=".(int)$home_id." AND workshop_item_id IN (".implode(",", $escaped_ids).")"
+ );
+}
+
+function scm_workshop_set_update_policy($db, $home_id, array $item_ids, $policy)
+{
+ if (empty($item_ids)) {
+ return false;
+ }
+ $policy = scm_normalize_workshop_update_policy($policy);
+ $pending = '';
+ if ($policy === 'download_only') {
+ $pending = 'install_on_restart';
+ }
+ elseif ($policy === 'install_on_restart') {
+ $pending = 'install_on_restart';
+ }
+ $escaped_ids = array();
+ foreach ($item_ids as $item_id) {
+ $escaped_ids[] = "'" . $db->realEscapeSingle((string)$item_id) . "'";
+ }
+ return (bool)$db->query(
+ "UPDATE `".OGP_DB_PREFIX."server_content_workshop`
+ SET update_policy='" . $db->realEscapeSingle($policy) . "',
+ pending_action=" . ($pending === '' ? "NULL" : "'" . $db->realEscapeSingle($pending) . "'") . ",
+ updated_at=NOW()
+ WHERE home_id=".(int)$home_id." AND workshop_item_id IN (".implode(",", $escaped_ids).")"
+ );
+}
+
+function scm_workshop_catalog_sort_sql($sort)
+{
+ $sort = trim((string)$sort);
+ $allowed = array(
+ 'name' => 'title ASC, workshop_id ASC',
+ 'install_count' => 'install_count DESC, last_installed DESC, workshop_id ASC',
+ 'published_date' => 'published_date DESC, title ASC, workshop_id ASC',
+ 'last_updated' => 'last_updated DESC, title ASC, workshop_id ASC',
+ 'last_installed' => 'last_installed DESC, title ASC, workshop_id ASC',
+ 'workshop_id' => 'CAST(workshop_id AS UNSIGNED) ASC, workshop_id ASC',
+ );
+ return isset($allowed[$sort]) ? $allowed[$sort] : $allowed['last_installed'];
+}
+
+function scm_get_workshop_catalog_rows($db, $app_id = '', $sort = 'last_installed', $limit = 50, $query = '', $tag = '')
+{
+ if (!scm_ensure_workshop_schema($db)) {
+ return array();
+ }
+ $where_parts = array();
+ $app_id = trim((string)$app_id);
+ if ($app_id !== '' && preg_match('/^[0-9]+$/', $app_id)) {
+ $where_parts[] = "app_id='" . $db->realEscapeSingle($app_id) . "'";
+ }
+ $query = trim((string)$query);
+ if ($query !== '') {
+ $query_id = scm_extract_workshop_item_id($query);
+ if ($query_id !== '') {
+ $where_parts[] = "workshop_id='" . $db->realEscapeSingle($query_id) . "'";
+ } else {
+ $like = "%" . $db->realEscapeSingle($query) . "%";
+ $where_parts[] = "(title LIKE '{$like}' OR author LIKE '{$like}' OR tags LIKE '{$like}' OR game_key LIKE '{$like}')";
+ }
+ }
+ $tag = trim((string)$tag);
+ if ($tag !== '') {
+ $like = "%" . $db->realEscapeSingle($tag) . "%";
+ $where_parts[] = "tags LIKE '{$like}'";
+ }
+ $where = empty($where_parts) ? '' : ('WHERE ' . implode(' AND ', $where_parts));
+ $limit = (int)$limit;
+ if ($limit <= 0 || $limit > 200) {
+ $limit = 50;
+ }
+ $rows = $db->resultQuery(
+ "SELECT * FROM `".OGP_DB_PREFIX."server_content_workshop_catalog`
+ {$where}
+ ORDER BY " . scm_workshop_catalog_sort_sql($sort) . "
+ LIMIT {$limit}"
+ );
+ return is_array($rows) ? $rows : array();
+}
+
+
+function scm_fetch_steam_workshop_details(array $item_ids)
+{
+ $ids = array();
+ foreach ($item_ids as $id) {
+ $id = (string)$id;
+ if (preg_match('/^[0-9]{3,20}$/', $id)) {
+ $ids[$id] = $id;
+ }
+ }
+ if (empty($ids)) {
+ return array();
+ }
+ $post = array('itemcount' => count($ids));
+ $i = 0;
+ foreach (array_values($ids) as $id) {
+ $post['publishedfileids['.$i.']'] = $id;
+ $i++;
+ }
+ $context = stream_context_create(array('http' => array(
+ 'method' => 'POST',
+ 'header' => "Content-type: application/x-www-form-urlencoded\r\n",
+ 'content' => http_build_query($post),
+ 'timeout' => 8,
+ )));
+ $json = @file_get_contents('https://api.steampowered.com/ISteamRemoteStorage/GetPublishedFileDetails/v1/', false, $context);
+ if ($json === false || $json === '') {
+ $json = @file_get_contents('http://api.steampowered.com/ISteamRemoteStorage/GetPublishedFileDetails/v1/', false, $context);
+ }
+ if ($json === false || $json === '') {
+ return array();
+ }
+ $data = json_decode($json, true);
+ if (!is_array($data) || empty($data['response']['publishedfiledetails'])) {
+ return array();
+ }
+ $out = array();
+ foreach ((array)$data['response']['publishedfiledetails'] as $row) {
+ if (empty($row['publishedfileid'])) {
+ continue;
+ }
+ $id = (string)$row['publishedfileid'];
+ $out[$id] = array(
+ 'title' => isset($row['title']) ? (string)$row['title'] : '',
+ 'author' => '',
+ 'thumbnail_url' => isset($row['preview_url']) ? (string)$row['preview_url'] : '',
+ 'published_date' => !empty($row['time_created']) ? date('Y-m-d H:i:s', (int)$row['time_created']) : null,
+ 'last_updated' => !empty($row['time_updated']) ? date('Y-m-d H:i:s', (int)$row['time_updated']) : null,
+ );
+ }
+ return $out;
+}
+function scm_workshop_record_catalog_items($db, $workshop_app_id, array $item_ids, array $home_info = array(), array $item_details = array(), $mark_update = false)
+{
+ if (empty($item_ids) || !scm_ensure_workshop_schema($db)) {
+ return false;
+ }
+ $workshop_app_id = preg_match('/^[0-9]+$/', (string)$workshop_app_id) ? (string)$workshop_app_id : '';
+ $game_key = isset($home_info['game_key']) ? (string)$home_info['game_key'] : '';
+ $steam_details = scm_fetch_steam_workshop_details($item_ids);
+ $steam_details = scm_fetch_steam_workshop_details($item_ids);
+ foreach ($item_ids as $item_id) {
+ $item_id = (string)$item_id;
+ if (!preg_match('/^[0-9]+$/', $item_id)) {
+ continue;
+ }
+ $detail = isset($item_details[$item_id]) && is_array($item_details[$item_id]) ? $item_details[$item_id] : array();
+ $title = isset($detail['title']) ? (string)$detail['title'] : '';
+ $author = isset($detail['author']) ? (string)$detail['author'] : '';
+ $thumbnail = isset($detail['thumbnail_url']) ? (string)$detail['thumbnail_url'] : '';
+ if (isset($steam_details[$item_id])) {
+ if ($title === '' && !empty($steam_details[$item_id]['title'])) {
+ $title = (string)$steam_details[$item_id]['title'];
+ }
+ if ($thumbnail === '' && !empty($steam_details[$item_id]['thumbnail_url'])) {
+ $thumbnail = (string)$steam_details[$item_id]['thumbnail_url'];
+ }
+ }
+ if (isset($steam_details[$item_id])) {
+ if ($title === '' && !empty($steam_details[$item_id]['title'])) {
+ $title = (string)$steam_details[$item_id]['title'];
+ }
+ if ($thumbnail === '' && !empty($steam_details[$item_id]['thumbnail_url'])) {
+ $thumbnail = (string)$steam_details[$item_id]['thumbnail_url'];
+ }
+ }
+ $install_path = isset($detail['target_path_resolved']) ? (string)$detail['target_path_resolved'] : '';
+ $db->query(
+ "INSERT INTO `".OGP_DB_PREFIX."server_content_workshop_catalog`
+ (workshop_id, app_id, title, author, thumbnail_url, install_count, first_seen, last_installed, last_updated, game_key, local_cache_path)
+ VALUES (
+ '".$db->realEscapeSingle($item_id)."',
+ '".$db->realEscapeSingle($workshop_app_id)."',
+ ".($title === '' ? "NULL" : "'".$db->realEscapeSingle($title)."'").",
+ ".($author === '' ? "NULL" : "'".$db->realEscapeSingle($author)."'").",
+ ".($thumbnail === '' ? "NULL" : "'".$db->realEscapeSingle($thumbnail)."'").",
+ 1,
+ NOW(),
+ NOW(),
+ ".($mark_update ? "NOW()" : "NULL").",
+ ".($game_key === '' ? "NULL" : "'".$db->realEscapeSingle($game_key)."'").",
+ ".($install_path === '' ? "NULL" : "'".$db->realEscapeSingle($install_path)."'")."
+ )
+ ON DUPLICATE KEY UPDATE
+ install_count=install_count+1,
+ title=IF(VALUES(title) IS NULL OR VALUES(title)='', title, VALUES(title)),
+ author=IF(VALUES(author) IS NULL OR VALUES(author)='', author, VALUES(author)),
+ thumbnail_url=IF(VALUES(thumbnail_url) IS NULL OR VALUES(thumbnail_url)='', thumbnail_url, VALUES(thumbnail_url)),
+ last_installed=NOW(),
+ last_updated=".($mark_update ? "NOW()" : "last_updated").",
+ game_key=IF(VALUES(game_key) IS NULL OR VALUES(game_key)='', game_key, VALUES(game_key)),
+ local_cache_path=IF(VALUES(local_cache_path) IS NULL OR VALUES(local_cache_path)='', local_cache_path, VALUES(local_cache_path))"
+ );
+ }
+ return true;
+}
+
+function scm_extract_workshop_item_id($value)
+{
+ $value = trim((string)$value);
+ if ($value === '') {
+ return '';
+ }
+ if (preg_match('/^[0-9]{1,20}$/', $value)) {
+ return ltrim($value, '0') === '' ? '' : $value;
+ }
+ if (preg_match('/[?&]id=([0-9]{1,20})(?:[^0-9]|$)/i', $value, $matches)) {
+ return ltrim($matches[1], '0') === '' ? '' : $matches[1];
+ }
+ if (preg_match('/steamcommunity\.com\/(?:sharedfiles|workshop)\/filedetails\/\?[^ \t\r\n]*id=([0-9]{1,20})/i', $value, $matches)) {
+ return ltrim($matches[1], '0') === '' ? '' : $matches[1];
+ }
+ return '';
+}
+
+function scm_parse_workshop_ids($raw, &$invalid = array())
+{
+ $invalid = array();
+ $ids = array();
+ // Accept IDs or full Steam Workshop URLs separated by commas, whitespace,
+ // or newlines. Customer input is reduced to numeric IDs before it reaches
+ // manifests, shell commands, or install paths.
+ $normalized = preg_replace('/[\r\n\t ]+/', ',', (string)$raw);
+ $parts = explode(',', (string)$normalized);
+ foreach ((array)$parts as $part) {
+ $value = trim((string)$part);
+ if ($value === '') {
+ continue;
+ }
+ $item_id = scm_extract_workshop_item_id($value);
+ if ($item_id === '') {
+ $invalid[] = $value;
+ continue;
+ }
+ $ids[$item_id] = $item_id;
+ }
+ return array_values($ids);
+}
+
+function scm_parse_selected_workshop_ids($selected)
+{
+ $ids = array();
+ if (!is_array($selected)) {
+ return $ids;
+ }
+ foreach ($selected as $item_id) {
+ $item_id = trim((string)$item_id);
+ if ($item_id !== '' && preg_match('/^[0-9]+$/', $item_id)) {
+ $ids[$item_id] = $item_id;
+ }
+ }
+ return array_values($ids);
+}
+
+function scm_h($value)
+{
+ return htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8');
+}
+
+function scm_is_windows_home(array $home_info)
+{
+ $game_key = isset($home_info['game_key']) ? strtolower((string)$home_info['game_key']) : '';
+ $cfg_file = isset($home_info['home_cfg_file']) ? strtolower((string)$home_info['home_cfg_file']) : '';
+ return (strpos($game_key, 'win') !== false) || (strpos($cfg_file, 'win') !== false);
+}
+
+function scm_path_is_under_home($home_path, $candidate_path)
+{
+ $home_path = rtrim(clean_path((string)$home_path), '/');
+ $candidate_path = clean_path((string)$candidate_path);
+ if ($home_path === '' || $candidate_path === '') {
+ return false;
+ }
+ return strpos($candidate_path.'/', $home_path.'/') === 0;
+}
+
+function scm_get_workshop_manifest_path(array $home_info)
+{
+ $home_path = rtrim(clean_path((string)$home_info['home_path']), '/');
+ $manifest_path = clean_path($home_path . '/gsp_server_content/workshop_manifest.json');
+ if (!scm_path_is_under_home($home_path, $manifest_path)) {
+ return false;
+ }
+ return $manifest_path;
+}
+
+function scm_extract_workshop_app_id($server_xml)
+{
+ if (isset($server_xml->workshop_support->workshop_app_id)) {
+ $value = trim((string)$server_xml->workshop_support->workshop_app_id);
+ if ($value !== '' && preg_match('/^[0-9]+$/', $value)) {
+ return $value;
+ }
+ }
+ return "";
+}
+
+function scm_workshop_is_supported($server_xml)
+{
+ if (!isset($server_xml->workshop_support)) {
+ return false;
+ }
+ if (isset($server_xml->workshop_support->enabled) && !scm_workshop_xml_bool((string)$server_xml->workshop_support->enabled, true)) {
+ return false;
+ }
+ return scm_extract_workshop_app_id($server_xml) !== '';
+}
+
+function scm_extract_workshop_steam_app_id($server_xml)
+{
+ if (isset($server_xml->workshop_support->steam_app_id)) {
+ $value = trim((string)$server_xml->workshop_support->steam_app_id);
+ if ($value !== '' && preg_match('/^[0-9]+$/', $value)) {
+ return $value;
+ }
+ }
+ return '';
+}
+
+function scm_extract_workshop_install_path($server_xml)
+{
+ if (isset($server_xml->workshop_support->install_path)) {
+ $value = trim((string)$server_xml->workshop_support->install_path);
+ if ($value !== '' && preg_match('/^[^\\r\\n\\0]+$/', $value)) {
+ return $value;
+ }
+ }
+ return '';
+}
+
+function scm_workshop_xml_bool($value, $default = false)
+{
+ $value = strtolower(trim((string)$value));
+ if ($value === '') {
+ return (bool)$default;
+ }
+ if (in_array($value, array('1', 'yes', 'true', 'on'), true)) {
+ return true;
+ }
+ if (in_array($value, array('0', 'no', 'false', 'off'), true)) {
+ return false;
+ }
+ return (bool)$default;
+}
+
+function scm_workshop_mod_prefix($server_xml)
+{
+ if (isset($server_xml->workshop_support->mod_prefix)) {
+ $prefix = trim((string)$server_xml->workshop_support->mod_prefix);
+ if ($prefix !== '' && strpos($prefix, '/') === false && strpos($prefix, '\\') === false && strpos($prefix, "\0") === false) {
+ return $prefix;
+ }
+ }
+ return '@';
+}
+
+function scm_get_bundled_workshop_script_source(array $home_info)
+{
+ $filename = scm_is_windows_home($home_info) ? SCM_WORKSHOP_SCRIPT_WINDOWS_DEFAULT : SCM_WORKSHOP_SCRIPT_LINUX_DEFAULT;
+ return dirname(__FILE__) . '/scripts/workshop/' . $filename;
+}
+
+function scm_get_agent_managed_workshop_script_path(array $home_info)
+{
+ $home_path = rtrim(clean_path((string)$home_info['home_path']), '/');
+ $filename = 'workshop_job_' . date('Ymd_His') . '_' . mt_rand(1000, 9999) . '.sh';
+ $remote_path = clean_path($home_path . '/gsp_server_content/jobs/workshop/' . $filename);
+ if (!scm_path_is_under_home($home_path, $remote_path)) {
+ return false;
+ }
+ return $remote_path;
+}
+
+function scm_prepare_workshop_script_for_agent($remote, array $home_info, $server_xml, &$error = '')
+{
+ $error = '';
+ $source_path = scm_get_bundled_workshop_script_source($home_info);
+ if (!is_file($source_path)) {
+ $error = 'Panel Workshop job template is missing: ' . $source_path;
+ return false;
+ }
+
+ $remote_path = scm_get_agent_managed_workshop_script_path($home_info);
+ if ($remote_path === false) {
+ $error = 'Unable to resolve an agent-managed workshop script path for this server.';
+ return false;
+ }
+
+ $script_body = @file_get_contents($source_path);
+ if ($script_body === false || $script_body === '') {
+ $error = 'Failed to read Panel Workshop job template: ' . $source_path;
+ return false;
+ }
+
+ $remote_dir = dirname($remote_path);
+ $remote->exec("mkdir -p " . escapeshellarg($remote_dir));
+ if ((int)$remote->remote_writefile($remote_path, $script_body) !== 1) {
+ $error = 'Failed to sync workshop script to agent host.';
+ return false;
+ }
+ $remote->exec("chmod 755 " . escapeshellarg($remote_path) . " >/dev/null 2>&1 || true");
+ return $remote_path;
+}
+
+function scm_get_default_workshop_target_template($install_strategy = '')
+{
+ $install_strategy = strtolower(trim((string)$install_strategy));
+ if (in_array($install_strategy, array('dayz_mod_folder', 'arma_mod_folder'), true)) {
+ return SCM_WORKSHOP_TARGET_MOD_ROOT_DEFAULT;
+ }
+ return SCM_WORKSHOP_TARGET_GENERIC_DEFAULT;
+}
+
+function scm_get_csrf_token()
+{
+ if (empty($_SESSION['addonsmanager_workshop_csrf'])) {
+ $_SESSION['addonsmanager_workshop_csrf'] = md5(uniqid((string)mt_rand(), true));
+ }
+ return $_SESSION['addonsmanager_workshop_csrf'];
+}
+
+function scm_validate_csrf_token($token)
+{
+ if (!isset($_SESSION['addonsmanager_workshop_csrf'])) {
+ return false;
+ }
+ return hash_equals((string)$_SESSION['addonsmanager_workshop_csrf'], (string)$token);
+}
+
+// ─────────────────────────────────────────────────────────────────────────────
+// Phase 2 helpers – schema guard, cache mode, manifest, install history
+// ─────────────────────────────────────────────────────────────────────────────
+
+/**
+ * Returns the allowed values for the server_content_cache_mode panel setting.
+ *
+ * disabled – Always install from the configured source. No scanning,
+ * no shared cache. (DEFAULT – safest choice)
+ * search_existing_servers – Agent may scan other local game-server folders for
+ * matching cacheable content and copy directly if safe.
+ * shared_cache – Agent may store cacheable content in a shared cache
+ * folder and reuse it on future installs.
+ * shared_cache_and_search – Both shared_cache and search_existing_servers are
+ * active simultaneously.
+ *
+ * Security note: only content explicitly marked is_cacheable=1 on the addon
+ * record may ever be shared or cached. Private configs, user-edited files,
+ * saves, databases, logs, and credentials must never be included.
+ *
+ * @return array key => human-readable label
+ */
+function scm_get_valid_cache_modes()
+{
+ return array(
+ 'disabled' => 'Disabled (always install from source)',
+ 'search_existing_servers' => 'Search existing servers (copy from local installs)',
+ 'shared_cache' => 'Shared cache (store and reuse cached copies)',
+ 'shared_cache_and_search' => 'Shared cache + search existing servers',
+ );
+}
+
+/**
+ * Reads the current server_content_cache_mode panel setting.
+ * Returns 'disabled' if not set.
+ *
+ * @param object $db Panel DB handle
+ * @return string One of the scm_get_valid_cache_modes() keys
+ */
+function scm_get_cache_mode($db)
+{
+ $valid = scm_get_valid_cache_modes();
+ $value = '';
+ if (method_exists($db, 'getSetting')) {
+ $value = (string)$db->getSetting('server_content_cache_mode');
+ }
+ return array_key_exists($value, $valid) ? $value : 'disabled';
+}
+
+/**
+ * Returns allowed install_method values and their display labels.
+ *
+ * @return array
+ */
+function scm_get_install_methods()
+{
+ return array(
+ 'download_zip' => 'Downloadable Mod',
+ 'steam_workshop' => 'Steam Workshop Mods',
+ 'config_edit' => 'Configuration Package',
+ 'post_script' => 'Scripted Installer',
+ );
+}
+
+function scm_get_install_method_help_text()
+{
+ return array(
+ 'download_zip' => 'Download and extract a ZIP, RAR, or archive file.',
+ 'steam_workshop' => 'Users install Steam Workshop items from their server page. App IDs, install paths, key-copy rules, and launch parameter format come from the game XML.',
+ 'config_edit' => 'Install configuration files, profiles, or templates.',
+ 'post_script' => 'Run a custom scripted installation process.',
+ );
+}
+
+function scm_get_install_method_required_fields()
+{
+ return array(
+ 'download_zip' => array('url'),
+ 'steam_workshop' => array(), // No required fields; users provide Workshop IDs on their server page
+ 'post_script' => array('post_script'),
+ 'config_edit' => array('path', 'config_edit_rule'),
+ );
+}
+
+function scm_get_install_method_validation_errors()
+{
+ return array(
+ 'download_zip' => 'Please enter a download URL.',
+ 'steam_workshop' => 'Workshop behavior is configured in the game XML.',
+ 'config_edit' => 'Please enter the config target and edit action.',
+ 'post_script' => 'Please enter the installer script/action.',
+ );
+}
+
+function scm_get_install_method_default($value = '')
+{
+ $value = trim((string)$value);
+ if ($value === 'download_file') {
+ $value = 'download_zip';
+ }
+ if ($value === 'create_folder') {
+ $value = 'config_edit';
+ }
+ $methods = scm_get_install_methods();
+ return isset($methods[$value]) ? $value : 'download_zip';
+}
+
+function scm_get_install_payload_keys()
+{
+ return array(
+ 'url',
+ 'path',
+ 'workshop_item_id',
+ 'workshop_app_id',
+ 'target_path_template',
+ 'optional_folder_name',
+ 'post_script',
+ 'config_edit_rule',
+ 'launch_param_additions',
+ );
+}
+
+function scm_collect_install_payload(array $defaults = array(), array $request = array(), array $override_keys = array())
+{
+ $payload = array();
+ foreach (scm_get_install_payload_keys() as $key) {
+ $payload[$key] = isset($defaults[$key]) ? trim((string)$defaults[$key]) : '';
+ }
+ foreach ($override_keys as $key) {
+ if (array_key_exists($key, $request)) {
+ $payload[$key] = trim((string)$request[$key]);
+ }
+ }
+ return $payload;
+}
+
+function scm_validate_numeric_content_value($value, $error_message, &$message, $allow_blank = false)
+{
+ $value = trim((string)$value);
+ if ($value === '') {
+ if ($allow_blank) {
+ $message = '';
+ return true;
+ }
+ $message = $error_message;
+ return false;
+ }
+ if (!preg_match('/^[0-9]+$/', $value)) {
+ $message = $error_message;
+ return false;
+ }
+ $message = '';
+ return true;
+}
+
+function scm_validate_download_content(array $payload, &$message = '')
+{
+ $url = isset($payload['url']) ? trim((string)$payload['url']) : '';
+ if ($url === '') {
+ $message = 'Please enter a download URL.';
+ return false;
+ }
+ $message = '';
+ return true;
+}
+
+function scm_validate_workshop_content(array $payload, &$message = '')
+{
+ $message = '';
+ return true;
+}
+
+function scm_validate_workshop_user_ids($raw_ids, &$message = '')
+{
+ $invalid = array();
+ $ids = scm_parse_workshop_ids($raw_ids, $invalid);
+ if (!empty($invalid)) {
+ $message = 'Invalid Workshop item entries. Use a numeric Workshop ID or a Steam Workshop URL: ' . implode(', ', $invalid);
+ return false;
+ }
+ if (empty($ids)) {
+ $message = 'Enter at least one Steam Workshop ID or Workshop URL.';
+ return false;
+ }
+ $message = '';
+ return true;
+}
+
+function scm_validate_scripted_installer(array $payload, &$message = '')
+{
+ $script = isset($payload['post_script']) ? trim((string)$payload['post_script']) : '';
+ if ($script === '') {
+ $message = 'Please enter the installer script/action.';
+ return false;
+ }
+ $message = '';
+ return true;
+}
+
+function scm_validate_configuration_package(array $payload, &$message = '')
+{
+ $path = isset($payload['path']) ? trim((string)$payload['path']) : '';
+ $rule = isset($payload['config_edit_rule']) ? trim((string)$payload['config_edit_rule']) : '';
+ if ($path === '' || $rule === '') {
+ $message = 'Please enter the config target and edit action.';
+ return false;
+ }
+ $message = '';
+ return true;
+}
+
+function scm_validate_install_method_payload($install_method, array $payload, &$message = '')
+{
+ $install_method = scm_get_install_method_default($install_method);
+ if (!isset(scm_get_install_method_required_fields()[$install_method])) {
+ $message = 'Invalid install/content type selected.';
+ return false;
+ }
+
+ if ($install_method === 'download_zip') {
+ return scm_validate_download_content($payload, $message);
+ }
+ if ($install_method === 'steam_workshop') {
+ return scm_validate_workshop_content($payload, $message);
+ }
+ if ($install_method === 'post_script') {
+ return scm_validate_scripted_installer($payload, $message);
+ }
+ if ($install_method === 'config_edit') {
+ return scm_validate_configuration_package($payload, $message);
+ }
+ $message = '';
+ return true;
+}
+
+function scm_build_workshop_runtime_context($db, array $home_info, $server_xml, array $payload, &$message = '')
+{
+ if (!scm_validate_workshop_content($payload, $message)) {
+ return false;
+ }
+
+ $workshop_item_id = trim((string)(isset($payload['workshop_item_id']) ? $payload['workshop_item_id'] : ''));
+ $install_strategy = isset($payload['install_strategy']) ? trim((string)$payload['install_strategy']) : '';
+ $workshop_app_id = scm_extract_workshop_app_id($server_xml);
+ $steam_app_id = scm_extract_workshop_steam_app_id($server_xml);
+ $folder_prefix = scm_workshop_mod_prefix($server_xml);
+ $folder_name = $folder_prefix . $workshop_item_id;
+ $xml_install_path = scm_extract_workshop_install_path($server_xml);
+ $effective_template = $xml_install_path !== '' ? $xml_install_path : scm_get_default_workshop_target_template($install_strategy);
+ $placeholder_map = scm_build_placeholder_map($home_info, array('exe_location' => isset($server_xml->exe_location) ? (string)$server_xml->exe_location : ''), array(
+ 'WORKSHOP_ID' => $workshop_item_id,
+ 'WORKSHOP_APP_ID' => $workshop_app_id,
+ 'STEAM_APP_ID' => $steam_app_id,
+ 'FOLDER_NAME' => $folder_name,
+ 'MOD_FOLDER' => $folder_name,
+ ));
+ $message = '';
+ return array(
+ 'workshop_item_id' => $workshop_item_id,
+ 'workshop_app_id' => $workshop_app_id,
+ 'steam_app_id' => $steam_app_id,
+ 'folder_name' => $folder_name,
+ 'target_path_template' => $effective_template,
+ 'target_path_resolved' => scm_apply_placeholders($effective_template, $placeholder_map),
+ 'server_root' => rtrim((string)$home_info['home_path'], '/'),
+ 'steamcmd_path' => '',
+ 'workshop_download_dir' => '',
+ );
+}
+
+function scm_detect_workshop_install_strategy(array $home_info, $server_xml, array $template = array())
+{
+ if (isset($server_xml->workshop_support->install_strategy)) {
+ $strategy = trim((string)$server_xml->workshop_support->install_strategy);
+ if ($strategy !== '' && preg_match('/^[a-z0-9_\-]+$/i', $strategy)) {
+ return strtolower($strategy);
+ }
+ }
+ $game_key = strtolower((string)(isset($home_info['game_key']) ? $home_info['game_key'] : ''));
+ $cfg_file = strtolower((string)(isset($home_info['home_cfg_file']) ? $home_info['home_cfg_file'] : ''));
+ $name = strtolower((string)(isset($home_info['game_name']) ? $home_info['game_name'] : ''));
+ $haystack = $game_key . ' ' . $cfg_file . ' ' . $name;
+ if (strpos($haystack, 'dayz') !== false) {
+ return 'dayz_mod_folder';
+ }
+ if (strpos($haystack, 'arma') !== false) {
+ return 'arma_mod_folder';
+ }
+ return 'copy_to_mod_folder';
+}
+
+function scm_workshop_should_copy_keys($server_xml, $install_strategy)
+{
+ if (isset($server_xml->workshop_support->copy_keys)) {
+ $attrs = $server_xml->workshop_support->copy_keys->attributes();
+ if (isset($attrs['enabled'])) {
+ return scm_workshop_xml_bool((string)$attrs['enabled'], false);
+ }
+ }
+ return in_array((string)$install_strategy, array('dayz_mod_folder', 'arma_mod_folder'), true);
+}
+
+function scm_workshop_keys_target_path($server_xml, array $home_info)
+{
+ $template = '';
+ if (isset($server_xml->workshop_support->copy_keys->target_path)) {
+ $template = trim((string)$server_xml->workshop_support->copy_keys->target_path);
+ }
+ if ($template === '') {
+ $template = '{SERVER_ROOT}/keys';
+ }
+ $map = scm_build_placeholder_map($home_info);
+ return scm_apply_placeholders($template, $map);
+}
+
+function scm_workshop_post_install_action($server_xml)
+{
+ if (!isset($server_xml->workshop_support->post_install_action)) {
+ return '';
+ }
+ $action = trim((string)$server_xml->workshop_support->post_install_action);
+ if ($action === '' || strpos($action, "\0") !== false || strpos($action, "\r") !== false || strpos($action, "\n") !== false) {
+ return '';
+ }
+ return $action;
+}
+
+function scm_build_placeholder_map(array $home_info, array $server_context = array(), array $overrides = array())
+{
+ $home_id = (int)(isset($home_info['home_id']) ? $home_info['home_id'] : 0);
+ $server_root = rtrim(clean_path((string)(isset($home_info['home_path']) ? $home_info['home_path'] : '')), '/');
+ $game_root = $server_root;
+ if (!empty($server_context['exe_location'])) {
+ $exe_location = clean_path((string)$server_context['exe_location']);
+ $exe_dir = dirname($exe_location);
+ if ($exe_dir !== '.' && $exe_dir !== '/') {
+ $game_root = clean_path($server_root . '/' . ltrim($exe_dir, '/'));
+ }
+ }
+ $map = array(
+ '{HOME_ID}' => (string)$home_id,
+ '{SERVER_ROOT}' => $server_root,
+ '{GAME_ROOT}' => $game_root,
+ '{WORKSHOP_ID}' => '',
+ '{WORKSHOP_APP_ID}' => '',
+ '{STEAM_APP_ID}' => '',
+ '{FOLDER_NAME}' => '',
+ '{MOD_FOLDER}' => '',
+ );
+ foreach ($overrides as $key => $value) {
+ $token = '{' . strtoupper(trim((string)$key, '{}')) . '}';
+ $map[$token] = (string)$value;
+ }
+ return $map;
+}
+
+function scm_apply_placeholders($template, array $placeholder_map)
+{
+ $template = (string)$template;
+ if ($template === '') {
+ return '';
+ }
+ return str_replace(array_keys($placeholder_map), array_values($placeholder_map), $template);
+}
+
+function scm_get_workshop_enabled_games($query = '', $tag = '')
+{
+ $games = array();
+ $config_dir = defined('SERVER_CONFIG_LOCATION') ? SERVER_CONFIG_LOCATION : 'modules/config_games/server_configs/';
+ $schema = defined('XML_SCHEMA') ? XML_SCHEMA : 'modules/config_games/schema_server_config.xml';
+ $query = strtolower(trim((string)$query));
+ $tag = strtolower(trim((string)$tag));
+ foreach (glob(rtrim($config_dir, '/') . '/*.xml') ?: array() as $file) {
+ $xml = @simplexml_load_file($file);
+ if ($xml === false || !scm_workshop_is_supported($xml)) {
+ continue;
+ }
+ $game_key = isset($xml->game_key) ? (string)$xml->game_key : '';
+ $game_name = isset($xml->game_name) ? (string)$xml->game_name : basename($file, '.xml');
+ $app_id = scm_extract_workshop_app_id($xml);
+ $haystack = strtolower($game_key . ' ' . $game_name . ' ' . basename($file) . ' ' . $app_id);
+ if ($query !== '' && strpos($haystack, $query) === false) {
+ continue;
+ }
+ if ($tag !== '' && strpos($haystack, $tag) === false) {
+ continue;
+ }
+ $games[] = array(
+ 'game_key' => $game_key,
+ 'game_name' => $game_name,
+ 'config_file' => basename($file),
+ 'workshop_app_id' => $app_id,
+ 'steam_app_id' => scm_extract_workshop_steam_app_id($xml),
+ 'install_strategy' => isset($xml->workshop_support->install_strategy) ? (string)$xml->workshop_support->install_strategy : '',
+ 'schema' => $schema,
+ );
+ }
+ usort($games, function ($a, $b) {
+ return strcasecmp($a['game_name'] . $a['config_file'], $b['game_name'] . $b['config_file']);
+ });
+ return $games;
+}
+
+function scm_content_logs_dir()
+{
+ return dirname(__FILE__) . '/logs';
+}
+
+function scm_content_log_file()
+{
+ return scm_content_logs_dir() . '/content_install.log';
+}
+
+function scm_log_content_install_action(array $context)
+{
+ $dir = scm_content_logs_dir();
+ if (!is_dir($dir)) {
+ @mkdir($dir, 0775, true);
+ }
+ $context['logged_at'] = date('Y-m-d H:i:s');
+ $line = json_encode($context);
+ if ($line === false) {
+ $line = '{"logged_at":"' . date('Y-m-d H:i:s') . '","error":"json_encode_failed"}';
+ }
+ @error_log($line . PHP_EOL, 3, scm_content_log_file());
+ return true;
+}
+
+/**
+ * Idempotently ensures the Phase 2 schema is present.
+ * Called from pages that use manifest / history data so that existing
+ * installs that have not yet run the module updater are covered.
+ *
+ * @param object $db Panel DB handle
+ * @return bool
+ */
+function scm_ensure_phase2_schema($db)
+{
+ static $phase2_checked = false;
+ if ($phase2_checked) {
+ return true;
+ }
+ $phase2_checked = true;
+ $prefix = OGP_DB_PREFIX;
+
+ // ── Extend addons table ───────────────────────────────────────────────────
+ $new_columns = array(
+ 'install_method' => "VARCHAR(32) NOT NULL DEFAULT 'download_zip'",
+ 'content_version' => "VARCHAR(64) NULL",
+ 'requires_stop' => "TINYINT(1) NOT NULL DEFAULT 1",
+ 'backup_before_install' => "TINYINT(1) NOT NULL DEFAULT 1",
+ 'restart_after_install' => "TINYINT(1) NOT NULL DEFAULT 0",
+ 'is_cacheable' => "TINYINT(1) NOT NULL DEFAULT 0",
+ 'description' => "TEXT NULL",
+ 'workshop_item_id' => "VARCHAR(64) NULL",
+ 'workshop_app_id' => "VARCHAR(32) NULL",
+ 'target_path_template' => "VARCHAR(255) NULL",
+ 'optional_folder_name' => "VARCHAR(255) NULL",
+ 'config_edit_rule' => "TEXT NULL",
+ 'launch_param_additions'=> "VARCHAR(255) NULL",
+ 'allow_user_workshop_ids' => "TINYINT(1) NOT NULL DEFAULT 1",
+ 'max_workshop_ids' => "INT NULL",
+ 'required_workshop_ids' => "TEXT NULL",
+ 'blocked_workshop_ids' => "TEXT NULL",
+ );
+ foreach ($new_columns as $col => $definition) {
+ $escaped_col = $db->realEscapeSingle($col);
+ $escaped_table = $db->realEscapeSingle($prefix . 'addons');
+ $check = $db->resultQuery(
+ "SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
+ WHERE TABLE_SCHEMA = DATABASE()
+ AND TABLE_NAME = '{$escaped_table}'
+ AND COLUMN_NAME = '{$escaped_col}'"
+ );
+ if (empty($check)) {
+ $db->query("ALTER TABLE `{$prefix}addons` ADD COLUMN `{$col}` {$definition}");
+ }
+ }
+
+ // ── Per-server manifest ───────────────────────────────────────────────────
+ $db->query(
+ "CREATE TABLE IF NOT EXISTS `{$prefix}server_content_manifest` (
+ `id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ `home_id` INT NOT NULL,
+ `addon_id` INT NOT NULL,
+ `install_method` VARCHAR(32) NOT NULL DEFAULT 'download_zip',
+ `content_version` VARCHAR(64) NULL,
+ `install_state` VARCHAR(32) NOT NULL DEFAULT 'installed',
+ `checksum_sha256` VARCHAR(64) NULL,
+ `source_url` VARCHAR(255) NULL,
+ `installed_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `installed_by` INT NULL,
+ `updated_at` DATETIME NULL,
+ `notes` TEXT NULL,
+ UNIQUE KEY `uniq_home_addon` (`home_id`, `addon_id`),
+ KEY `idx_home_id` (`home_id`),
+ KEY `idx_addon_id` (`addon_id`),
+ KEY `idx_install_state` (`install_state`)
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
+ );
+
+ // ── Install history ───────────────────────────────────────────────────────
+ $db->query(
+ "CREATE TABLE IF NOT EXISTS `{$prefix}server_content_install_history` (
+ `id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ `home_id` INT NOT NULL,
+ `addon_id` INT NOT NULL,
+ `installed_by` INT NULL,
+ `started_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `completed_at` DATETIME NULL,
+ `install_state` VARCHAR(32) NOT NULL DEFAULT 'started',
+ `install_method` VARCHAR(32) NULL,
+ `content_version` VARCHAR(64) NULL,
+ `source_url` VARCHAR(255) NULL,
+ `cache_mode_used` VARCHAR(32) NULL,
+ `result_code` INT NULL,
+ `log_output` MEDIUMTEXT NULL,
+ KEY `idx_home_id` (`home_id`),
+ KEY `idx_addon_id` (`addon_id`),
+ KEY `idx_started_at` (`started_at`)
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
+ );
+
+ return true;
+}
+
+/**
+ * Returns all manifest rows for a given server home.
+ *
+ * @param object $db
+ * @param int $home_id
+ * @return array
+ */
+function scm_get_manifest_rows($db, $home_id)
+{
+ $home_id = (int)$home_id;
+ if ($home_id <= 0 || !scm_ensure_phase2_schema($db)) {
+ return array();
+ }
+ $rows = $db->resultQuery(
+ "SELECT m.*, a.name AS addon_name, a.addon_type, a.install_method AS addon_install_method
+ FROM `".OGP_DB_PREFIX."server_content_manifest` m
+ LEFT JOIN `".OGP_DB_PREFIX."addons` a ON a.addon_id = m.addon_id
+ WHERE m.home_id = {$home_id}
+ ORDER BY m.installed_at DESC"
+ );
+ return is_array($rows) ? $rows : array();
+}
+
+/**
+ * Creates a new install history row and returns its insert ID.
+ * Returns 0 on failure.
+ *
+ * @param object $db
+ * @param int $home_id
+ * @param int $addon_id
+ * @param int $user_id
+ * @param string $source_url
+ * @param string $content_version
+ * @param string $install_method
+ * @param string $cache_mode_used
+ * @return int history row ID, or 0 on failure
+ */
+function scm_record_install_start($db, $home_id, $addon_id, $user_id, $source_url = '', $content_version = '', $install_method = 'download_zip', $cache_mode_used = 'disabled')
+{
+ $home_id = (int)$home_id;
+ $addon_id = (int)$addon_id;
+ $user_id = (int)$user_id;
+ $source_url = $db->realEscapeSingle((string)$source_url);
+ $content_version = $db->realEscapeSingle((string)$content_version);
+ $install_method = $db->realEscapeSingle((string)$install_method);
+ $cache_mode_used = $db->realEscapeSingle((string)$cache_mode_used);
+
+ if (!scm_ensure_phase2_schema($db)) {
+ return 0;
+ }
+ $id = $db->resultInsertId(
+ 'server_content_install_history',
+ array(
+ 'home_id' => $home_id,
+ 'addon_id' => $addon_id,
+ 'installed_by' => $user_id,
+ 'install_state' => 'started',
+ 'install_method' => $install_method,
+ 'content_version' => $content_version,
+ 'source_url' => $source_url,
+ 'cache_mode_used' => $cache_mode_used,
+ )
+ );
+ return is_numeric($id) ? (int)$id : 0;
+}
+
+/**
+ * Updates an existing install history row with the final result.
+ *
+ * @param object $db
+ * @param int $history_id
+ * @param string $state 'installed' | 'failed' | 'cancelled'
+ * @param int $result_code Exit code (0 = success)
+ * @param string $log_output Script/download log snippet
+ * @return bool
+ */
+function scm_record_install_done($db, $history_id, $state = 'installed', $result_code = 0, $log_output = '')
+{
+ $history_id = (int)$history_id;
+ $state = $db->realEscapeSingle((string)$state);
+ $result_code = (int)$result_code;
+ $log_output = $db->realEscapeSingle((string)$log_output);
+ if ($history_id <= 0) {
+ return false;
+ }
+ return (bool)$db->query(
+ "UPDATE `".OGP_DB_PREFIX."server_content_install_history`
+ SET install_state = '{$state}',
+ result_code = {$result_code},
+ log_output = '{$log_output}',
+ completed_at = NOW()
+ WHERE id = {$history_id}"
+ );
+}
+
+/**
+ * Inserts or updates a server_content_manifest row for a successful install.
+ *
+ * @param object $db
+ * @param int $home_id
+ * @param int $addon_id
+ * @param array $fields Optional overrides: install_method, content_version,
+ * install_state, source_url, checksum_sha256, installed_by
+ * @return bool
+ */
+function scm_upsert_manifest($db, $home_id, $addon_id, array $fields = array())
+{
+ $home_id = (int)$home_id;
+ $addon_id = (int)$addon_id;
+ if ($home_id <= 0 || $addon_id <= 0 || !scm_ensure_phase2_schema($db)) {
+ return false;
+ }
+ $install_method = $db->realEscapeSingle((string)(isset($fields['install_method']) ? $fields['install_method'] : 'download_zip'));
+ $content_version = $db->realEscapeSingle((string)(isset($fields['content_version']) ? $fields['content_version'] : ''));
+ $install_state = $db->realEscapeSingle((string)(isset($fields['install_state']) ? $fields['install_state'] : 'installed'));
+ $source_url = $db->realEscapeSingle((string)(isset($fields['source_url']) ? $fields['source_url'] : ''));
+ $checksum = $db->realEscapeSingle((string)(isset($fields['checksum_sha256']) ? $fields['checksum_sha256'] : ''));
+ $installed_by = isset($fields['installed_by']) ? (int)$fields['installed_by'] : 'NULL';
+ if ($installed_by !== 'NULL' && $installed_by <= 0) {
+ $installed_by = 'NULL';
+ }
+
+ return (bool)$db->query(
+ "INSERT INTO `".OGP_DB_PREFIX."server_content_manifest`
+ (`home_id`,`addon_id`,`install_method`,`content_version`,`install_state`,`source_url`,`checksum_sha256`,`installed_by`,`installed_at`,`updated_at`)
+ VALUES
+ ({$home_id},{$addon_id},'{$install_method}','{$content_version}','{$install_state}','{$source_url}','{$checksum}',{$installed_by},NOW(),NOW())
+ ON DUPLICATE KEY UPDATE
+ install_method = VALUES(install_method),
+ content_version = VALUES(content_version),
+ install_state = VALUES(install_state),
+ source_url = VALUES(source_url),
+ checksum_sha256 = VALUES(checksum_sha256),
+ installed_at = NOW(),
+ updated_at = NOW()"
+ );
+}
diff --git a/Panel/modules/addonsmanager/workshop_action.php b/Panel/modules/addonsmanager/workshop_action.php
index 292b9a62..261eafbe 100644
--- a/Panel/modules/addonsmanager/workshop_action.php
+++ b/Panel/modules/addonsmanager/workshop_action.php
@@ -1,487 +1,11 @@
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.';
}
+?>
diff --git a/Panel/modules/addonsmanager/workshop_content.php b/Panel/modules/addonsmanager/workshop_content.php
index ed8cad53..8ab9c7d8 100644
--- a/Panel/modules/addonsmanager/workshop_content.php
+++ b/Panel/modules/addonsmanager/workshop_content.php
@@ -1,308 +1,16 @@
'.htmlspecialchars($msg, ENT_QUOTES, 'UTF-8').'';
}
-
- $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 "Workshop Mods: " . scm_h($home_info['home_name']) . " ";
- if ($addon_template !== null) {
- echo "Content template: " . scm_h($addon_template['name']) . " ";
- if (!empty($addon_template['description'])) {
- echo " – " . scm_h($addon_template['description']);
- }
- echo "
";
- }
- echo "Enter a Steam Workshop URL or numeric item ID. GSP stores only the numeric Workshop ID. App IDs, install paths, mod folder strategy, key-copy behavior, and launch parameter format come from this game's XML.
";
-
- if ($message !== '') {
- if ($is_error) {
- print_failure($message);
- } else {
- print_success($message);
- }
- }
- ?>
-
- Server Name:
- Game Name:
- Workshop App ID:
- Steam App ID:
- Install Strategy:
-
-
- Search Workshop
-
-
-
-
-
-
-
-
-
-
-
-
- Workshop Search Results
-
- GSP searched the local catalog for this selected game's Workshop App ID.
- If nothing appears below, use the Steam Workshop search link above, then paste the Workshop URL or numeric ID into the install box.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Workshop URLs / IDs
-
-
- Enter one or more Steam Workshop URLs or numeric IDs, one per line, comma-separated, or space-separated. Example for Arma 3 CBA_A3: https://steamcommunity.com/sharedfiles/filedetails/?id=450814997
-
-
- Install / Queue
-
-
-
-
-
-
-
-
-
-
-
- $policy_label): ?>
-
-
-
-
- Save Policy
- Update Selected
- Download Selected
- Remove Selected
- Enable Selected
- Disable Selected
- Update All
-
-
-
-
- Known Workshop Items
- These are Workshop items previously installed through Server Content Manager. The catalog grows automatically from real installs. Metadata is optional; direct ID or URL install remains available even when Steam metadata has not been fetched yet.
-
-
- Workshop ID
- Name
- Author
- Thumbnail
- Install Count
- Published
- Last Updated
- Last Installed
- Sort ID
- Action
-
-
- No known Workshop items have been installed for this app yet.
-
-
-
-
-
-
-
-
- ' alt='' style='max-width:72px;max-height:48px;' />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ' />
- Install
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Steam Workshop';
+print_failure('Steam Workshop has been removed from Server Content. Use the Steam Workshop module instead.');
+echo 'The working legacy Steam Workshop module handles Workshop installs. Server Content will continue to manage normal downloadable content/addons only.
';
+?>