Resolve merge conflicts with Panel-unstable
Co-authored-by: iaretechnician <2749183+iaretechnician@users.noreply.github.com>
This commit is contained in:
commit
286c2a673f
35 changed files with 4815 additions and 2229 deletions
4
.github/module-map.md
vendored
4
.github/module-map.md
vendored
|
|
@ -33,14 +33,14 @@ This file captures how the control panel, storefront, agents, and helper scripts
|
|||
| `config_games` | `modules/config_games/add_mod.php`, `server_config_parser.php`, XML files under `server_configs/` | Admin UI for XML definitions. Controls what appears in storefront/service catalog. | Feeds `gamemanager`, billing catalog, cron installers. |
|
||||
| `steam_workshop` | `modules/steam_workshop/admin.php`, `user.php`, `Panel/includes/functions.php`, `navigation.xml` | Admin profile defaults + per-home mod management. Profile defaults can now be refreshed from game XML and the user route is explicitly exposed via `p=user`. | Uses `config_games` XML metadata + `server_homes`/assignment tables; feeds workshop agent updater. |
|
||||
| `user_games` | `modules/user_games/add_home.php`, `assign_home.php`, `edit_home.php` | Admin workflow to add homes manually or edit assignments. Shares DB tables with billing provisioner. | Uses `game_homes`, `remote_servers`, `billing_orders`. |
|
||||
| `administration` / `user_admin` | CRUD around users, groups, permissions, expire dates. Also hosts the panel updater (`modules/administration/panel_update.php`) with preflight checks, required pre-update patches (`modules/update/patches`), root-layout sync, backup/rollback, and Apache path scan/repair helpers. | Sets roles consumed by storefront admin guard and provisioning ACLs; updater now coordinates root `Panel/` + `Website/` deployment safety. |
|
||||
| `administration` / `user_admin` | CRUD around users, groups, permissions, expire dates. `modules/administration/panel_update.php` now also runs repository-layout-aware panel updates, preflight checks, updater self-refresh, backup/rollback for Panel+Website, patch execution, and Apache path scan/fix helpers. | Sets roles consumed by storefront admin guard and provisioning ACLs; writes update lifecycle traces to root `logs/update_trace.log` and patch state via `modules/update/patches` + `update_patches` tracking. |
|
||||
| `server` | `modules/server/*` | Remote server management (agents, IPs, ports, reinstall keys). Billing uses these tables for available nodes/locations. |
|
||||
| `modulemanager` | Manage module install/uninstall/menus. Billing module registers `navigation.xml` to surface `create_servers.php` & admin pages. |
|
||||
| `tickets`, `support` | Support ticketing/email utilities. | Pulls user info and logger records. |
|
||||
| `extras`, `addonsmanager` | Workshop/add-on management. | Hooks into game homes after provisioning. |
|
||||
| `litefm`, `ftp`, `TS3Admin` | File managers and TeamSpeak controllers. | Depend on homes and remote server credentials set during provisioning. |
|
||||
| `news`, `circular`, `faq` | Content modules for panel UI. | Use standard MVC wrappers, share session/auth. |
|
||||
| `cron` | Scheduler UI feeding `scripts/` commands. | Maintains job metadata that OS cron reads. |
|
||||
| `cron` | Scheduler UI feeding `scripts/` commands. | Maintains job metadata that OS cron reads, including scheduler-triggered Server Content actions via `ogp_api.php?server_content/run_scheduled_action` and `modules/addonsmanager/server_content_actions.php`. |
|
||||
|
||||
## Storefront (`Panel/modules/billing` runtime + `Website/` compatibility wrappers)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
# Changelog
|
||||
|
||||
## 2026-05-18
|
||||
- **Protocol/image upgrade Phase 1 scaffolding (non-breaking):** Added `protocol/gsp_query.php` normalized query wrapper (LGSL default provider with future-provider placeholders), documented current protocol integration and migration plan in `protocol/PROTOCOL_UPGRADE_REVIEW.md`, and documented image module comparison/unification direction in `modules/SERVER_IMAGE_MODULE_REVIEW.md` without removing LGSL, dsi, or `lgsl_with_img_mod`.
|
||||
- **Cron ↔ Server Content action hook integration:** Added scheduler-callable Server Content hooks in `modules/addonsmanager/server_content_actions.php`, exposed API route `server_content/run_scheduled_action`, and wired cron/user-cron action builders/parsing to support server content scheduled actions (check/install/queue/restart/validate/backup flows) without embedding game-specific install logic in the scheduler.
|
||||
- **Server Content Workshop Phase 1 in addonsmanager:** Added a new `Workshop Content` flow under Server Content with per-home Workshop ID storage, ID validation/deduplication, install/update/remove/update-all actions, manifest-based script handoff (`gsp_server_content/workshop_manifest.json`), safe placeholder workshop scripts for Linux/Cygwin, and schema support via `server_content_workshop` plus `addons.addon_type VARCHAR(32)`.
|
||||
- **Updater layout hardening + pre-update patch framework:** Reworked `modules/administration/panel_update.php` to resolve explicit GSP root/Panel/Website paths, run mandatory preflight checks, self-update updater files before main sync when drift is detected, and apply ordered required patches from `modules/update/patches/` with DB/local state tracking. Backup/rollback now includes both Panel + Website archives and root `version.json`, logs moved to root `logs/update_trace.log`, and the admin Update UI now exposes preflight, patch apply, Apache path scan/fix, backup, update, and rollback actions.
|
||||
- **Billing runtime relocation + portable path bootstrap:** Re-homed storefront runtime to `Panel/modules/billing`, added portable runtime helpers (`billing_bootstrap.php`, `site_config.php`, `site_config.example.php`) with env/local override support for base path and panel path, normalized critical storefront redirects/links to computed billing URLs, and added `Website` compatibility wrappers for key billing entrypoints.
|
||||
- **Panel updater panel-subtree safety:** Hardened updater logic to treat repository `/panel` as the update source when present (ZIP + git flows) so root-level docs/examples/scripts are no longer candidates for panel file overwrite during updates.
|
||||
- **Updater root-layout hardening + pre-update patch workflow:** Reworked the admin updater to treat `/var/www/html/GSP` as repo root (with explicit `Panel/` + `Website/` handling), added mandatory preflight + required patch execution (`Panel/modules/update/patches` + persisted patch-state file), implemented updater self-refresh detection/resume, expanded backups/rollback to include Panel+Website+`version.json` with retention, and added Apache stale-path scan/repair tooling with backups + `apache2ctl configtest` safeguards.
|
||||
|
|
|
|||
|
|
@ -17,3 +17,7 @@
|
|||
- Add a panel settings health check that validates reCAPTCHA site/secret keys against active panel/storefront domains and warns admins before registration users see widget errors.
|
||||
- Add an automated deployment check that fails when `Website/timestamp.txt` and `modules/billing/timestamp.txt` diverge after storefront/content changes.
|
||||
- Add an integration smoke test that exercises updater preflight, required patch state persistence, Apache path scan output, and rollback restore of Panel/Website/version.json artifacts.
|
||||
- Add an admin preview/diff panel for Apache path repairs so staff can review exact vhost line changes before confirming `Fix Apache Paths`.
|
||||
- Add Phase 2 Workshop Content UX in `addonsmanager`: browse/search/select Workshop items with metadata while reusing the Phase 1 per-home saved-ID action pipeline.
|
||||
- Add localized language strings/tooltips for the new cron scheduler `server_content_*` action labels across all supported panel locales.
|
||||
- Add a Game Manager "Live Server Status" panel that consumes `Panel/protocol/gsp_query.php` and shows banner preview plus copyable embed code.
|
||||
|
|
|
|||
|
|
@ -55,6 +55,9 @@ function get_function_args($main_request)
|
|||
|
||||
//______________ Steam Workshop
|
||||
$functions["steam_workshop/install"] = array("token" => true, "ip" => true, "port" => true, "mod_key" => false, "mods_list" => true);
|
||||
|
||||
//______________ Server Content
|
||||
$functions["server_content/run_scheduled_action"] = array("token" => true, "home_id" => true, "action" => true, "options" => false);
|
||||
|
||||
//______________ Settings
|
||||
$functions["setting/get"] = array("token" => true, "setting_name" => true);
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ define('LANG_gamemanager', "Game Manager");
|
|||
define('LANG_game_monitor', "Game Monitor");
|
||||
define('LANG_steam_workshop', "Steam Workshop");
|
||||
define('LANG_dashboard', "Dashboard");
|
||||
define('LANG_user_addons', "Addons");
|
||||
define('LANG_user_addons', "Server Content");
|
||||
define('LANG_ftp', "FTP");
|
||||
define('LANG_shop', "Shop");
|
||||
define('LANG_shop_guest', "Shop");
|
||||
|
|
@ -95,7 +95,7 @@ define('LANG_user_admin', "Users");
|
|||
define('LANG_sub_users', "Sub Users");
|
||||
define('LANG_show_groups', "Groups");
|
||||
define('LANG_user_games', "Game Servers");
|
||||
define('LANG_addons_manager', "Addons Manager");
|
||||
define('LANG_addons_manager', "Server Content Manager");
|
||||
define('LANG_ftp_admin', "FTP users");
|
||||
define('LANG_orders', "Orders");
|
||||
define('LANG_services', "Services");
|
||||
|
|
|
|||
|
|
@ -22,49 +22,61 @@
|
|||
*
|
||||
*/
|
||||
|
||||
define('LANG_install_plugin', "Install Plugins");
|
||||
define('LANG_install_mappack', "Install Maps");
|
||||
define('LANG_install_config', "Install Configs");
|
||||
// --- Server Content Manager (formerly Addons Manager) ---
|
||||
// UI labels are updated to use "Server Content" terminology.
|
||||
// Internal keys remain unchanged for backward compatibility with other languages.
|
||||
|
||||
define('LANG_install_plugin', "Install Plugin / Mod");
|
||||
define('LANG_install_mappack', "Install Map Pack");
|
||||
define('LANG_install_config', "Install Config Pack");
|
||||
define('LANG_game_name', "Game Name");
|
||||
define('LANG_directory', "Directory Path");
|
||||
define('LANG_remote_server', "Remote server");
|
||||
define('LANG_select_addon', "Select Addon");
|
||||
define('LANG_select_addon', "Select Server Content Item");
|
||||
define('LANG_install', "Install");
|
||||
define('LANG_failed_to_start_file_download', "Failed to start file download.");
|
||||
define('LANG_no_games_servers_available', "There are no game servers available in your account.");
|
||||
define('LANG_addon_installed_successfully', "Addon installed successfully");
|
||||
define('LANG_addon_installed_successfully', "Server content item installed successfully");
|
||||
define('LANG_path', "Path");
|
||||
define('LANG_wait_while_decompressing', "Wait while the file %s is decompressed.");
|
||||
define('LANG_addon_name', "Addon Name");
|
||||
define('LANG_addon_name', "Content Item Name");
|
||||
define('LANG_url', "URL");
|
||||
define('LANG_select_game_type', "Select Game Type");
|
||||
define('LANG_plugin', "Plugin");
|
||||
define('LANG_mappack', "MapPack");
|
||||
define('LANG_config', "Config");
|
||||
define('LANG_type', "Addon Type");
|
||||
define('LANG_plugin', "Plugins / Mods");
|
||||
define('LANG_mappack', "Map Packs");
|
||||
define('LANG_config', "Config Packs");
|
||||
// Additional category labels (for future content types already defined in server_content_categories.php)
|
||||
define('LANG_server_content_version', "Server Versions");
|
||||
define('LANG_modpack', "Modpacks");
|
||||
define('LANG_workshop', "Workshop Content");
|
||||
define('LANG_script', "Scripted Installer");
|
||||
define('LANG_profile', "Server Profiles");
|
||||
define('LANG_type', "Content Type");
|
||||
define('LANG_game', "Game");
|
||||
define('LANG_show_all_addons', "Show All Addons");
|
||||
define('LANG_show_addons_for_selected_type', "Show Addons For Selected Type");
|
||||
define('LANG_show_addons_for_selected_game', "Show Addons For Selected Game");
|
||||
define('LANG_show_all_addons', "Show All Server Content");
|
||||
define('LANG_show_addons_for_selected_type', "Show Content For Selected Type");
|
||||
define('LANG_show_addons_for_selected_game', "Show Content For Selected Game");
|
||||
define('LANG_linux_games', "Linux Games:");
|
||||
define('LANG_windows_games', "Windows Games:");
|
||||
define('LANG_create_addon', "Create Addon");
|
||||
define('LANG_addons_db', "Addons Database");
|
||||
define('LANG_addon_has_been_created', "The addon %s has been created.");
|
||||
define('LANG_remove_addon', "Remove Addon");
|
||||
define('LANG_fill_the_url_address_to_a_compressed_file', "Please, fill an URL address for a compressed file.");
|
||||
define('LANG_fill_the_addon_name', "Please, fill a name for the addon package.");
|
||||
define('LANG_select_an_addon_type', "Please, select an addon type.");
|
||||
define('LANG_select_a_game_type', "Please, select a game type.");
|
||||
define('LANG_edit_addon', "Edit Addon");
|
||||
define('LANG_post-script', "Post-install script(bash)");
|
||||
define('LANG_create_addon', "Create Server Content Item");
|
||||
define('LANG_addons_db', "Server Content Database");
|
||||
define('LANG_addon_has_been_created', "The server content item \"%s\" has been created.");
|
||||
define('LANG_remove_addon', "Remove");
|
||||
define('LANG_fill_the_url_address_to_a_compressed_file', "Please enter a URL for the compressed file to download.");
|
||||
define('LANG_fill_the_addon_name', "Please enter a name for the server content item.");
|
||||
define('LANG_select_an_addon_type', "Please select a content type.");
|
||||
define('LANG_select_a_game_type', "Please select a game type.");
|
||||
define('LANG_edit_addon', "Edit");
|
||||
define('LANG_invalid_addon', "Invalid server content item or access denied.");
|
||||
define('LANG_invalid_addon_type', "Invalid content type selected.");
|
||||
define('LANG_post-script', "Post-install script (bash)");
|
||||
define('LANG_replacements', "Replacements:");
|
||||
define('LANG_addon_name_info', "Enter a name for this addon, this is the name that the user sees.");
|
||||
define('LANG_url_info', "Enter a web address that contains a file to download, if compressed in zip or tar.gz will be unpacked in the root directory of the server or on the path given below.");
|
||||
define('LANG_path_info', "The path must be relative to the server folder and contain no slashes at the beginning or end, eg: cstrike/cfg. If left blank will use the server root path.");
|
||||
define('LANG_post-script_info', "Enter Bash language code, this will be executed as a script, you can use text replacements to customize the installation, they will be replaced by data from the server on which you install the addon. The script will start from the root folder of the server or the specified path.");
|
||||
define('LANG_addon_name_info', "Enter a display name for this server content item.");
|
||||
define('LANG_url_info', "Enter a download URL for a compressed file (.zip or .tar.gz). It will be extracted into the server root or the path specified below.");
|
||||
define('LANG_path_info', "Path relative to the server folder, with no leading or trailing slashes (e.g. cstrike/cfg). Leave blank to use the server root.");
|
||||
define('LANG_post-script_info', "Enter a Bash script to run after installation. Use the replacement variables listed on the left to inject server-specific values. The script runs from the server root or the specified path.");
|
||||
define('LANG_show_to_group', "Show to group");
|
||||
define('LANG_all_groups', "All groups");
|
||||
define('LANG_show_addons_for_selected_group', "Show addons for selected group");
|
||||
define('LANG_show_addons_for_selected_group', "Show content for selected group");
|
||||
define('LANG_group', "Group");
|
||||
?>
|
||||
?>
|
||||
|
|
|
|||
|
|
@ -45,4 +45,18 @@ define('LANG_cron_no_servers_tied_to_account', "There are no servers directly as
|
|||
define('LANG_cron_admin_link_display_text', "Cron Administration (All Servers)");
|
||||
define('LANG_cron_admin_no_ogp_servers_to_display', "There aren't any servers currently configured in OGP.");
|
||||
define('LANG_bad_inputs', "Scheduled time contains invalid characters.");
|
||||
?>
|
||||
define('LANG_server_content_check_updates', 'Check Server Content Updates');
|
||||
define('LANG_server_content_check_workshop_updates', 'Check Workshop Updates');
|
||||
define('LANG_server_content_install_updates', 'Install Server Content Updates');
|
||||
define('LANG_server_content_install_updates_if_stopped', 'Install Updates If Server Is Stopped');
|
||||
define('LANG_server_content_install_updates_next_restart', 'Install Updates On Next Restart');
|
||||
define('LANG_server_content_install_updates_now', 'Install Updates Now');
|
||||
define('LANG_server_content_install_updates_and_restart', 'Install Updates And Restart');
|
||||
define('LANG_server_content_notify_updates_only', 'Notify Updates Only');
|
||||
define('LANG_server_content_update_all', 'Update All Server Content');
|
||||
define('LANG_server_content_validate_files', 'Validate Server Content Files');
|
||||
define('LANG_server_content_backup_before_update', 'Backup Before Server Content Update');
|
||||
define('LANG_safe_restart', 'Safe Restart');
|
||||
define('LANG_workshop_content', 'Workshop Content');
|
||||
define('LANG_server_content', 'Server Content');
|
||||
?>
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@ define('LANG_server_cant_start', "server can not start");
|
|||
define('LANG_server_cant_stop', "server can not stop");
|
||||
define('LANG_error_occured_remote_host', "Error occurred on the remote host");
|
||||
define('LANG_follow_server_status', "You can follow the server status from");
|
||||
define('LANG_addons', "Addons");
|
||||
define('LANG_addons', "Server Content");
|
||||
define('LANG_hostname', "Hostname");
|
||||
define('LANG_ping', "Ping");
|
||||
define('LANG_team', "Team");
|
||||
|
|
|
|||
80
Panel/modules/SERVER_IMAGE_MODULE_REVIEW.md
Normal file
80
Panel/modules/SERVER_IMAGE_MODULE_REVIEW.md
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
# Server Image Module Review (Phase 1)
|
||||
|
||||
## Scope reviewed
|
||||
- `Panel/modules/lgsl_with_img_mod/`
|
||||
- `Panel/modules/dsi/`
|
||||
- Integration touchpoints in `Panel/modules/gamemanager/`
|
||||
|
||||
## Entry points
|
||||
|
||||
### lgsl_with_img_mod
|
||||
- Module metadata: `Panel/modules/lgsl_with_img_mod/module.php`
|
||||
- User/admin pages:
|
||||
- `lgsl.php`
|
||||
- `lgsl_admin.php`
|
||||
- Image endpoint:
|
||||
- `image.php` (reads by `s` argument, supports `img_type`)
|
||||
- Core query/cache/image logic:
|
||||
- `lgsl_files/lgsl_class.php`
|
||||
|
||||
### dsi
|
||||
- Module metadata: `Panel/modules/dsi/module.php`
|
||||
- User/admin/list pages:
|
||||
- `dsi_user.php`
|
||||
- `dsi_admin.php`
|
||||
- `dsi_list.php`
|
||||
- Image endpoint:
|
||||
- `image.php` (`modules/dsi/s-IP_PORT-type.png` style)
|
||||
- Helpers:
|
||||
- `includes/functions.php`
|
||||
- `includes/functions_ui.php`
|
||||
|
||||
## Comparison
|
||||
|
||||
### Which module generates images?
|
||||
- **Both** generate PNG status banners.
|
||||
- `lgsl_with_img_mod` is LGSL-centric and built around the `OGP_DB_PREFIXlgsl` cache model.
|
||||
- `dsi` supports LGSL/GameQ/TS3 monitor includes and can render banner/code snippets directly for panel users.
|
||||
|
||||
### Which has better cache support?
|
||||
- **lgsl_with_img_mod** has deeper cache integration:
|
||||
- DB-backed query cache (`OGP_DB_PREFIXlgsl.cache`, `cache_time`)
|
||||
- image file cache handling
|
||||
- pending/retry semantics in `lgsl_query_cached(...)`
|
||||
- `dsi` has simple file cache (60s TTL) per generated image and relies on protocol monitor include side effects for query state.
|
||||
|
||||
### Which integrates with server monitor better?
|
||||
- **dsi** is currently more user-facing for “banner + embed code” workflows:
|
||||
- `dsi_render_table(...)` outputs HTML/BBCode snippets.
|
||||
- Integrates query handlers by protocol in `dsi/image.php`.
|
||||
- `lgsl_with_img_mod` is more standalone/legacy LGSL module flow.
|
||||
|
||||
### Which supports player info better?
|
||||
- Both are focused on banner status fields (name/map/players/status).
|
||||
- Neither is currently a full player-list UI provider for Game Manager.
|
||||
- Query-level player data comes from monitor protocol paths, not these image modules as a first-class shared API.
|
||||
|
||||
### Which is easier to modernize?
|
||||
- **dsi** is the better base for a future unified GSP banner module:
|
||||
- simpler structure
|
||||
- clear image endpoint + code generation UI
|
||||
- already aware of LGSL/GameQ/TS3 protocol branching
|
||||
- `lgsl_with_img_mod` contains useful mature cache ideas and map/image utilities worth reusing.
|
||||
|
||||
## Recommended future GSP banner direction
|
||||
Future module target: `Panel/modules/server_status_banner/`
|
||||
|
||||
### Plan
|
||||
1. Keep both current modules in place during migration.
|
||||
2. Use `dsi` UX flow and embed-code patterns as baseline.
|
||||
3. Reuse selective `lgsl_with_img_mod` cache + map/image helper ideas.
|
||||
4. Drive data from normalized query cache (planned `server_query_cache`) and wrapper output.
|
||||
5. Generate GSP-owned PNG banners (small / wide / large styles).
|
||||
6. Avoid external GameTracker asset dependency.
|
||||
7. Provide HTML / BBCode / direct image URL output.
|
||||
8. Surface banner preview and code tool in Game Manager.
|
||||
|
||||
## No-removal statement (Phase 1 safety)
|
||||
- `lgsl_with_img_mod` was **not removed**.
|
||||
- `dsi` was **not removed**.
|
||||
- This phase is documentation and direction-setting only.
|
||||
417
Panel/modules/addonsmanager/SERVER_CONTENT_ROADMAP.md
Normal file
417
Panel/modules/addonsmanager/SERVER_CONTENT_ROADMAP.md
Normal file
|
|
@ -0,0 +1,417 @@
|
|||
# Server Content Manager — Roadmap & Safety Review
|
||||
|
||||
> **Module:** `Panel/modules/addonsmanager`
|
||||
> **Status:** Phase 1 complete — UI/language cleanup, category map, VARCHAR(32) migration, installer documentation
|
||||
> **Branch:** Panel-unstable
|
||||
> **Maintained by:** WDS (GSP is a heavily customized fork of OGP)
|
||||
|
||||
---
|
||||
|
||||
## 1. Current Behaviour Summary
|
||||
|
||||
The **Addons Manager** (now labelled "Server Content Manager" in the UI) lets
|
||||
admins define downloadable content items that can be pushed to game server
|
||||
homes by users.
|
||||
|
||||
### Flow
|
||||
|
||||
```
|
||||
Admin creates Server Content item (addons_manager.php)
|
||||
└─> stored in OGP_DB_PREFIXaddons
|
||||
|
||||
User visits game monitor
|
||||
└─> monitor_buttons.php checks for content items for that game type
|
||||
└─> "Server Content (N)" button appears
|
||||
|
||||
User clicks button
|
||||
└─> user_addons.php — shows available category links
|
||||
└─> addons_installer.php?addon_type=<type>
|
||||
└─> user picks a specific item
|
||||
└─> state=start → agent.start_file_download(url, path, filename, "uncompress")
|
||||
└─> optional post_script runs on the agent after extraction
|
||||
└─> page auto-refreshes to show download/script progress
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Database Schema (db_version 4 — Phase 2)
|
||||
|
||||
### `OGP_DB_PREFIXaddons` (content item catalogue)
|
||||
|
||||
| Column | Type | Default | Description |
|
||||
|-----------------------|-----------------|---------------|-------------|
|
||||
| addon_id | INT UNSIGNED PK | auto | Primary key |
|
||||
| name | VARCHAR(80) | | Display name |
|
||||
| url | VARCHAR(200) | | Download URL (zip / tar.gz / API endpoint) |
|
||||
| path | VARCHAR(80) | | Relative target path inside server home |
|
||||
| addon_type | VARCHAR(32) | | Category key (plugin / mappack / config / …) |
|
||||
| home_cfg_id | VARCHAR(7) | | Linked game config ID |
|
||||
| post_script | LONGTEXT | | Bash script run by agent after install |
|
||||
| group_id | INT(11) NULL | | Restrict to a specific user group |
|
||||
| install_method ¹ | VARCHAR(32) | download_zip | How the content is delivered |
|
||||
| content_version ¹ | VARCHAR(64) NULL| | Version tag shown in manifest |
|
||||
| requires_stop ¹ | TINYINT(1) | 1 | Must server be stopped before install? |
|
||||
| backup_before_install¹| TINYINT(1) | 1 | Back up target path before installing? |
|
||||
| restart_after_install¹| TINYINT(1) | 0 | Restart server after successful install? |
|
||||
| is_cacheable ¹ | TINYINT(1) | 0 | Safe for cross-server copy / shared cache? |
|
||||
| description ¹ | TEXT NULL | | Short description shown to users |
|
||||
|
||||
¹ Added in db_version 4 (Phase 2).
|
||||
|
||||
### `OGP_DB_PREFIXserver_content_manifest` (Phase 2 — per-server installed state)
|
||||
|
||||
One row per (home_id, addon_id) pair. Updated on every successful install.
|
||||
|
||||
| Column | Type | Description |
|
||||
|-----------------|------------------|-------------|
|
||||
| id | INT UNSIGNED PK | Auto primary key |
|
||||
| home_id | INT | Server home |
|
||||
| addon_id | INT | Installed content item |
|
||||
| install_method | VARCHAR(32) | Method used for the install |
|
||||
| content_version | VARCHAR(64) NULL | Version tag at time of install |
|
||||
| install_state | VARCHAR(32) | installed / failed / removed |
|
||||
| checksum_sha256 | VARCHAR(64) NULL | SHA-256 of the installed archive |
|
||||
| source_url | VARCHAR(255) NULL| URL used for the install |
|
||||
| installed_at | DATETIME | Timestamp of last successful install |
|
||||
| installed_by | INT NULL | User ID who performed the install |
|
||||
| updated_at | DATETIME NULL | Last update time |
|
||||
| notes | TEXT NULL | Free-text admin notes |
|
||||
|
||||
### `OGP_DB_PREFIXserver_content_install_history` (Phase 2 — install log)
|
||||
|
||||
One row per install attempt. Never deleted (audit trail).
|
||||
|
||||
| Column | Type | Description |
|
||||
|-----------------|-------------------|-------------|
|
||||
| id | INT UNSIGNED PK | Auto primary key |
|
||||
| home_id | INT | Server home |
|
||||
| addon_id | INT | Content item installed |
|
||||
| installed_by | INT NULL | User who triggered the install |
|
||||
| started_at | DATETIME | When the install was initiated |
|
||||
| completed_at | DATETIME NULL | When the install completed |
|
||||
| install_state | VARCHAR(32) | started / installed / failed / cancelled |
|
||||
| install_method | VARCHAR(32) NULL | Method used |
|
||||
| content_version | VARCHAR(64) NULL | Version tag |
|
||||
| source_url | VARCHAR(255) NULL | Source URL used |
|
||||
| cache_mode_used | VARCHAR(32) NULL | Value of server_content_cache_mode at install time |
|
||||
| result_code | INT NULL | Script exit code (0 = success) |
|
||||
| log_output | MEDIUMTEXT NULL | Script / download log excerpt |
|
||||
|
||||
---
|
||||
|
||||
## 3. Existing Flow: user_addons.php → addons_installer.php
|
||||
|
||||
1. `user_addons.php` queries all content items for the server's `home_cfg_id`.
|
||||
2. It groups items by `addon_type` and renders one link per category.
|
||||
3. `addons_installer.php` (page key: `addons`) receives `addon_type` and
|
||||
`home_id` in the query string.
|
||||
4. On first load (no `state`), it renders a dropdown of available items.
|
||||
5. On submit (`state=start`), it calls `$remote->start_file_download()` and
|
||||
begins polling.
|
||||
6. Subsequent loads with `state=refresh` poll the agent for download progress
|
||||
and script log output.
|
||||
|
||||
---
|
||||
|
||||
## 4. Current post_script Replacement Variables
|
||||
|
||||
| Variable | Replaced with |
|
||||
|--------------------|--------------------------------------------------------------|
|
||||
| `%home_path%` | Absolute filesystem path of the server home directory |
|
||||
| `%home_name%` | Human-readable name of the server home |
|
||||
| `%control_password%` | RCON / control password for this server instance |
|
||||
| `%max_players%` | Maximum player count for this mod slot |
|
||||
| `%ip%` | IP address bound to this server |
|
||||
| `%port%` | Game port |
|
||||
| `%query_port%` | Query/status port (derived from game XML rules) |
|
||||
| `%incremental%` | Internal incremental counter for this mod/home combination |
|
||||
|
||||
All replacements are case-insensitive (`preg_replace … /i`).
|
||||
|
||||
---
|
||||
|
||||
## 5. Security Concerns
|
||||
|
||||
### Current risks
|
||||
|
||||
1. **No path validation in the panel** — the `path` field is passed directly
|
||||
to the agent without checking for `../`. The agent is the last line of
|
||||
defence. A malicious admin could craft a path that escapes the home
|
||||
directory if the agent's validation is insufficient.
|
||||
|
||||
2. **SQL injection in filter queries** — `addon_type` is interpolated into
|
||||
SQL strings in several places. A whitelist check via `in_array()` against
|
||||
the registered category keys prevents injection, but this must remain in
|
||||
place whenever new query sites are added.
|
||||
|
||||
3. **post_script is admin-only but powerful** — admins write arbitrary bash.
|
||||
This is intentional; users cannot supply scripts. However, the variable
|
||||
substitution should be audited to ensure no user-controlled value (e.g.
|
||||
a server name containing shell metacharacters) can affect the script.
|
||||
|
||||
### Recommended hardening (next phase)
|
||||
|
||||
- Add explicit `../` stripping / validation of `path` on the panel side before
|
||||
sending to the agent.
|
||||
- Sanitise all `%variable%` substitution inputs (strip shell metacharacters
|
||||
from home_name, ip, port before substitution).
|
||||
- Consider signing or hashing the post_script blob to detect tampering.
|
||||
- Rate-limit install actions per user to prevent abuse.
|
||||
|
||||
---
|
||||
|
||||
## 6. Proposed Next Database Fields
|
||||
|
||||
```sql
|
||||
ALTER TABLE OGP_DB_PREFIXaddons
|
||||
MODIFY addon_type VARCHAR(32) NOT NULL, -- already applied in db_version 2
|
||||
ADD COLUMN install_method VARCHAR(32) NOT NULL DEFAULT 'download_zip',
|
||||
ADD COLUMN content_version VARCHAR(64) NULL,
|
||||
ADD COLUMN requires_stop TINYINT(1) NOT NULL DEFAULT 1,
|
||||
ADD COLUMN backup_before_install TINYINT(1) NOT NULL DEFAULT 1,
|
||||
ADD COLUMN restart_after_install TINYINT(1) NOT NULL DEFAULT 0,
|
||||
ADD COLUMN is_profile TINYINT(1) NOT NULL DEFAULT 0,
|
||||
ADD COLUMN description TEXT NULL;
|
||||
```
|
||||
|
||||
Apply this as `$install_queries[2]` (db_version 3) in `module.php` when ready.
|
||||
|
||||
---
|
||||
|
||||
## 7. Proposed Install Methods
|
||||
|
||||
| install_method | Description |
|
||||
|-------------------|---------------------------------------------------------------------|
|
||||
| `download_zip` | Download a .zip / .tar.gz and extract into the server path (current default) |
|
||||
| `download_file` | Download a single file (no extraction) into the server path |
|
||||
| `post_script` | Run only the post_script — no download, no extraction |
|
||||
| `steam_workshop` | Pass Workshop item IDs to the agent's `steamcmd +workshop_download_item` helper |
|
||||
| `minecraft_jar` | Download a server jar from Mojang / Paper / Purpur / Fabric APIs |
|
||||
| `profile_copy` | Copy a stored profile directory tree into the server home |
|
||||
|
||||
---
|
||||
|
||||
## 8. Proposed Categories (server_content_categories.php)
|
||||
|
||||
| addon_type | Display label | Notes |
|
||||
|-------------|---------------------|------------------------------------|
|
||||
| `plugin` | Plugins / Mods | Original — always present |
|
||||
| `mappack` | Map Packs | Original — always present |
|
||||
| `config` | Config Packs | Original — always present |
|
||||
| `version` | Server Versions | e.g. Minecraft jar switcher |
|
||||
| `modpack` | Modpacks | CurseForge / ATLauncher packs |
|
||||
| `workshop` | Workshop Content | Steam Workshop (requires VARCHAR(32)) |
|
||||
| `script` | Scripted Installer | Admin-defined script only |
|
||||
| `profile` | Server Profiles | Full profile: configs + mods + scripts |
|
||||
|
||||
---
|
||||
|
||||
## 9. Recommended Phased Migration Plan
|
||||
|
||||
### Phase 1 (complete)
|
||||
- [x] UI labels renamed to "Server Content Manager / Server Content".
|
||||
- [x] Central category map created (`server_content_categories.php`).
|
||||
- [x] `addon_type` column expanded to VARCHAR(32) via db_version 2 migration.
|
||||
- [x] `addons_installer.php` and `user_addons.php` use category map for validation.
|
||||
- [x] Full TODO/comment blocks added to installer for next phase work.
|
||||
- [x] Module folder, table names, URL routes, function names unchanged.
|
||||
- [x] Workshop Content Phase 1: manual Workshop ID entry, per-server manifest,
|
||||
agent script runner, install/update/remove actions (`db_version 3`).
|
||||
|
||||
### Phase 2 — Schema & install_method support
|
||||
- [x] Apply Phase 2 schema: `install_method`, `content_version`, `requires_stop`,
|
||||
`backup_before_install`, `restart_after_install`, `is_cacheable`, `description`
|
||||
columns added to `addons` table via `$install_queries[3]` (db_version 4).
|
||||
- [x] `server_content_manifest` table created – tracks one row per installed
|
||||
content item per server home (version, checksum, state, installed_by).
|
||||
- [x] `server_content_install_history` table created – one row per install
|
||||
attempt with result code, log snippet, and cache_mode_used.
|
||||
- [x] `install_method` dropdown added to admin create/edit form.
|
||||
- [x] `content_version`, `description`, `is_cacheable` fields added to form.
|
||||
- [x] `requires_stop` / `backup_before_install` / `restart_after_install`
|
||||
checkboxes added to admin form.
|
||||
- [x] `requires_stop` check implemented in installer: blocks install if server
|
||||
is running (Phase 3 will add automatic stop/start).
|
||||
- [x] Install history row written on `state=start`; completed on success.
|
||||
- [x] Manifest row upserted on successful install.
|
||||
- [x] `server_content_cache_mode` panel setting added (disabled / search_existing_servers /
|
||||
shared_cache / shared_cache_and_search; default: **disabled**).
|
||||
- [x] `is_cacheable` flag stored per content item; cache mode and is_cacheable
|
||||
guard are in place — actual cross-server/cache copy logic is Phase 3.
|
||||
- [ ] `backup_before_install` agent call (Phase 3 — needs agent tar/zip helper).
|
||||
- [ ] `restart_after_install` agent call (Phase 3 — needs server start after install).
|
||||
|
||||
### Phase 3 — Steam Workshop integration
|
||||
See Part 6 below.
|
||||
|
||||
### Phase 4 — Minecraft jar / version switcher
|
||||
See Part 7 below.
|
||||
|
||||
### Phase 5 — DayZ / Arma profile switcher
|
||||
See Part 8 below.
|
||||
|
||||
---
|
||||
|
||||
## 10. Part 6: Steam Workshop Integration
|
||||
|
||||
### Concept
|
||||
Steam Workshop content is treated as a Server Content type (`addon_type=workshop`,
|
||||
`install_method=steam_workshop`).
|
||||
|
||||
### Browser UI
|
||||
- A "Workshop Browser" page within the module fetches the workshop item list
|
||||
from Steam's Web API and lets users select items.
|
||||
- Selected item IDs are stored as server content selections linked to the home.
|
||||
|
||||
### Agent side
|
||||
- The agent runs `steamcmd +login anonymous +workshop_download_item <appid> <item_id> +quit`
|
||||
for each selected item.
|
||||
- Downloaded content is moved into the correct server mod directory.
|
||||
- The agent reports progress back to the panel via the existing rsync_progress mechanism
|
||||
or a new workshop_progress RPC.
|
||||
|
||||
### Restart behaviour (configurable per content item)
|
||||
| Mode | Description |
|
||||
|------|-------------|
|
||||
| 1 | Install immediately if server is stopped |
|
||||
| 2 | Queue installation to run on next restart |
|
||||
| 3 | Restart automatically if updates are available |
|
||||
| 4 | Notify only — do not install automatically |
|
||||
|
||||
---
|
||||
|
||||
## 11. Part 7: Minecraft Example
|
||||
|
||||
### Base game: Minecraft
|
||||
|
||||
### Server Content options (addon_type=version, install_method=minecraft_jar)
|
||||
|
||||
| Content Item | Source API / URL |
|
||||
|-------------------|---------------------------------------------------------------|
|
||||
| Vanilla 1.21.x | Mojang version manifest API |
|
||||
| Paper 1.21.x | papermc.io API |
|
||||
| Purpur 1.21.x | purpurmc.org API |
|
||||
| Forge 1.20.1 | files.minecraftforge.net |
|
||||
| Fabric 1.20.1 | meta.fabricmc.net |
|
||||
| Modpack installer | CurseForge / ATLauncher / FTB API (addon_type=modpack) |
|
||||
|
||||
### Install flow
|
||||
1. Admin creates a content item with `install_method=minecraft_jar` and sets
|
||||
`url` to the download endpoint (or a version ID for API-resolved URLs).
|
||||
2. User selects the version from the Server Content page.
|
||||
3. Installer downloads the jar to the server home path.
|
||||
4. post_script patches the startup command line with the new jar filename.
|
||||
5. If `restart_after_install=1`, the server restarts with the new jar.
|
||||
|
||||
---
|
||||
|
||||
## 12. Part 8: DayZ / Arma Example
|
||||
|
||||
### Base game: Arma 2 / DayZ-capable server
|
||||
|
||||
### Server Content options
|
||||
|
||||
| Content Item | Type | Description |
|
||||
|--------------|---------|--------------------------------------------------|
|
||||
| DayZ Vanilla | config | Vanilla DayZ config + mission files |
|
||||
| DayZ Epoch | profile | Epoch mod files + config profile |
|
||||
| Overpoch | profile | Combined Overwatch + Epoch profile |
|
||||
| Map Pack | mappack | Additional map files (Chernarus, Lingor, etc.) |
|
||||
| Config Pack | config | Server config preset (difficulty, loot tables) |
|
||||
|
||||
### Install flow
|
||||
1. Admin defines each option as a Server Content item with `install_method=download_zip`
|
||||
or `install_method=profile_copy`.
|
||||
2. post_script copies required files, patches `mission.sqm`, `server.cfg`, etc.
|
||||
3. If `requires_stop=1`, the server is stopped before applying changes.
|
||||
4. If `restart_after_install=1`, the server starts with the new profile.
|
||||
|
||||
---
|
||||
|
||||
## 13. Part 9: Security Direction
|
||||
|
||||
### Core principles
|
||||
|
||||
1. **Users must not be allowed to enter arbitrary commands.**
|
||||
Admins define Server Content items including scripts.
|
||||
Users only select from the approved list.
|
||||
|
||||
2. **Script execution is scoped to the assigned server.**
|
||||
The post_script runs with only the target server home path and the approved
|
||||
replacement variables. It cannot reference paths outside the home directory.
|
||||
|
||||
3. **All paths must be validated against the home directory boundary.**
|
||||
- Strip or reject any `../` sequences in the `path` field.
|
||||
- Reject absolute paths unless the content item is explicitly marked
|
||||
admin-only and the admin has been warned.
|
||||
- The agent enforces path containment at the OS level; the panel should
|
||||
add a redundant check as defence-in-depth.
|
||||
|
||||
4. **Replacement variable values must be shell-safe.**
|
||||
- Escape shell metacharacters in `home_name`, `ip`, `port`, `home_path`,
|
||||
etc. before substitution into post_script.
|
||||
- Consider wrapping each value in single quotes in the substituted script.
|
||||
|
||||
5. **Workshop and external API downloads must be verified.**
|
||||
- Check Content-Type and file signature/hash where possible.
|
||||
- Reject downloads that exceed a configurable size limit.
|
||||
|
||||
6. **Install history must be logged.**
|
||||
- Record who installed what, when, and the script exit code.
|
||||
- This log must be readable by admins but not modifiable by users.
|
||||
|
||||
---
|
||||
|
||||
## 14. Part 10: Content Reuse / Cache Mode Security Rules
|
||||
|
||||
### Governing setting: `server_content_cache_mode`
|
||||
|
||||
| Value | Agent behaviour |
|
||||
|--------------------------|-----------------|
|
||||
| `disabled` (**default**) | Always install from the configured source/script. No cross-server copy, no shared cache. |
|
||||
| `search_existing_servers` | Agent may scan other local game-server folders for matching **cacheable** content and copy directly if the checksum matches. |
|
||||
| `shared_cache` | Agent may store **cacheable** content in a shared cache folder and use it for future installs. |
|
||||
| `shared_cache_and_search` | Both `shared_cache` and `search_existing_servers` are active. |
|
||||
|
||||
### Content eligibility for sharing / caching
|
||||
|
||||
Only content where **`is_cacheable = 1`** on the addon record may ever be
|
||||
shared or placed in the shared cache. Admins must explicitly opt in per item.
|
||||
|
||||
**Never mark the following as cacheable:**
|
||||
- Config files, server.cfg, whitelist / banlist, RCON passwords
|
||||
- User-edited files, saves, player databases
|
||||
- Log files, crash dumps
|
||||
- Files containing credentials, tokens, or keys
|
||||
- Entire server home directories
|
||||
|
||||
**Safe to mark cacheable (public, non-sensitive, reproducible):**
|
||||
- Publicly distributed map packs (.bsp, .nav, .vmf bundles)
|
||||
- Workshop content / mod archives sourced from public URLs
|
||||
- Vanilla server jars (Minecraft, etc.) from official APIs
|
||||
- Stock config templates (not user-edited copies)
|
||||
|
||||
### Checksum enforcement
|
||||
|
||||
When a cached or copied file is used, the agent **must** verify the SHA-256
|
||||
checksum against the value stored in `server_content_manifest.checksum_sha256`
|
||||
(if present). A checksum mismatch must abort the install and fall back to the
|
||||
original source download.
|
||||
|
||||
### Path restrictions
|
||||
|
||||
- The agent must never copy files **outside** approved content paths.
|
||||
- Users **cannot** specify source paths. Only admin-defined content items
|
||||
define where content comes from and where it is placed.
|
||||
- Path traversal (`../`) is forbidden in all `path` fields. The panel
|
||||
validates this; the agent provides a second layer of defence.
|
||||
- The shared cache folder must be separate from any game server home and not
|
||||
accessible directly by game server processes.
|
||||
|
||||
### Phase 3 implementation checklist (not yet done)
|
||||
|
||||
- [ ] Agent-side: implement copy-from-existing-server with checksum verification.
|
||||
- [ ] Agent-side: implement shared cache store and restore.
|
||||
- [ ] Panel-side: validate `is_cacheable` before passing cache-mode hint to agent.
|
||||
- [ ] Panel-side: expose cache hit/miss in install history log.
|
||||
- [ ] Shared cache path configurable in panel settings (`server_content_cache_path`).
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
# Server Content Workshop – Phase 1
|
||||
|
||||
Phase 1 adds manual Workshop ID support inside the existing `addonsmanager` module (user-facing label: **Server Content**).
|
||||
|
||||
## Scope (Phase 1)
|
||||
|
||||
- No Steam Workshop browser/search UI yet.
|
||||
- No Steam scraping.
|
||||
- User enters comma-separated numeric Workshop IDs.
|
||||
- Panel validates IDs, removes duplicates, and stores them per server home.
|
||||
- Panel lists saved IDs and supports:
|
||||
- Install New
|
||||
- Update Selected
|
||||
- Remove Selected
|
||||
- Update All
|
||||
- Panel generates a per-server manifest at:
|
||||
- `%home_path%/gsp_server_content/workshop_manifest.json`
|
||||
- Panel runs an approved script path (safe default or game-specific config), never user-supplied command/path.
|
||||
|
||||
## Security model
|
||||
|
||||
- Ownership check: non-admin users can only access homes assigned to them; admins can access any home.
|
||||
- Actions are scoped to one `home_id`.
|
||||
- IDs must be numeric only.
|
||||
- Script path is not user-editable.
|
||||
- Manifest path is validated to remain under server home.
|
||||
- Remove is non-destructive in the generic scripts (preserve/move behavior for Phase 1).
|
||||
- All actions are logged through panel logging.
|
||||
|
||||
## Database
|
||||
|
||||
Phase 1 introduces:
|
||||
|
||||
- `OGP_DB_PREFIXserver_content_workshop`
|
||||
|
||||
and keeps `OGP_DB_PREFIXaddons.addon_type` at `VARCHAR(32)` so `workshop` is valid.
|
||||
|
||||
## Game/admin config TODO (next phase hardening)
|
||||
|
||||
Each game should define and document:
|
||||
|
||||
- `workshop_app_id`
|
||||
- Linux workshop script path
|
||||
- Windows/Cygwin workshop script path
|
||||
- target install location
|
||||
- restart/update behavior
|
||||
|
||||
## Phase 2 (not included here)
|
||||
|
||||
- Workshop browsing/search/select UI
|
||||
- richer metadata/title lookups
|
||||
- per-game install adapters and deeper status reporting
|
||||
|
|
@ -1,25 +1,93 @@
|
|||
<?php
|
||||
/*
|
||||
*
|
||||
* OGP - Open Game Panel
|
||||
* Copyright (C) 2008 - 2018 The OGP Development Team
|
||||
* GSP - Game Server Panel (a heavily customized fork of OGP maintained by WDS)
|
||||
*
|
||||
* http://www.opengamepanel.org/
|
||||
* Server Content Installer (module: addonsmanager, page: addons)
|
||||
* ─────────────────────────────────────────────────────────────────────────────
|
||||
* This file handles the actual download+extraction and post-install script
|
||||
* execution for a Server Content item selected by a user.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 2
|
||||
* of the License, or any later version.
|
||||
* CURRENT FLOW:
|
||||
* 1. User selects a content type (plugin / mappack / config / ...) from
|
||||
* user_addons.php which links here with addon_type=<type>.
|
||||
* 2. User picks a specific content item from a dropdown.
|
||||
* 3. On form submit, state=start is set and start_file_download() is called
|
||||
* on the remote agent with the configured URL and target path.
|
||||
* 4. The agent downloads and extracts the archive.
|
||||
* 5. If a post_script is defined it is run on the agent after extraction.
|
||||
* 6. The page auto-refreshes (state=refresh) to show download/script progress.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
* 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
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
* 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)
|
||||
|
|
@ -41,14 +109,17 @@ function do_progress($kbytes,$totalsize)
|
|||
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');
|
||||
|
||||
function exec_ogp_module() {
|
||||
|
||||
global $db,$view;
|
||||
$home_id = $_REQUEST['home_id'];
|
||||
$mod_id = $_REQUEST['mod_id'];
|
||||
$ip = $_REQUEST['ip'];
|
||||
$port = $_REQUEST['port'];
|
||||
$mod_id = $_REQUEST['mod_id'];
|
||||
$ip = $_REQUEST['ip'];
|
||||
$port = $_REQUEST['port'];
|
||||
$user_id = $_SESSION['user_id'];
|
||||
|
||||
$isAdmin = $db->isAdmin( $_SESSION['user_id'] );
|
||||
|
|
@ -76,19 +147,21 @@ function exec_ogp_module() {
|
|||
}
|
||||
|
||||
$home_cfg_id = $home_info['home_cfg_id'];
|
||||
$server_xml = read_server_config(SERVER_CONFIG_LOCATION."/".$home_info['home_cfg_file']);
|
||||
$server_xml = read_server_config(SERVER_CONFIG_LOCATION."/".$home_info['home_cfg_file']);
|
||||
|
||||
$addon_types = array('plugin', 'mappack', 'config');
|
||||
$addon_type = isset($_REQUEST['addon_type']) ? $_REQUEST['addon_type'] : "";
|
||||
// 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']) ? $_REQUEST['addon_type'] : "";
|
||||
|
||||
$state = isset($_REQUEST['state']) ? $_REQUEST['state'] : "";
|
||||
$pid = isset($_REQUEST['pid']) ? $_REQUEST['pid'] : -1;
|
||||
$pid = isset($_REQUEST['pid']) ? $_REQUEST['pid'] : -1;
|
||||
|
||||
if ( $state != "" )
|
||||
{
|
||||
$addon_id = (int)$_REQUEST['addon_id'];
|
||||
|
||||
$addons_rows = $db->resultQuery("SELECT url, path, post_script FROM OGP_DB_PREFIXaddons WHERE addon_id=".$addon_id.$query_groups);
|
||||
$addons_rows = $db->resultQuery("SELECT url, path, post_script, addon_type, install_method, content_version, requires_stop, restart_after_install FROM OGP_DB_PREFIXaddons WHERE addon_id=".$addon_id.$query_groups);
|
||||
if (!is_array($addons_rows)) {
|
||||
$addons_rows = [];
|
||||
}
|
||||
|
|
@ -101,10 +174,30 @@ function exec_ogp_module() {
|
|||
|
||||
$remote = new OGPRemoteLibrary($home_info['agent_ip'],$home_info['agent_port'],$home_info['encryption_key'],$home_info['timeout']);
|
||||
|
||||
$addon_info = $addons_rows[0];
|
||||
$addon_info = $addons_rows[0];
|
||||
$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 = !empty($addon_info['requires_stop']) ? 1 : 0;
|
||||
|
||||
// ── requires_stop guard ───────────────────────────────────────────────
|
||||
// If the content item requires the server to be stopped first, check
|
||||
// whether the server is currently running and block the install if so.
|
||||
// (Phase 2 blocks install; automatic stop/start is Phase 3.)
|
||||
if ( $state == "start" && $requires_stop ) {
|
||||
$is_running = $remote->is_screen_running( $home_info['home_name'], $home_info['home_id'] );
|
||||
if ( $is_running === 1 ) {
|
||||
print_failure('This content item requires the server to be stopped before installing. Please stop the server and try again.');
|
||||
echo "<p><a href=\"?m=addonsmanager&p=addons&addon_type=".urlencode($addon_info['addon_type'] ?? '')."&home_id=$home_id&mod_id=$mod_id&ip=$ip&port=$port\">".get_lang('back')."</a></p>";
|
||||
return;
|
||||
}
|
||||
}
|
||||
$url = $addon_info['url'];
|
||||
$filename = basename($url);
|
||||
#### This makes replacements to the bash script:
|
||||
#### 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']);
|
||||
|
|
@ -153,9 +246,23 @@ function exec_ogp_module() {
|
|||
}
|
||||
}
|
||||
|
||||
#### end of replacememnts
|
||||
if ( $state == "start" AND $addon_id != "" )
|
||||
#### 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;
|
||||
$pid = $remote->start_file_download( $addon_info['url'], $home_info['home_path']."/".$addon_info['path'], $filename, "uncompress", $post_script);
|
||||
}
|
||||
|
||||
$headers = get_headers($url, 1);
|
||||
|
||||
|
|
@ -224,6 +331,19 @@ function exec_ogp_module() {
|
|||
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,
|
||||
));
|
||||
echo "<p><a href=\"?m=addonsmanager&p=user_addons&home_id=$home_id".
|
||||
"&mod_id=$mod_id&ip=$ip&port=$port\">".get_lang('back')."</a></p>";
|
||||
$view->refresh("?m=addonsmanager&p=user_addons&home_id=$home_id".
|
||||
|
|
@ -249,9 +369,20 @@ function exec_ogp_module() {
|
|||
|
||||
return;
|
||||
}
|
||||
if ($addon_type === 'workshop') {
|
||||
scm_ensure_workshop_schema($db);
|
||||
$view->refresh('?m=addonsmanager&p=workshop_content&home_id='.(int)$home_id.'&mod_id='.(int)$mod_id.'&ip='.urlencode((string)$ip).'&port='.urlencode((string)$port), 0);
|
||||
return;
|
||||
}
|
||||
|
||||
?>
|
||||
<h2><?php echo htmlentities($home_info['home_name'])." ".get_lang($addon_type) ;?></h2>
|
||||
<?php
|
||||
$addon_type_lang_key = "server_content_".$addon_type;
|
||||
$addon_type_lang = get_lang($addon_type_lang_key);
|
||||
if($addon_type_lang === "_".$addon_type_lang_key."_")
|
||||
$addon_type_lang = get_lang($addon_type);
|
||||
?>
|
||||
<h2><?php echo htmlentities($home_info['home_name'])." ".$addon_type_lang ;?></h2>
|
||||
<table class='center'>
|
||||
<form method='get'>
|
||||
<input type='hidden' name='m' value='addonsmanager' />
|
||||
|
|
|
|||
|
|
@ -2,31 +2,45 @@
|
|||
<?php
|
||||
/*
|
||||
*
|
||||
* OGP - Open Game Panel
|
||||
* Copyright (C) 2008 - 2018 The OGP Development Team
|
||||
* GSP - Game Server Panel (a heavily customized fork of OGP maintained by WDS)
|
||||
*
|
||||
* http://www.opengamepanel.org/
|
||||
* Admin page: Server Content Manager (module: addonsmanager)
|
||||
* ─────────────────────────────────────────────────────────────────────────────
|
||||
* This page lets admins create, edit, and remove Server Content items.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 2
|
||||
* of the License, or any later version.
|
||||
* A "Server Content item" is anything that can be pushed to a game server:
|
||||
* 1. A zip/file package extracted into the server directory.
|
||||
* 2. A downloaded file placed into the server directory.
|
||||
* 3. A script-driven installer (post_script only, no download required).
|
||||
* 4. A Minecraft server jar / version switcher (future: install_method=minecraft_jar).
|
||||
* 5. A DayZ/Epoch/Arma profile copy (future: install_method=profile_copy).
|
||||
* 6. A Steam Workshop content bundle (future: install_method=steam_workshop).
|
||||
* 7. A config preset (type=config).
|
||||
* 8. A full server profile built from multiple actions (type=profile).
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
* DB table: OGP_DB_PREFIXaddons (unchanged for backward compatibility).
|
||||
* See SERVER_CONTENT_ROADMAP.md for the full migration plan.
|
||||
*
|
||||
*/
|
||||
|
||||
// Central category map — defines all valid addon_type values and their labels.
|
||||
require_once(dirname(__FILE__) . '/server_content_categories.php');
|
||||
require_once(dirname(__FILE__) . '/server_content_helpers.php');
|
||||
|
||||
function exec_ogp_module() {
|
||||
|
||||
global $db;
|
||||
|
||||
|
||||
// Ensure Phase 2 schema is present (idempotent).
|
||||
scm_ensure_phase2_schema($db);
|
||||
|
||||
// Build the complete list of allowed content types from the category map.
|
||||
// Admins can create items of any registered type; the original three types
|
||||
// (plugin, mappack, config) are always included.
|
||||
$addon_types = get_server_content_type_keys(); // all keys
|
||||
$addon_type_labels = get_server_content_categories(); // key => label
|
||||
$install_methods = scm_get_install_methods(); // install_method keys => labels
|
||||
|
||||
if (isset($_POST['create_addon']) AND isset($_POST['name']) AND $_POST['url']=="")
|
||||
{
|
||||
print_failure(get_lang("fill_the_url_address_to_a_compressed_file"));
|
||||
|
|
@ -45,13 +59,21 @@ function exec_ogp_module() {
|
|||
}
|
||||
elseif (isset($_POST['create_addon']) AND isset($_POST['name']) AND isset($_POST['url']) AND isset($_POST['addon_type']) and isset($_POST['home_cfg_id']) )
|
||||
{
|
||||
$fields['name'] = $_POST['name'];
|
||||
$fields['url'] = $_POST['url'];
|
||||
$fields['path'] = $_POST['path'];
|
||||
$fields['addon_type'] = $_POST['addon_type'];
|
||||
$fields['home_cfg_id'] = $_POST['home_cfg_id'];
|
||||
$fields['post_script'] = $_POST['post_script'];
|
||||
$fields['group_id'] = $_POST['group_id'];
|
||||
$valid_install_methods = array_keys($install_methods);
|
||||
$fields['name'] = $_POST['name'];
|
||||
$fields['url'] = $_POST['url'];
|
||||
$fields['path'] = $_POST['path'];
|
||||
$fields['addon_type'] = $_POST['addon_type'];
|
||||
$fields['home_cfg_id'] = $_POST['home_cfg_id'];
|
||||
$fields['post_script'] = $_POST['post_script'];
|
||||
$fields['group_id'] = $_POST['group_id'];
|
||||
$fields['install_method'] = in_array($_POST['install_method'], $valid_install_methods) ? $_POST['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'] : '';
|
||||
if( is_numeric($db->resultInsertId( 'addons', $fields )) )
|
||||
{
|
||||
print_success(get_lang_f("addon_has_been_created",$_POST['name']));
|
||||
|
|
@ -61,14 +83,20 @@ function exec_ogp_module() {
|
|||
}
|
||||
|
||||
echo "<h2>".get_lang('addons_manager')."</h2>";
|
||||
$name = isset($_POST['name']) ? $_POST['name'] : "";
|
||||
$url = isset($_POST['url']) ? $_POST['url'] : "";
|
||||
$path = isset($_POST['path']) ? $_POST['path'] : "";
|
||||
$post_script = isset($_POST['post_script']) ? $_POST['post_script'] : "";
|
||||
$home_cfg_id = isset($_POST['home_cfg_id']) ? $_POST['home_cfg_id'] : "";
|
||||
$addon_type = isset($_POST['addon_type']) ? $_POST['addon_type'] : "";
|
||||
$group_id = isset($_POST['group_id']) ? $_POST['group_id'] : "";
|
||||
$addon_types = array('plugin', 'mappack', 'config');
|
||||
$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'] : "";
|
||||
|
||||
if (isset($_POST['addon_id']) && (int)$_POST['addon_id'] > 0 && isset($_POST['edit']))
|
||||
{
|
||||
|
|
@ -76,14 +104,21 @@ function exec_ogp_module() {
|
|||
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 = isset($addon_info['addon_type']) ? $addon_info['addon_type'] : "";
|
||||
$group_id = isset($addon_info['group_id']) ? $addon_info['group_id'] : "";
|
||||
$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 = isset($addon_info['addon_type']) ? $addon_info['addon_type'] : "";
|
||||
$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'] : "";
|
||||
}
|
||||
?>
|
||||
<form action="" method="post">
|
||||
|
|
@ -104,7 +139,8 @@ function exec_ogp_module() {
|
|||
<input type="text" value="<?php echo $url; ?>" name="url" size="85" title="<?php print_lang('url_info'); ?>" />
|
||||
</td>
|
||||
</tr>
|
||||
<!-- If any, you can set the destination path, should be a relative path to the main game server folder. -->
|
||||
<!-- Destination path — must be relative to the game server home directory.
|
||||
Path traversal (../) is not allowed; the agent enforces this. -->
|
||||
<tr>
|
||||
<td align="right">
|
||||
<b><?php print_lang('path'); ?></b>
|
||||
|
|
@ -174,11 +210,12 @@ function exec_ogp_module() {
|
|||
</td>
|
||||
<td align="left">
|
||||
<?php
|
||||
$types = array( 'plugin', 'mappack', 'config' );
|
||||
foreach ((array)$types as $type)
|
||||
// Render a radio button for every registered content type.
|
||||
// New types automatically appear here once added to server_content_categories.php.
|
||||
foreach ((array)$addon_type_labels as $type_key => $type_label)
|
||||
{
|
||||
$checked = ( isset($addon_type) AND $type == $addon_type) ? 'checked' : '';
|
||||
echo '<input type="radio" name="addon_type" value="'.$type.'" '.$checked.'>'.get_lang($type);
|
||||
$checked = ( isset($addon_type) AND $type_key == $addon_type) ? 'checked' : '';
|
||||
echo '<input type="radio" name="addon_type" value="'.htmlspecialchars($type_key).'" '.$checked.'>'.htmlspecialchars($type_label).' ';
|
||||
}
|
||||
?>
|
||||
</td>
|
||||
|
|
@ -204,6 +241,79 @@ function exec_ogp_module() {
|
|||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- ── Phase 2 fields ────────────────────────────────────────────────── -->
|
||||
<tr>
|
||||
<td align="right">
|
||||
<b>Install Method</b>
|
||||
</td>
|
||||
<td align="left">
|
||||
<select name="install_method">
|
||||
<?php
|
||||
foreach ((array)$install_methods as $method_key => $method_label) {
|
||||
$sel = ($method_key == $install_method) ? 'selected="selected"' : '';
|
||||
echo '<option value="'.htmlspecialchars($method_key).'" '.$sel.'>'.htmlspecialchars($method_label).'</option>'."\n";
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
<small style="color:#666;"> The mechanism used to deliver this content to the server.</small>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="right">
|
||||
<b>Content Version</b>
|
||||
</td>
|
||||
<td align="left">
|
||||
<input type="text" value="<?php echo htmlspecialchars($content_version, ENT_QUOTES, 'UTF-8'); ?>" name="content_version" size="40" placeholder="e.g. 1.21.1 or 2024-05-01" />
|
||||
<small style="color:#666;"> Optional version tag shown in the installed-content list.</small>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="right">
|
||||
<b>Description</b>
|
||||
</td>
|
||||
<td align="left">
|
||||
<textarea name="description" style="width:99%;height:60px;" placeholder="Short description shown to users."><?php echo htmlspecialchars($description, ENT_QUOTES, 'UTF-8'); ?></textarea>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="right">
|
||||
<b>Behaviour Options</b>
|
||||
</td>
|
||||
<td align="left">
|
||||
<label>
|
||||
<input type="checkbox" name="requires_stop" value="1" <?php echo $requires_stop ? 'checked' : ''; ?> />
|
||||
Stop server before installing
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<input type="checkbox" name="backup_before_install" value="1" <?php echo $backup_before_install ? 'checked' : ''; ?> />
|
||||
Backup target path before installing
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<input type="checkbox" name="restart_after_install" value="1" <?php echo $restart_after_install ? 'checked' : ''; ?> />
|
||||
Restart server after successful install
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="right">
|
||||
<b>Content Reuse</b>
|
||||
</td>
|
||||
<td align="left">
|
||||
<label>
|
||||
<input type="checkbox" name="is_cacheable" value="1" <?php echo $is_cacheable ? 'checked' : ''; ?> />
|
||||
Mark as cacheable / reusable
|
||||
</label>
|
||||
<small style="color:#666;">
|
||||
Only check this for public, non-sensitive content (maps, mods, jars).
|
||||
<strong>Never</strong> check for configs, saves, credentials, or user-edited files.
|
||||
Caching only activates when the <em>Server Content Cache Mode</em> panel
|
||||
setting (in Panel Settings) is set to something other than <em>Disabled</em>.
|
||||
</small>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- ── end Phase 2 fields ─────────────────────────────────────────────── -->
|
||||
<tr>
|
||||
<td colspan="2" align="center">
|
||||
<?php
|
||||
|
|
@ -276,14 +386,14 @@ function exec_ogp_module() {
|
|||
<?php
|
||||
$option = '';
|
||||
|
||||
foreach ((array)$addon_types as $k) {
|
||||
foreach ((array)$addon_type_labels as $k => $label) {
|
||||
$option .= '<option';
|
||||
|
||||
if (isset($_GET['addon_type']) && $_GET['addon_type'] == $k) {
|
||||
$option .= ' selected';
|
||||
}
|
||||
|
||||
$option .= ' value="'. $k .'">'.get_lang($k).'</option>';
|
||||
$option .= ' value="'. htmlspecialchars($k) .'">'.htmlspecialchars($label).'</option>';
|
||||
}
|
||||
|
||||
echo $option;
|
||||
|
|
@ -324,8 +434,9 @@ function exec_ogp_module() {
|
|||
}
|
||||
|
||||
$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']) && is_array($addon_types) && 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;
|
||||
// Validate the requested addon_type against the full category map so new types are accepted.
|
||||
$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']) )
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,45 +1,168 @@
|
|||
<?php
|
||||
/*
|
||||
*
|
||||
* OGP - Open Game Panel
|
||||
* Copyright (C) 2008 - 2018 The OGP Development Team
|
||||
* GSP - Game Server Panel (a heavily customized fork of OGP maintained by WDS)
|
||||
*
|
||||
* http://www.opengamepanel.org/
|
||||
* Module: addonsmanager → Server Content Manager
|
||||
* ─────────────────────────────────────────────────────────────────────────────
|
||||
* The module folder and DB table names are intentionally unchanged for
|
||||
* backward compatibility. Only UI labels have been updated to the new
|
||||
* "Server Content" terminology.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
* db_version history:
|
||||
* 1 – initial schema (addons table, addon_type VARCHAR(7))
|
||||
* 2 – expand addon_type to VARCHAR(32) to support extended content types
|
||||
* (workshop=8 chars, and any future type up to 32 chars)
|
||||
* 3 – add server_content_workshop table for per-server Workshop item selections
|
||||
* 4 – Phase 2: add install_method / content_version / requires_stop /
|
||||
* backup_before_install / restart_after_install / is_cacheable /
|
||||
* description columns to addons table; add server_content_manifest
|
||||
* and server_content_install_history tables
|
||||
*
|
||||
*/
|
||||
|
||||
// Module general information
|
||||
$module_title = "Addons Manager";
|
||||
$module_version = "1.0";
|
||||
$db_version = 1;
|
||||
$module_title = "Server Content Manager";
|
||||
$module_version = "2.2";
|
||||
$db_version = 4;
|
||||
$module_required = TRUE;
|
||||
$module_menus = array( array( 'subpage' => 'addons_manager', 'name'=>'Addons Manager', 'group'=>'admin' ) );
|
||||
$module_menus = array(
|
||||
array( 'subpage' => 'addons_manager', 'name' => 'Server Content Manager', 'group' => 'admin' )
|
||||
);
|
||||
|
||||
$install_queries = array();
|
||||
// ── 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
|
||||
`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;
|
||||
},
|
||||
);
|
||||
?>
|
||||
|
|
|
|||
|
|
@ -1,24 +1,12 @@
|
|||
<?php
|
||||
/*
|
||||
*
|
||||
* OGP - Open Game Panel
|
||||
* Copyright (C) 2008 - 2018 The OGP Development Team
|
||||
* GSP - Game Server Panel (a heavily customized fork of OGP maintained by WDS)
|
||||
*
|
||||
* http://www.opengamepanel.org/
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
* Monitor button: Server Content (module: addonsmanager)
|
||||
* ─────────────────────────────────────────────────────────────────────────────
|
||||
* Injects a "Server Content" button into the game monitor toolbar when at
|
||||
* least one Server Content item is configured for the server's game type.
|
||||
*
|
||||
*/
|
||||
|
||||
|
|
|
|||
|
|
@ -2,4 +2,5 @@
|
|||
<page key="user_addons" file="user_addons.php" access="admin,user" />
|
||||
<page key="addons_manager" file="addons_manager.php" access="admin" />
|
||||
<page key="addons" file="addons_installer.php" access="admin,user" />
|
||||
<page key="workshop_content" file="workshop_content.php" access="admin,user" />
|
||||
</navigation>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
MANIFEST_PATH="${1:-}"
|
||||
if [[ -z "$MANIFEST_PATH" ]]; then
|
||||
echo "Usage: $0 <manifest_path>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$MANIFEST_PATH" ]]; then
|
||||
echo "Manifest not found: $MANIFEST_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
MANIFEST_DIR="$(dirname "$MANIFEST_PATH")"
|
||||
WORKSHOP_DIR="${MANIFEST_DIR}/workshop"
|
||||
REMOVED_DIR="${WORKSHOP_DIR}/removed"
|
||||
LOG_FILE="${MANIFEST_DIR}/workshop_phase1.log"
|
||||
|
||||
mkdir -p "$WORKSHOP_DIR" "$REMOVED_DIR"
|
||||
|
||||
ACTION="$(python3 - <<'PY' "$MANIFEST_PATH"
|
||||
import json,sys
|
||||
with open(sys.argv[1], "r", encoding="utf-8") as f:
|
||||
data=json.load(f)
|
||||
print(data.get("action",""))
|
||||
PY
|
||||
)"
|
||||
|
||||
ITEMS="$(python3 - <<'PY' "$MANIFEST_PATH"
|
||||
import json,sys
|
||||
with open(sys.argv[1], "r", encoding="utf-8") as f:
|
||||
data=json.load(f)
|
||||
items=data.get("items",[])
|
||||
print(",".join(str(x) for x in items if str(x).isdigit()))
|
||||
PY
|
||||
)"
|
||||
|
||||
{
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] workshop_phase1 action=${ACTION} manifest=${MANIFEST_PATH}"
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] workshop_phase1 items=${ITEMS}"
|
||||
} >> "$LOG_FILE"
|
||||
|
||||
case "$ACTION" in
|
||||
install|update)
|
||||
# TODO: Replace with game-specific SteamCMD workshop install/update logic.
|
||||
# Example flow:
|
||||
# 1) Use workshop_app_id + item IDs from the manifest.
|
||||
# 2) Download/refresh content into a controlled staging folder.
|
||||
# 3) Copy/sync approved files into the game server content path.
|
||||
;;
|
||||
remove)
|
||||
# Phase 1 safety behavior: avoid destructive delete.
|
||||
# TODO: move/disable per-item content using game-specific mapping rules.
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] remove requested; preserving files (non-destructive phase 1)." >> "$LOG_FILE"
|
||||
;;
|
||||
*)
|
||||
echo "Unknown workshop action: ${ACTION}" >> "$LOG_FILE"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
exit 0
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
MANIFEST_PATH="${1:-}"
|
||||
if [[ -z "$MANIFEST_PATH" ]]; then
|
||||
echo "Usage: $0 <manifest_path>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$MANIFEST_PATH" ]]; then
|
||||
echo "Manifest not found: $MANIFEST_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
MANIFEST_DIR="$(dirname "$MANIFEST_PATH")"
|
||||
WORKSHOP_DIR="${MANIFEST_DIR}/workshop"
|
||||
REMOVED_DIR="${WORKSHOP_DIR}/removed"
|
||||
LOG_FILE="${MANIFEST_DIR}/workshop_phase1_windows.log"
|
||||
|
||||
mkdir -p "$WORKSHOP_DIR" "$REMOVED_DIR"
|
||||
|
||||
ACTION="$(python3 - <<'PY' "$MANIFEST_PATH"
|
||||
import json,sys
|
||||
with open(sys.argv[1], "r", encoding="utf-8") as f:
|
||||
data=json.load(f)
|
||||
print(data.get("action",""))
|
||||
PY
|
||||
)"
|
||||
|
||||
ITEMS="$(python3 - <<'PY' "$MANIFEST_PATH"
|
||||
import json,sys
|
||||
with open(sys.argv[1], "r", encoding="utf-8") as f:
|
||||
data=json.load(f)
|
||||
items=data.get("items",[])
|
||||
print(",".join(str(x) for x in items if str(x).isdigit()))
|
||||
PY
|
||||
)"
|
||||
|
||||
{
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] workshop_phase1_windows action=${ACTION} manifest=${MANIFEST_PATH}"
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] workshop_phase1_windows items=${ITEMS}"
|
||||
} >> "$LOG_FILE"
|
||||
|
||||
case "$ACTION" in
|
||||
install|update)
|
||||
# TODO: Replace with game-specific SteamCMD workshop install/update logic for Cygwin environments.
|
||||
;;
|
||||
remove)
|
||||
# Phase 1 safety behavior: avoid destructive delete.
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] remove requested; preserving files (non-destructive phase 1)." >> "$LOG_FILE"
|
||||
;;
|
||||
*)
|
||||
echo "Unknown workshop action: ${ACTION}" >> "$LOG_FILE"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
exit 0
|
||||
472
Panel/modules/addonsmanager/server_content_actions.php
Normal file
472
Panel/modules/addonsmanager/server_content_actions.php
Normal file
|
|
@ -0,0 +1,472 @@
|
|||
<?php
|
||||
/*
|
||||
*
|
||||
* GSP - Server Content scheduler action hooks (addonsmanager)
|
||||
*
|
||||
*/
|
||||
|
||||
require_once("includes/lib_remote.php");
|
||||
require_once("modules/config_games/server_config_parser.php");
|
||||
require_once(dirname(__FILE__) . '/server_content_helpers.php');
|
||||
require_once(dirname(__FILE__) . '/workshop_action.php');
|
||||
|
||||
function server_content_get_home_info($home_id)
|
||||
{
|
||||
global $db;
|
||||
$home_id = (int)$home_id;
|
||||
if ($home_id <= 0) {
|
||||
return false;
|
||||
}
|
||||
return $db->getGameHome($home_id);
|
||||
}
|
||||
|
||||
function server_content_collect_workshop_ids($db, $home_id)
|
||||
{
|
||||
$rows = $db->resultQuery(
|
||||
"SELECT workshop_item_id FROM `".OGP_DB_PREFIX."server_content_workshop`
|
||||
WHERE home_id=".(int)$home_id." AND install_state<>'removed'"
|
||||
);
|
||||
$item_ids = array();
|
||||
if (is_array($rows)) {
|
||||
foreach ((array)$rows as $row) {
|
||||
$item_id = trim((string)$row['workshop_item_id']);
|
||||
if ($item_id !== '' && preg_match('/^[0-9]+$/', $item_id)) {
|
||||
$item_ids[$item_id] = $item_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
return array_values($item_ids);
|
||||
}
|
||||
|
||||
function server_content_collect_manifest_addon_rows($db, $home_id)
|
||||
{
|
||||
$rows = $db->resultQuery(
|
||||
"SELECT m.addon_id, m.install_method, m.content_version, m.install_state, a.name, a.url
|
||||
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=".(int)$home_id."
|
||||
ORDER BY m.updated_at DESC, m.installed_at DESC"
|
||||
);
|
||||
return is_array($rows) ? $rows : array();
|
||||
}
|
||||
|
||||
function server_content_create_remote(array $home_info)
|
||||
{
|
||||
return new OGPRemoteLibrary(
|
||||
$home_info['agent_ip'],
|
||||
$home_info['agent_port'],
|
||||
$home_info['encryption_key'],
|
||||
$home_info['timeout']
|
||||
);
|
||||
}
|
||||
|
||||
function server_content_result($status, $message, array $details = array())
|
||||
{
|
||||
return array(
|
||||
'status' => (string)$status,
|
||||
'success' => ((string)$status === 'success' || (string)$status === 'no_updates' || (string)$status === 'restart_required'),
|
||||
'message' => (string)$message,
|
||||
'details' => $details,
|
||||
);
|
||||
}
|
||||
|
||||
function server_content_record_installed_content_state(array $home_info, array $state)
|
||||
{
|
||||
$home_path = rtrim(clean_path((string)$home_info['home_path']), '/');
|
||||
$file_path = clean_path($home_path . '/gsp_server_content/installed_content.json');
|
||||
if (!scm_path_is_under_home($home_path, $file_path)) {
|
||||
return false;
|
||||
}
|
||||
$json = json_encode($state);
|
||||
if ($json === false) {
|
||||
return false;
|
||||
}
|
||||
$remote = server_content_create_remote($home_info);
|
||||
$remote->exec("mkdir -p " . escapeshellarg(dirname($file_path)));
|
||||
return ((int)$remote->remote_writefile($file_path, $json) === 1);
|
||||
}
|
||||
|
||||
function server_content_log_action($home_id, $action, $status, $message = '', $details = array())
|
||||
{
|
||||
global $db;
|
||||
$payload = array(
|
||||
'home_id' => (int)$home_id,
|
||||
'action' => (string)$action,
|
||||
'status' => (string)$status,
|
||||
'message' => (string)$message,
|
||||
'details' => $details,
|
||||
);
|
||||
$db->logger("server_content_action " . json_encode($payload));
|
||||
return true;
|
||||
}
|
||||
|
||||
function server_content_build_manifest($home_id, $content_type, $action, $items = array(), $options = array())
|
||||
{
|
||||
$home_info = server_content_get_home_info($home_id);
|
||||
if ($home_info === false) {
|
||||
return false;
|
||||
}
|
||||
$home_path = rtrim(clean_path((string)$home_info['home_path']), '/');
|
||||
$manifest_dir = clean_path($home_path . '/gsp_server_content/manifests');
|
||||
$file_stub = preg_replace('/[^a-z0-9_\-]+/i', '_', (string)$content_type . '_' . (string)$action);
|
||||
if ($file_stub === '' || $file_stub === null) {
|
||||
$file_stub = 'manifest';
|
||||
}
|
||||
$manifest_path = clean_path($manifest_dir . '/' . $file_stub . '_' . date('Ymd_His') . '_' . mt_rand(1000, 9999) . '.json');
|
||||
if (!scm_path_is_under_home($home_path, $manifest_path)) {
|
||||
return false;
|
||||
}
|
||||
$manifest = array(
|
||||
'manifest_version' => 1,
|
||||
'home_id' => (int)$home_info['home_id'],
|
||||
'home_cfg_id' => (int)$home_info['home_cfg_id'],
|
||||
'remote_server_id' => (int)$home_info['remote_server_id'],
|
||||
'content_type' => (string)$content_type,
|
||||
'action' => (string)$action,
|
||||
'items' => is_array($items) ? array_values($items) : array(),
|
||||
'options' => is_array($options) ? $options : array(),
|
||||
'generated_at' => date('Y-m-d H:i:s'),
|
||||
);
|
||||
$manifest_json = json_encode($manifest);
|
||||
if ($manifest_json === false) {
|
||||
return false;
|
||||
}
|
||||
$remote = server_content_create_remote($home_info);
|
||||
$remote->exec("mkdir -p " . escapeshellarg($manifest_dir));
|
||||
if ((int)$remote->remote_writefile($manifest_path, $manifest_json) !== 1) {
|
||||
return false;
|
||||
}
|
||||
return $manifest_path;
|
||||
}
|
||||
|
||||
function server_content_resolve_script_path(array $home_info, $script_key, array $options = array())
|
||||
{
|
||||
$script_path = '';
|
||||
if (isset($options['script_path'])) {
|
||||
$script_path = trim((string)$options['script_path']);
|
||||
}
|
||||
$server_xml = read_server_config(SERVER_CONFIG_LOCATION . "/" . $home_info['home_cfg_file']);
|
||||
if ($server_xml === false) {
|
||||
return array(false, false);
|
||||
}
|
||||
if ($script_path === '' && $script_key === 'workshop') {
|
||||
$script_path = scm_get_workshop_script_path($home_info, $server_xml);
|
||||
}
|
||||
if ($script_path === '' && $script_key !== '' && isset($server_xml->$script_key)) {
|
||||
$script_path = trim((string)$server_xml->$script_key);
|
||||
}
|
||||
return array($server_xml, $script_path);
|
||||
}
|
||||
|
||||
function server_content_execute_manifest($home_id, $manifest_path, $script_key, $options = array())
|
||||
{
|
||||
$home_info = server_content_get_home_info($home_id);
|
||||
if ($home_info === false) {
|
||||
return server_content_result('failed', 'Invalid server home.', array('home_id' => (int)$home_id));
|
||||
}
|
||||
list($server_xml, $script_path) = server_content_resolve_script_path($home_info, $script_key, $options);
|
||||
if ($server_xml === false) {
|
||||
return server_content_result('failed', 'Unable to load server XML configuration.', array('home_id' => (int)$home_id));
|
||||
}
|
||||
$script_path = trim((string)$script_path);
|
||||
if ($script_path === '' || !preg_match('/^[^\r\n\0]+$/', $script_path)) {
|
||||
return server_content_result('failed', 'Configured server content script path is invalid.', array('script_key' => (string)$script_key));
|
||||
}
|
||||
$remote = server_content_create_remote($home_info);
|
||||
if ($remote->status_chk() !== 1) {
|
||||
return server_content_result('failed', 'Agent is offline.', array('remote_server_id' => (int)$home_info['remote_server_id']));
|
||||
}
|
||||
if ((int)$remote->rfile_exists($script_path) !== 1) {
|
||||
return server_content_result('failed', 'Server content script was not found on agent host.', array('script_path' => $script_path));
|
||||
}
|
||||
$command = "bash " . escapeshellarg($script_path) . " " . escapeshellarg((string)$manifest_path) . " ; echo __GSP_SERVER_CONTENT_EXIT:$?";
|
||||
$output = $remote->exec($command);
|
||||
if (!is_string($output) || $output === '') {
|
||||
return server_content_result('failed', 'Server content script did not return output.', array('script_path' => $script_path));
|
||||
}
|
||||
if (!preg_match('/__GSP_SERVER_CONTENT_EXIT:(\d+)/', $output, $matches)) {
|
||||
return server_content_result('failed', 'Server content script exit marker was not found.', array('output' => trim($output)));
|
||||
}
|
||||
$exit_code = (int)$matches[1];
|
||||
if ($exit_code !== 0) {
|
||||
return server_content_result('failed', 'Server content script failed.', array(
|
||||
'exit_code' => $exit_code,
|
||||
'output' => trim($output),
|
||||
'script_path' => $script_path,
|
||||
));
|
||||
}
|
||||
return server_content_result('success', 'Server content script executed successfully.', array(
|
||||
'exit_code' => $exit_code,
|
||||
'output' => trim($output),
|
||||
'script_path' => $script_path,
|
||||
'manifest_path' => (string)$manifest_path,
|
||||
));
|
||||
}
|
||||
|
||||
function server_content_check_updates($home_id, $options = array())
|
||||
{
|
||||
$options['check_only'] = true;
|
||||
return server_content_install_updates($home_id, $options);
|
||||
}
|
||||
|
||||
function server_content_update_workshop($home_id, $options = array())
|
||||
{
|
||||
$options['workshop_only'] = true;
|
||||
return server_content_install_updates($home_id, $options);
|
||||
}
|
||||
|
||||
function server_content_install_updates($home_id, $options = array())
|
||||
{
|
||||
global $db;
|
||||
$home_info = server_content_get_home_info($home_id);
|
||||
if ($home_info === false) {
|
||||
return server_content_result('failed', 'Invalid server home.');
|
||||
}
|
||||
scm_ensure_phase2_schema($db);
|
||||
scm_ensure_workshop_schema($db);
|
||||
|
||||
$workshop_action = isset($options['workshop_action']) ? (string)$options['workshop_action'] : '';
|
||||
if ($workshop_action === '') {
|
||||
$workshop_action = !empty($options['check_only']) ? 'check_updates' : 'update';
|
||||
}
|
||||
$workshop_ids = server_content_collect_workshop_ids($db, (int)$home_info['home_id']);
|
||||
$manifest_rows = server_content_collect_manifest_addon_rows($db, (int)$home_info['home_id']);
|
||||
if (empty($workshop_ids) && empty($manifest_rows)) {
|
||||
$result = server_content_result('no_updates', 'No installed server content records were found for this home.', array(
|
||||
'home_id' => (int)$home_info['home_id'],
|
||||
));
|
||||
server_content_record_installed_content_state($home_info, array(
|
||||
'home_id' => (int)$home_info['home_id'],
|
||||
'last_action' => 'no_updates',
|
||||
'last_updated_at' => date('Y-m-d H:i:s'),
|
||||
));
|
||||
return $result;
|
||||
}
|
||||
|
||||
if (!empty($workshop_ids) && empty($options['check_only'])) {
|
||||
scm_workshop_update_rows_state($db, (int)$home_info['home_id'], $workshop_ids, 'installing', null, false, false);
|
||||
}
|
||||
|
||||
$manifest_items = array(
|
||||
'workshop_item_ids' => $workshop_ids,
|
||||
'manifest_addons' => $manifest_rows,
|
||||
);
|
||||
$manifest_path = server_content_build_manifest($home_info['home_id'], 'server_content', $workshop_action, $manifest_items, $options);
|
||||
if ($manifest_path === false) {
|
||||
if (!empty($workshop_ids) && empty($options['check_only'])) {
|
||||
scm_workshop_update_rows_state($db, (int)$home_info['home_id'], $workshop_ids, 'failed', 'Failed to build server content manifest.', false, false);
|
||||
}
|
||||
return server_content_result('failed', 'Failed to build server content manifest.');
|
||||
}
|
||||
|
||||
$execute = server_content_execute_manifest($home_info['home_id'], $manifest_path, 'workshop', $options);
|
||||
if (empty($execute['success'])) {
|
||||
if (!empty($workshop_ids) && empty($options['check_only'])) {
|
||||
$error_message = isset($execute['message']) ? $execute['message'] : 'Unknown failure.';
|
||||
scm_workshop_update_rows_state($db, (int)$home_info['home_id'], $workshop_ids, 'failed', $error_message, false, false);
|
||||
}
|
||||
return $execute;
|
||||
}
|
||||
|
||||
if (!empty($workshop_ids) && empty($options['check_only'])) {
|
||||
scm_workshop_update_rows_state($db, (int)$home_info['home_id'], $workshop_ids, 'installed', null, false, true);
|
||||
}
|
||||
server_content_record_installed_content_state($home_info, array(
|
||||
'home_id' => (int)$home_info['home_id'],
|
||||
'last_action' => (string)$workshop_action,
|
||||
'last_result' => 'success',
|
||||
'last_manifest' => $manifest_path,
|
||||
'last_updated_at' => date('Y-m-d H:i:s'),
|
||||
'installed_workshop_ids' => $workshop_ids,
|
||||
));
|
||||
|
||||
if (!empty($options['check_only'])) {
|
||||
return server_content_result('success', 'Server content update check completed.', array(
|
||||
'manifest_path' => $manifest_path,
|
||||
'workshop_items' => count($workshop_ids),
|
||||
));
|
||||
}
|
||||
return server_content_result('success', 'Server content updates were installed.', array(
|
||||
'manifest_path' => $manifest_path,
|
||||
'workshop_items' => count($workshop_ids),
|
||||
'manifest_rows' => count($manifest_rows),
|
||||
));
|
||||
}
|
||||
|
||||
function server_content_home_is_running(array $home_info)
|
||||
{
|
||||
$remote = server_content_create_remote($home_info);
|
||||
return ($remote->is_screen_running(OGP_SCREEN_TYPE_HOME, $home_info['home_id']) == 1);
|
||||
}
|
||||
|
||||
function server_content_install_updates_if_stopped($home_id, $options = array())
|
||||
{
|
||||
$home_info = server_content_get_home_info($home_id);
|
||||
if ($home_info === false) {
|
||||
return server_content_result('failed', 'Invalid server home.');
|
||||
}
|
||||
if (server_content_home_is_running($home_info)) {
|
||||
return server_content_result('restart_required', 'Server is running; update skipped until server is stopped.', array(
|
||||
'home_id' => (int)$home_info['home_id'],
|
||||
));
|
||||
}
|
||||
return server_content_install_updates($home_id, $options);
|
||||
}
|
||||
|
||||
function server_content_install_updates_next_restart($home_id, $options = array())
|
||||
{
|
||||
$home_info = server_content_get_home_info($home_id);
|
||||
if ($home_info === false) {
|
||||
return server_content_result('failed', 'Invalid server home.');
|
||||
}
|
||||
$options['queued_for_restart'] = true;
|
||||
$manifest_path = server_content_build_manifest($home_info['home_id'], 'server_content', 'install_next_restart', array(), $options);
|
||||
if ($manifest_path === false) {
|
||||
return server_content_result('failed', 'Failed to queue update manifest for next restart.');
|
||||
}
|
||||
server_content_record_installed_content_state($home_info, array(
|
||||
'home_id' => (int)$home_info['home_id'],
|
||||
'last_action' => 'install_next_restart',
|
||||
'last_result' => 'queued',
|
||||
'queued_manifest' => $manifest_path,
|
||||
'last_updated_at' => date('Y-m-d H:i:s'),
|
||||
));
|
||||
return server_content_result('restart_required', 'Server content updates were queued for next restart.', array(
|
||||
'manifest_path' => $manifest_path,
|
||||
));
|
||||
}
|
||||
|
||||
function server_content_restart_home($home_id, $options = array())
|
||||
{
|
||||
global $db, $user_info;
|
||||
$home_info = server_content_get_home_info($home_id);
|
||||
if ($home_info === false) {
|
||||
return server_content_result('failed', 'Invalid server home.');
|
||||
}
|
||||
$server_xml = read_server_config(SERVER_CONFIG_LOCATION . "/" . $home_info['home_cfg_file']);
|
||||
if ($server_xml === false) {
|
||||
return server_content_result('failed', 'Could not load server XML for restart.');
|
||||
}
|
||||
$remote = server_content_create_remote($home_info);
|
||||
$host_stat = $remote->status_chk();
|
||||
if ($host_stat !== 1) {
|
||||
return server_content_result('failed', 'Agent is offline; cannot restart server.');
|
||||
}
|
||||
|
||||
$ip_ports = $db->getHomeIpPorts($home_info['home_id']);
|
||||
if (!is_array($ip_ports) || !isset($ip_ports[0])) {
|
||||
return server_content_result('failed', 'No IP/port mapping found for server restart.');
|
||||
}
|
||||
$ip = $ip_ports[0]['ip'];
|
||||
$port = (int)$ip_ports[0]['port'];
|
||||
|
||||
$mod_id = key($home_info['mods']);
|
||||
$start_cmd = get_start_cmd($user_info, $remote, $server_xml, $home_info, $mod_id, $ip, $port, $db);
|
||||
$preStart = isset($server_xml->pre_start) ? trim((string)$server_xml->pre_start) : "";
|
||||
$envVars = isset($server_xml->environment_variables) ? trim((string)$server_xml->environment_variables) : "";
|
||||
|
||||
$delay = isset($options['restart_delay_seconds']) ? (int)$options['restart_delay_seconds'] : 0;
|
||||
if ($delay > 0) {
|
||||
if ($delay > 300) {
|
||||
$delay = 300;
|
||||
}
|
||||
sleep($delay);
|
||||
}
|
||||
|
||||
$remote_retval = $remote->remote_restart_server(
|
||||
$home_info['home_id'],
|
||||
$ip,
|
||||
$port,
|
||||
$server_xml->control_protocol,
|
||||
$home_info['control_password'],
|
||||
$server_xml->control_protocol_type,
|
||||
$home_info['home_path'],
|
||||
$server_xml->server_exec_name,
|
||||
$server_xml->exe_location,
|
||||
$start_cmd,
|
||||
$home_info['mods'][$mod_id]['cpu_affinity'],
|
||||
$home_info['mods'][$mod_id]['nice'],
|
||||
$preStart,
|
||||
$envVars,
|
||||
$server_xml->game_key,
|
||||
(isset($server_xml->console_log) ? $server_xml->console_log : "")
|
||||
);
|
||||
if ($remote_retval !== 1) {
|
||||
return server_content_result('restart_required', 'Update completed but automatic restart failed.', array(
|
||||
'restart_status' => $remote_retval,
|
||||
));
|
||||
}
|
||||
return server_content_result('success', 'Update completed and server restart was triggered.', array(
|
||||
'restart_status' => $remote_retval,
|
||||
));
|
||||
}
|
||||
|
||||
function server_content_install_updates_and_restart($home_id, $options = array())
|
||||
{
|
||||
$install_result = server_content_install_updates($home_id, $options);
|
||||
if (empty($install_result['success']) || $install_result['status'] === 'failed') {
|
||||
return $install_result;
|
||||
}
|
||||
$restart_result = server_content_restart_home($home_id, $options);
|
||||
$install_result['details']['restart'] = $restart_result;
|
||||
if ($restart_result['status'] === 'success') {
|
||||
$install_result['status'] = 'success';
|
||||
$install_result['message'] = 'Server content updates installed and server restart requested.';
|
||||
return $install_result;
|
||||
}
|
||||
$install_result['status'] = 'restart_required';
|
||||
$install_result['success'] = true;
|
||||
$install_result['message'] = 'Server content updates installed; restart is still required.';
|
||||
return $install_result;
|
||||
}
|
||||
|
||||
function server_content_run_scheduled_action($home_id, $action, $options = array())
|
||||
{
|
||||
$home_id = (int)$home_id;
|
||||
$action = trim((string)$action);
|
||||
if ($home_id <= 0) {
|
||||
return server_content_result('failed', 'Invalid server home id.');
|
||||
}
|
||||
$handlers = array(
|
||||
'server_content_check_updates' => 'server_content_check_updates',
|
||||
'server_content_check_workshop_updates' => 'server_content_update_workshop',
|
||||
'server_content_install_updates_if_stopped' => 'server_content_install_updates_if_stopped',
|
||||
'server_content_install_updates_next_restart' => 'server_content_install_updates_next_restart',
|
||||
'server_content_install_updates_now' => 'server_content_install_updates',
|
||||
'server_content_install_updates_and_restart' => 'server_content_install_updates_and_restart',
|
||||
'server_content_update_workshop' => 'server_content_update_workshop',
|
||||
'server_content_update_all' => 'server_content_install_updates',
|
||||
'server_content_notify_updates_only' => 'server_content_check_updates',
|
||||
'server_content_validate_files' => 'server_content_update_workshop',
|
||||
'server_content_backup_before_update' => 'server_content_install_updates',
|
||||
);
|
||||
if (!isset($handlers[$action]) || !function_exists($handlers[$action])) {
|
||||
$result = server_content_result('failed', 'Unsupported scheduled server content action.', array(
|
||||
'action' => $action,
|
||||
));
|
||||
server_content_log_action($home_id, $action, $result['status'], $result['message'], $result['details']);
|
||||
return $result;
|
||||
}
|
||||
|
||||
if ($action === 'server_content_check_workshop_updates' || $action === 'server_content_validate_files') {
|
||||
$options['check_only'] = true;
|
||||
$options['workshop_action'] = ($action === 'server_content_validate_files') ? 'validate_files' : 'check_updates';
|
||||
}
|
||||
if ($action === 'server_content_backup_before_update') {
|
||||
$options['backup_before_update'] = true;
|
||||
}
|
||||
if ($action === 'server_content_install_updates_and_restart' && !isset($options['safe_restart'])) {
|
||||
$options['safe_restart'] = true;
|
||||
}
|
||||
if ($action === 'server_content_notify_updates_only') {
|
||||
$options['notify_only'] = true;
|
||||
$options['check_only'] = true;
|
||||
}
|
||||
|
||||
server_content_log_action($home_id, $action, 'started', 'Scheduled action started.', $options);
|
||||
$handler = $handlers[$action];
|
||||
$result = $handler($home_id, $options);
|
||||
server_content_log_action($home_id, $action, $result['status'], $result['message'], $result['details']);
|
||||
return $result;
|
||||
}
|
||||
|
||||
82
Panel/modules/addonsmanager/server_content_categories.php
Normal file
82
Panel/modules/addonsmanager/server_content_categories.php
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
<?php
|
||||
/*
|
||||
*
|
||||
* GSP - Game Server Panel (a heavily customized fork of OGP maintained by WDS)
|
||||
*
|
||||
* Server Content Category Map
|
||||
* ─────────────────────────────────────────────────────────────────────────────
|
||||
* This file is the single source of truth for all Server Content types.
|
||||
* It maps internal addon_type DB values to human-readable display labels.
|
||||
*
|
||||
* BACKWARD COMPATIBILITY:
|
||||
* The three original types (plugin, mappack, config) are preserved exactly
|
||||
* as they exist in the addons table. Do not rename or remove them.
|
||||
*
|
||||
* ADDING NEW TYPES:
|
||||
* 1. Add the key/label pair below.
|
||||
* 2. Ensure the DB column addon_type is VARCHAR(32) (migration db_version 2
|
||||
* in module.php handles this automatically on the next module update).
|
||||
* 3. No other code changes are required for the type to appear in the admin
|
||||
* "Create Server Content Item" form.
|
||||
*
|
||||
* PLANNED INSTALL METHODS (see SERVER_CONTENT_ROADMAP.md for details):
|
||||
* download_zip – download a .zip/.tar.gz and extract into the server path
|
||||
* download_file – download a single file into the server path
|
||||
* post_script – run only a post-install bash script (no download)
|
||||
* steam_workshop – install/update via Steam Workshop item IDs through agent
|
||||
* minecraft_jar – download a Minecraft server jar and update startup config
|
||||
* profile_copy – copy a profile directory tree into the server home
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* Returns the full Server Content category map.
|
||||
*
|
||||
* Keys : addon_type values stored in OGP_DB_PREFIXaddons.addon_type
|
||||
* Values : Human-readable display label shown in admin and user UI
|
||||
*
|
||||
* @return array<string,string>
|
||||
*/
|
||||
function get_server_content_categories()
|
||||
{
|
||||
return array(
|
||||
// ── Original types (must remain for backward compatibility) ──────────
|
||||
'plugin' => 'Plugins / Mods',
|
||||
'mappack' => 'Map Packs',
|
||||
'config' => 'Config Packs',
|
||||
|
||||
// ── Extended types (require addon_type VARCHAR(32) – db_version 2) ──
|
||||
'version' => 'Server Versions', // e.g. Minecraft jar switcher
|
||||
'modpack' => 'Modpacks', // e.g. CurseForge / ATLauncher packs
|
||||
'workshop' => 'Workshop Content', // Steam Workshop item bundles
|
||||
'script' => 'Scripted Installer', // Admin-defined install-only scripts
|
||||
'profile' => 'Server Profiles', // Full profile: configs + mods + scripts
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns only the original three types that existed before db_version 2.
|
||||
* Use this when you need to restrict to legacy values, e.g. for
|
||||
* installs that have not yet run the VARCHAR(32) migration.
|
||||
*
|
||||
* @return array<string,string>
|
||||
*/
|
||||
function get_legacy_addon_types()
|
||||
{
|
||||
return array(
|
||||
'plugin' => 'Plugins / Mods',
|
||||
'mappack' => 'Map Packs',
|
||||
'config' => 'Config Packs',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an ordered list of addon_type keys only (no labels).
|
||||
* Useful as a whitelist for input validation.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
function get_server_content_type_keys()
|
||||
{
|
||||
return array_keys(get_server_content_categories());
|
||||
}
|
||||
492
Panel/modules/addonsmanager/server_content_helpers.php
Normal file
492
Panel/modules/addonsmanager/server_content_helpers.php
Normal file
|
|
@ -0,0 +1,492 @@
|
|||
<?php
|
||||
/*
|
||||
*
|
||||
* GSP - Server Content helpers (addonsmanager)
|
||||
*
|
||||
*/
|
||||
|
||||
if (!defined('SCM_WORKSHOP_SCRIPT_LINUX_DEFAULT')) {
|
||||
define('SCM_WORKSHOP_SCRIPT_LINUX_DEFAULT', '/home/gameserver/OGP_User_Files/modules/addonsmanager/scripts/workshop/generic_steam_workshop_linux.sh');
|
||||
}
|
||||
if (!defined('SCM_WORKSHOP_SCRIPT_WINDOWS_DEFAULT')) {
|
||||
define('SCM_WORKSHOP_SCRIPT_WINDOWS_DEFAULT', '/home/gameserver/OGP_User_Files/modules/addonsmanager/scripts/workshop/generic_steam_workshop_windows_cygwin.sh');
|
||||
}
|
||||
|
||||
function scm_ensure_workshop_schema($db)
|
||||
{
|
||||
static $schema_checked = false;
|
||||
if ($schema_checked) {
|
||||
return true;
|
||||
}
|
||||
$schema_checked = true;
|
||||
|
||||
$db->query("ALTER TABLE `".OGP_DB_PREFIX."addons` MODIFY `addon_type` VARCHAR(32) NOT NULL");
|
||||
return (bool)$db->query(
|
||||
"CREATE TABLE IF NOT EXISTS `".OGP_DB_PREFIX."server_content_workshop` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
`home_id` INT NOT NULL,
|
||||
`home_cfg_id` INT NOT NULL,
|
||||
`remote_server_id` INT NULL,
|
||||
`workshop_app_id` VARCHAR(32) NULL,
|
||||
`workshop_item_id` VARCHAR(64) NOT NULL,
|
||||
`title` VARCHAR(255) NULL,
|
||||
`install_state` VARCHAR(32) NOT NULL DEFAULT 'selected',
|
||||
`last_installed_at` DATETIME NULL,
|
||||
`last_updated_at` DATETIME NULL,
|
||||
`last_error` TEXT NULL,
|
||||
`created_by` INT NULL,
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME NULL,
|
||||
UNIQUE KEY `uniq_home_workshop_item` (`home_id`, `workshop_item_id`),
|
||||
KEY `idx_home_id` (`home_id`),
|
||||
KEY `idx_home_cfg_id` (`home_cfg_id`),
|
||||
KEY `idx_install_state` (`install_state`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
|
||||
);
|
||||
}
|
||||
|
||||
function scm_get_home_for_user($db, $home_id, $user_id)
|
||||
{
|
||||
$home_id = (int)$home_id;
|
||||
$user_id = (int)$user_id;
|
||||
if ($home_id <= 0 || $user_id <= 0) {
|
||||
return false;
|
||||
}
|
||||
if ($db->isAdmin($user_id)) {
|
||||
return $db->getGameHome($home_id);
|
||||
}
|
||||
return $db->getUserGameHome($user_id, $home_id);
|
||||
}
|
||||
|
||||
function scm_get_workshop_saved_count($db, $home_id)
|
||||
{
|
||||
$home_id = (int)$home_id;
|
||||
if ($home_id <= 0 || !scm_ensure_workshop_schema($db)) {
|
||||
return 0;
|
||||
}
|
||||
$rows = $db->resultQuery(
|
||||
"SELECT COUNT(*) AS cnt FROM `".OGP_DB_PREFIX."server_content_workshop` WHERE home_id=".$home_id." AND install_state<>'removed'"
|
||||
);
|
||||
if (!is_array($rows) || !isset($rows[0]['cnt'])) {
|
||||
return 0;
|
||||
}
|
||||
return (int)$rows[0]['cnt'];
|
||||
}
|
||||
|
||||
function scm_get_workshop_rows($db, $home_id)
|
||||
{
|
||||
$home_id = (int)$home_id;
|
||||
if ($home_id <= 0 || !scm_ensure_workshop_schema($db)) {
|
||||
return array();
|
||||
}
|
||||
$rows = $db->resultQuery(
|
||||
"SELECT * FROM `".OGP_DB_PREFIX."server_content_workshop` WHERE home_id=".$home_id." ORDER BY created_at DESC, workshop_item_id ASC"
|
||||
);
|
||||
return is_array($rows) ? $rows : array();
|
||||
}
|
||||
|
||||
function scm_parse_workshop_ids($raw, &$invalid = array())
|
||||
{
|
||||
$invalid = array();
|
||||
$ids = array();
|
||||
$parts = explode(',', (string)$raw);
|
||||
foreach ((array)$parts as $part) {
|
||||
$value = trim((string)$part);
|
||||
if ($value === '') {
|
||||
continue;
|
||||
}
|
||||
if (!preg_match('/^[0-9]+$/', $value)) {
|
||||
$invalid[] = $value;
|
||||
continue;
|
||||
}
|
||||
$ids[$value] = $value;
|
||||
}
|
||||
return array_values($ids);
|
||||
}
|
||||
|
||||
function scm_parse_selected_workshop_ids($selected)
|
||||
{
|
||||
$ids = array();
|
||||
if (!is_array($selected)) {
|
||||
return $ids;
|
||||
}
|
||||
foreach ($selected as $item_id) {
|
||||
$item_id = trim((string)$item_id);
|
||||
if ($item_id !== '' && preg_match('/^[0-9]+$/', $item_id)) {
|
||||
$ids[$item_id] = $item_id;
|
||||
}
|
||||
}
|
||||
return array_values($ids);
|
||||
}
|
||||
|
||||
function scm_h($value)
|
||||
{
|
||||
return htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
function scm_is_windows_home(array $home_info)
|
||||
{
|
||||
$game_key = isset($home_info['game_key']) ? strtolower((string)$home_info['game_key']) : '';
|
||||
$cfg_file = isset($home_info['home_cfg_file']) ? strtolower((string)$home_info['home_cfg_file']) : '';
|
||||
return (strpos($game_key, 'win') !== false) || (strpos($cfg_file, 'win') !== false);
|
||||
}
|
||||
|
||||
function scm_path_is_under_home($home_path, $candidate_path)
|
||||
{
|
||||
$home_path = rtrim(clean_path((string)$home_path), '/');
|
||||
$candidate_path = clean_path((string)$candidate_path);
|
||||
if ($home_path === '' || $candidate_path === '') {
|
||||
return false;
|
||||
}
|
||||
return strpos($candidate_path.'/', $home_path.'/') === 0;
|
||||
}
|
||||
|
||||
function scm_get_workshop_manifest_path(array $home_info)
|
||||
{
|
||||
$home_path = rtrim(clean_path((string)$home_info['home_path']), '/');
|
||||
$manifest_path = clean_path($home_path . '/gsp_server_content/workshop_manifest.json');
|
||||
if (!scm_path_is_under_home($home_path, $manifest_path)) {
|
||||
return false;
|
||||
}
|
||||
return $manifest_path;
|
||||
}
|
||||
|
||||
function scm_extract_workshop_app_id($server_xml)
|
||||
{
|
||||
$candidates = array(
|
||||
'workshop_app_id',
|
||||
'workshop_appid',
|
||||
'steam_workshop_app_id',
|
||||
'steam_workshop_appid',
|
||||
);
|
||||
foreach ((array)$candidates as $candidate) {
|
||||
if (isset($server_xml->$candidate)) {
|
||||
$value = trim((string)$server_xml->$candidate);
|
||||
if ($value !== '' && preg_match('/^[0-9]+$/', $value)) {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function scm_get_workshop_script_path(array $home_info, $server_xml)
|
||||
{
|
||||
$key = scm_is_windows_home($home_info) ? 'workshop_script_windows' : 'workshop_script_linux';
|
||||
if (isset($server_xml->$key)) {
|
||||
$xml_path = trim((string)$server_xml->$key);
|
||||
if ($xml_path !== '' && preg_match('/^[^\\r\\n\\0]+$/', $xml_path)) {
|
||||
return $xml_path;
|
||||
}
|
||||
}
|
||||
return scm_is_windows_home($home_info) ? SCM_WORKSHOP_SCRIPT_WINDOWS_DEFAULT : SCM_WORKSHOP_SCRIPT_LINUX_DEFAULT;
|
||||
}
|
||||
|
||||
function scm_get_csrf_token()
|
||||
{
|
||||
if (empty($_SESSION['addonsmanager_workshop_csrf'])) {
|
||||
$_SESSION['addonsmanager_workshop_csrf'] = md5(uniqid((string)mt_rand(), true));
|
||||
}
|
||||
return $_SESSION['addonsmanager_workshop_csrf'];
|
||||
}
|
||||
|
||||
function scm_validate_csrf_token($token)
|
||||
{
|
||||
if (!isset($_SESSION['addonsmanager_workshop_csrf'])) {
|
||||
return false;
|
||||
}
|
||||
return hash_equals((string)$_SESSION['addonsmanager_workshop_csrf'], (string)$token);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 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<string,string> 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<string,string>
|
||||
*/
|
||||
function scm_get_install_methods()
|
||||
{
|
||||
return array(
|
||||
'download_zip' => 'Download & extract archive (.zip / .tar.gz)',
|
||||
'download_file' => 'Download single file (no extraction)',
|
||||
'post_script' => 'Run post-script only (no download)',
|
||||
'steam_workshop' => 'Steam Workshop (via agent SteamCMD)',
|
||||
'minecraft_jar' => 'Minecraft server jar / version switcher',
|
||||
'profile_copy' => 'Copy stored profile directory',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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",
|
||||
);
|
||||
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()"
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1,33 +1,30 @@
|
|||
<?php
|
||||
/*
|
||||
*
|
||||
* OGP - Open Game Panel
|
||||
* Copyright (C) 2008 - 2018 The OGP Development Team
|
||||
* GSP - Game Server Panel (a heavily customized fork of OGP maintained by WDS)
|
||||
*
|
||||
* http://www.opengamepanel.org/
|
||||
* User page: Server Content (module: addonsmanager, page: user_addons)
|
||||
* ─────────────────────────────────────────────────────────────────────────────
|
||||
* Shows the available Server Content categories for a specific game server
|
||||
* home. Each category that has at least one content item configured for the
|
||||
* server's game type is displayed as a clickable link that takes the user to
|
||||
* the installer (addons_installer.php).
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
* Group filtering: non-admin users only see content items assigned to one of
|
||||
* their groups, or to the "All groups" (group_id=0 / NULL) bucket.
|
||||
*
|
||||
*/
|
||||
|
||||
// Central category map — load so we can iterate all types dynamically.
|
||||
require_once(dirname(__FILE__) . '/server_content_categories.php');
|
||||
require_once(dirname(__FILE__) . '/server_content_helpers.php');
|
||||
|
||||
function exec_ogp_module() {
|
||||
global $db;
|
||||
$home_id = $_GET['home_id'];
|
||||
$mod_id = $_GET['mod_id'];
|
||||
$ip = $_GET['ip'];
|
||||
$port = $_GET['port'];
|
||||
$mod_id = $_GET['mod_id'];
|
||||
$ip = $_GET['ip'];
|
||||
$port = $_GET['port'];
|
||||
$user_id = $_SESSION['user_id'];
|
||||
// Check if user has some games.
|
||||
$isAdmin = $db->isAdmin( $user_id );
|
||||
|
|
@ -48,46 +45,71 @@ function exec_ogp_module() {
|
|||
}
|
||||
if ($home_info)
|
||||
{
|
||||
scm_ensure_workshop_schema($db);
|
||||
$home_cfg_id = $home_info['home_cfg_id'];
|
||||
echo "<h2>".get_lang('user_addons').": ".htmlentities($home_info['home_name'])."</h2>\n".
|
||||
echo "<h2>Server Content: ".htmlentities($home_info['home_name'])."</h2>\n".
|
||||
"<table class='center' >\n".
|
||||
"<tr><td>\n";
|
||||
$plugins = $db->resultQuery("SELECT DISTINCT addon_id, name, game_name ".
|
||||
"FROM OGP_DB_PREFIXaddons ".
|
||||
"NATURAL JOIN OGP_DB_PREFIXconfig_homes ".
|
||||
"WHERE addon_type='plugin' ".
|
||||
"AND home_cfg_id=".$home_cfg_id.$query_groups);
|
||||
$plugins_qty = is_array($plugins) ? count((array)$plugins) : 0;
|
||||
if($plugins and $plugins_qty >= 1)
|
||||
echo "<a href='?m=addonsmanager&p=addons&home_id=".$home_id.
|
||||
"&mod_id=".$mod_id."&addon_type=plugin&ip=".$ip.
|
||||
"&port=".$port."'>".get_lang('install_plugin')."(".$plugins_qty.")</a>\n";
|
||||
|
||||
$mappacks = $db->resultQuery("SELECT DISTINCT addon_id, name, game_name ".
|
||||
"FROM OGP_DB_PREFIXaddons ".
|
||||
"NATURAL JOIN OGP_DB_PREFIXconfig_homes ".
|
||||
"WHERE addon_type='mappack' ".
|
||||
"AND home_cfg_id=".$home_cfg_id.$query_groups);
|
||||
$mappacks_qty = is_array($mappacks) ? count((array)$mappacks) : 0;
|
||||
if($mappacks and $mappacks_qty >= 1){
|
||||
echo "</td><td>";
|
||||
echo "<a href='?m=addonsmanager&p=addons&home_id=".$home_id.
|
||||
"&mod_id=".$mod_id."&addon_type=mappack&ip=".$ip.
|
||||
"&port=".$port."'>".get_lang('install_mappack')."(".$mappacks_qty.")</a>\n";
|
||||
"<tr>\n";
|
||||
|
||||
// Iterate all registered content types. Each type that has at least
|
||||
// one item for this game generates a link to the installer page.
|
||||
// New types added to server_content_categories.php automatically
|
||||
// appear here without any further code changes.
|
||||
$categories = get_server_content_categories(); // key => label
|
||||
$printed_any_cell = false;
|
||||
|
||||
foreach ((array)$categories as $type_key => $type_label)
|
||||
{
|
||||
if ($type_key === 'workshop')
|
||||
{
|
||||
$workshop_count = scm_get_workshop_saved_count($db, (int)$home_id);
|
||||
if ($printed_any_cell)
|
||||
echo "</td><td>\n";
|
||||
else
|
||||
echo "<td>\n";
|
||||
$printed_any_cell = true;
|
||||
echo "<a href='?m=addonsmanager&p=workshop_content" .
|
||||
"&home_id=" . (int)$home_id .
|
||||
"&mod_id=" . (int)$mod_id .
|
||||
"&ip=" . htmlspecialchars($ip) .
|
||||
"&port=" . htmlspecialchars($port) . "'>" .
|
||||
"Workshop Content (" . (int)$workshop_count . ")" .
|
||||
"</a>\n";
|
||||
continue;
|
||||
}
|
||||
|
||||
$items = $db->resultQuery(
|
||||
"SELECT DISTINCT addon_id, name, game_name " .
|
||||
"FROM OGP_DB_PREFIXaddons " .
|
||||
"NATURAL JOIN OGP_DB_PREFIXconfig_homes " .
|
||||
"WHERE addon_type='" . $db->realEscapeSingle($type_key) . "' " .
|
||||
"AND home_cfg_id=" . (int)$home_cfg_id . $query_groups
|
||||
);
|
||||
$items_qty = is_array($items) ? count((array)$items) : 0;
|
||||
if ($items && $items_qty >= 1)
|
||||
{
|
||||
if ($printed_any_cell)
|
||||
echo "</td><td>\n";
|
||||
else
|
||||
echo "<td>\n";
|
||||
$printed_any_cell = true;
|
||||
// Display label comes from the category map; the internal
|
||||
// addon_type key is passed in the URL for backward compatibility.
|
||||
echo "<a href='?m=addonsmanager&p=addons" .
|
||||
"&home_id=" . (int)$home_id .
|
||||
"&mod_id=" . (int)$mod_id .
|
||||
"&addon_type=" . urlencode($type_key) .
|
||||
"&ip=" . htmlspecialchars($ip) .
|
||||
"&port=" . htmlspecialchars($port) . "'>" .
|
||||
htmlspecialchars($type_label) . " (" . $items_qty . ")" .
|
||||
"</a>\n";
|
||||
}
|
||||
}
|
||||
$configs = $db->resultQuery("SELECT DISTINCT addon_id, name, game_name ".
|
||||
"FROM OGP_DB_PREFIXaddons ".
|
||||
"NATURAL JOIN OGP_DB_PREFIXconfig_homes ".
|
||||
"WHERE addon_type='config' ".
|
||||
"AND home_cfg_id=".$home_cfg_id.$query_groups);
|
||||
$configs_qty = is_array($configs) ? count((array)$configs) : 0;
|
||||
if($configs and $configs_qty >= 1){
|
||||
echo "</td><td>";
|
||||
echo "<a href='?m=addonsmanager&p=addons&home_id=".$home_id.
|
||||
"&mod_id=".$mod_id."&addon_type=config&ip=".$ip.
|
||||
"&port=".$port."'>".get_lang('install_config')."(".$configs_qty.")</a>\n";
|
||||
}
|
||||
echo "</td></tr>\n".
|
||||
|
||||
if ($printed_any_cell)
|
||||
echo "</td>\n";
|
||||
|
||||
echo "</tr>\n".
|
||||
"</table>\n".
|
||||
"<form action='?m=gamemanager&p=game_monitor&home_id-mod_id-ip-port=$home_id-$mod_id-$ip-$port' method='POST'>\n".
|
||||
"<input type='submit' value='".get_lang('back')."' />\n".
|
||||
|
|
|
|||
277
Panel/modules/addonsmanager/workshop_action.php
Normal file
277
Panel/modules/addonsmanager/workshop_action.php
Normal file
|
|
@ -0,0 +1,277 @@
|
|||
<?php
|
||||
/*
|
||||
*
|
||||
* GSP - Workshop Content actions (Phase 1)
|
||||
*
|
||||
*/
|
||||
|
||||
require_once("includes/lib_remote.php");
|
||||
require_once("modules/config_games/server_config_parser.php");
|
||||
require_once(dirname(__FILE__) . '/server_content_helpers.php');
|
||||
|
||||
function scm_workshop_log_action($db, $home_id, $user_id, $message)
|
||||
{
|
||||
$db->logger("server_content_workshop home_id=".(int)$home_id." user_id=".(int)$user_id." ".$message);
|
||||
}
|
||||
|
||||
function scm_workshop_update_rows_state($db, $home_id, array $item_ids, $state, $error = null, $mark_install = false, $mark_update = false)
|
||||
{
|
||||
if (empty($item_ids)) {
|
||||
return true;
|
||||
}
|
||||
$escaped_ids = array();
|
||||
foreach ($item_ids as $item_id) {
|
||||
$escaped_ids[] = "'" . $db->realEscapeSingle((string)$item_id) . "'";
|
||||
}
|
||||
$set = array(
|
||||
"install_state='" . $db->realEscapeSingle($state) . "'",
|
||||
"updated_at=NOW()",
|
||||
);
|
||||
if ($mark_install) {
|
||||
$set[] = "last_installed_at=NOW()";
|
||||
}
|
||||
if ($mark_update) {
|
||||
$set[] = "last_updated_at=NOW()";
|
||||
}
|
||||
if ($error === null) {
|
||||
$set[] = "last_error=NULL";
|
||||
} else {
|
||||
$set[] = "last_error='" . $db->realEscapeSingle($error) . "'";
|
||||
}
|
||||
|
||||
$query = "UPDATE `".OGP_DB_PREFIX."server_content_workshop`
|
||||
SET ".implode(", ", $set)."
|
||||
WHERE home_id=".(int)$home_id." AND workshop_item_id IN (".implode(",", $escaped_ids).")";
|
||||
return (bool)$db->query($query);
|
||||
}
|
||||
|
||||
function scm_workshop_filter_existing_ids($db, $home_id, array $item_ids)
|
||||
{
|
||||
if (empty($item_ids)) {
|
||||
return array();
|
||||
}
|
||||
$escaped_ids = array();
|
||||
foreach ($item_ids as $item_id) {
|
||||
$escaped_ids[] = "'" . $db->realEscapeSingle((string)$item_id) . "'";
|
||||
}
|
||||
$rows = $db->resultQuery(
|
||||
"SELECT workshop_item_id FROM `".OGP_DB_PREFIX."server_content_workshop`
|
||||
WHERE home_id=".(int)$home_id." AND workshop_item_id IN (".implode(",", $escaped_ids).")"
|
||||
);
|
||||
$allowed = array();
|
||||
if (is_array($rows)) {
|
||||
foreach ((array)$rows as $row) {
|
||||
$allowed[(string)$row['workshop_item_id']] = (string)$row['workshop_item_id'];
|
||||
}
|
||||
}
|
||||
return array_values($allowed);
|
||||
}
|
||||
|
||||
function scm_workshop_write_manifest_and_run($db, array $home_info, $server_xml, $action, array $item_ids, &$error = '')
|
||||
{
|
||||
$error = '';
|
||||
if (empty($item_ids)) {
|
||||
$error = 'No Workshop IDs were selected for this action.';
|
||||
return false;
|
||||
}
|
||||
|
||||
$manifest_path = scm_get_workshop_manifest_path($home_info);
|
||||
if ($manifest_path === false) {
|
||||
$error = 'Manifest path validation failed for this server home.';
|
||||
return false;
|
||||
}
|
||||
|
||||
$script_path = scm_get_workshop_script_path($home_info, $server_xml);
|
||||
$script_path = trim((string)$script_path);
|
||||
if ($script_path === '' || !preg_match('/^[^\\r\\n\\0]+$/', $script_path)) {
|
||||
$error = 'Workshop script path is invalid.';
|
||||
return false;
|
||||
}
|
||||
|
||||
$home_path = rtrim(clean_path((string)$home_info['home_path']), '/');
|
||||
if (!scm_path_is_under_home($home_path, $manifest_path)) {
|
||||
$error = 'Manifest path is outside of the server home.';
|
||||
return false;
|
||||
}
|
||||
|
||||
$manifest_dir = dirname($manifest_path);
|
||||
$manifest = array(
|
||||
'action' => (string)$action,
|
||||
'home_id' => (int)$home_info['home_id'],
|
||||
'home_cfg_id' => (int)$home_info['home_cfg_id'],
|
||||
'workshop_app_id' => scm_extract_workshop_app_id($server_xml),
|
||||
'items' => array_values($item_ids),
|
||||
);
|
||||
$manifest_json = json_encode($manifest);
|
||||
if ($manifest_json === false) {
|
||||
$error = 'Failed to encode workshop manifest JSON.';
|
||||
return false;
|
||||
}
|
||||
|
||||
$remote = new OGPRemoteLibrary(
|
||||
$home_info['agent_ip'],
|
||||
$home_info['agent_port'],
|
||||
$home_info['encryption_key'],
|
||||
$home_info['timeout']
|
||||
);
|
||||
|
||||
$remote->exec("mkdir -p " . escapeshellarg($manifest_dir));
|
||||
if ((int)$remote->remote_writefile($manifest_path, $manifest_json) !== 1) {
|
||||
$error = 'Failed to write workshop manifest to remote server.';
|
||||
return false;
|
||||
}
|
||||
if ((int)$remote->rfile_exists($script_path) !== 1) {
|
||||
$error = 'Configured workshop script not found on agent host: ' . $script_path;
|
||||
return false;
|
||||
}
|
||||
|
||||
$command = "bash " . escapeshellarg($script_path) . " " . escapeshellarg($manifest_path) . " ; echo __GSP_WORKSHOP_EXIT:$?";
|
||||
$output = $remote->exec($command);
|
||||
if (!is_string($output) || $output === '') {
|
||||
$error = 'Workshop script did not return an execution status.';
|
||||
return false;
|
||||
}
|
||||
if (!preg_match('/__GSP_WORKSHOP_EXIT:(\d+)/', $output, $matches)) {
|
||||
$error = 'Workshop script exit marker not found in output.';
|
||||
return false;
|
||||
}
|
||||
$exit_code = (int)$matches[1];
|
||||
if ($exit_code !== 0) {
|
||||
$error = 'Workshop script failed (exit '.$exit_code.'): '.trim($output);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function scm_workshop_handle_action($db, array $home_info, $user_id, $action, $raw_ids, array $selected_ids, &$message, &$is_error)
|
||||
{
|
||||
$message = '';
|
||||
$is_error = true;
|
||||
if (!scm_ensure_workshop_schema($db)) {
|
||||
$message = 'Workshop schema migration failed.';
|
||||
return false;
|
||||
}
|
||||
|
||||
$home_id = (int)$home_info['home_id'];
|
||||
$user_id = (int)$user_id;
|
||||
$server_xml = read_server_config(SERVER_CONFIG_LOCATION . "/" . $home_info['home_cfg_file']);
|
||||
if ($server_xml === false) {
|
||||
$message = 'Unable to read server configuration for workshop action.';
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($action === 'install_new') {
|
||||
$invalid = array();
|
||||
$item_ids = scm_parse_workshop_ids($raw_ids, $invalid);
|
||||
if (!empty($invalid)) {
|
||||
$message = 'Invalid Workshop IDs: ' . implode(', ', $invalid);
|
||||
return false;
|
||||
}
|
||||
if (empty($item_ids)) {
|
||||
$message = 'Enter at least one numeric Workshop ID.';
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($item_ids as $item_id) {
|
||||
$query = "INSERT INTO `".OGP_DB_PREFIX."server_content_workshop`
|
||||
(home_id, home_cfg_id, remote_server_id, workshop_app_id, workshop_item_id, install_state, created_by, created_at, updated_at)
|
||||
VALUES (
|
||||
".$home_id.",
|
||||
".(int)$home_info['home_cfg_id'].",
|
||||
".(int)$home_info['remote_server_id'].",
|
||||
'".$db->realEscapeSingle(scm_extract_workshop_app_id($server_xml))."',
|
||||
'".$db->realEscapeSingle($item_id)."',
|
||||
'selected',
|
||||
".$user_id.",
|
||||
NOW(),
|
||||
NOW()
|
||||
)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
home_cfg_id=VALUES(home_cfg_id),
|
||||
remote_server_id=VALUES(remote_server_id),
|
||||
workshop_app_id=VALUES(workshop_app_id),
|
||||
install_state='selected',
|
||||
last_error=NULL,
|
||||
updated_at=NOW()";
|
||||
$db->query($query);
|
||||
}
|
||||
|
||||
scm_workshop_update_rows_state($db, $home_id, $item_ids, 'installing', null, false, false);
|
||||
$error = '';
|
||||
$ok = scm_workshop_write_manifest_and_run($db, $home_info, $server_xml, 'install', $item_ids, $error);
|
||||
if ($ok) {
|
||||
scm_workshop_update_rows_state($db, $home_id, $item_ids, 'installed', null, true, true);
|
||||
scm_workshop_log_action($db, $home_id, $user_id, "install_new ids=".implode(',', $item_ids)." status=success");
|
||||
$is_error = false;
|
||||
$message = 'Workshop IDs installed successfully.';
|
||||
return true;
|
||||
}
|
||||
scm_workshop_update_rows_state($db, $home_id, $item_ids, 'failed', $error, false, false);
|
||||
scm_workshop_log_action($db, $home_id, $user_id, "install_new ids=".implode(',', $item_ids)." status=failed error=".$error);
|
||||
$message = $error;
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($action === 'update_selected' || $action === 'remove_selected') {
|
||||
$item_ids = scm_workshop_filter_existing_ids($db, $home_id, scm_parse_selected_workshop_ids($selected_ids));
|
||||
if (empty($item_ids)) {
|
||||
$message = 'Select one or more saved Workshop IDs.';
|
||||
return false;
|
||||
}
|
||||
$target_action = ($action === 'remove_selected') ? 'remove' : 'update';
|
||||
scm_workshop_update_rows_state($db, $home_id, $item_ids, 'installing', null, false, false);
|
||||
$error = '';
|
||||
$ok = scm_workshop_write_manifest_and_run($db, $home_info, $server_xml, $target_action, $item_ids, $error);
|
||||
if ($ok) {
|
||||
if ($target_action === 'remove') {
|
||||
scm_workshop_update_rows_state($db, $home_id, $item_ids, 'removed', null, false, true);
|
||||
} else {
|
||||
scm_workshop_update_rows_state($db, $home_id, $item_ids, 'installed', null, false, true);
|
||||
}
|
||||
scm_workshop_log_action($db, $home_id, $user_id, $action." ids=".implode(',', $item_ids)." status=success");
|
||||
$is_error = false;
|
||||
$message = ($target_action === 'remove') ? 'Selected Workshop IDs marked removed.' : 'Selected Workshop IDs updated successfully.';
|
||||
return true;
|
||||
}
|
||||
scm_workshop_update_rows_state($db, $home_id, $item_ids, 'failed', $error, false, false);
|
||||
scm_workshop_log_action($db, $home_id, $user_id, $action." ids=".implode(',', $item_ids)." status=failed error=".$error);
|
||||
$message = $error;
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($action === 'update_all') {
|
||||
$rows = $db->resultQuery(
|
||||
"SELECT workshop_item_id FROM `".OGP_DB_PREFIX."server_content_workshop`
|
||||
WHERE home_id=".$home_id." AND install_state<>'removed'"
|
||||
);
|
||||
$item_ids = array();
|
||||
if (is_array($rows)) {
|
||||
foreach ((array)$rows as $row) {
|
||||
$item_ids[] = (string)$row['workshop_item_id'];
|
||||
}
|
||||
}
|
||||
$item_ids = scm_parse_selected_workshop_ids($item_ids);
|
||||
if (empty($item_ids)) {
|
||||
$message = 'No Workshop IDs are currently saved for this server.';
|
||||
return false;
|
||||
}
|
||||
scm_workshop_update_rows_state($db, $home_id, $item_ids, 'installing', null, false, false);
|
||||
$error = '';
|
||||
$ok = scm_workshop_write_manifest_and_run($db, $home_info, $server_xml, 'update', $item_ids, $error);
|
||||
if ($ok) {
|
||||
scm_workshop_update_rows_state($db, $home_id, $item_ids, 'installed', null, false, true);
|
||||
scm_workshop_log_action($db, $home_id, $user_id, "update_all ids=".implode(',', $item_ids)." status=success");
|
||||
$is_error = false;
|
||||
$message = 'All saved Workshop IDs updated successfully.';
|
||||
return true;
|
||||
}
|
||||
scm_workshop_update_rows_state($db, $home_id, $item_ids, 'failed', $error, false, false);
|
||||
scm_workshop_log_action($db, $home_id, $user_id, "update_all ids=".implode(',', $item_ids)." status=failed error=".$error);
|
||||
$message = $error;
|
||||
return false;
|
||||
}
|
||||
|
||||
$message = 'Invalid workshop action.';
|
||||
return false;
|
||||
}
|
||||
|
||||
148
Panel/modules/addonsmanager/workshop_content.php
Normal file
148
Panel/modules/addonsmanager/workshop_content.php
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
<?php
|
||||
/*
|
||||
*
|
||||
* GSP - Server Content Workshop page (Phase 1)
|
||||
*
|
||||
*/
|
||||
|
||||
require_once(dirname(__FILE__) . '/server_content_helpers.php');
|
||||
require_once(dirname(__FILE__) . '/workshop_action.php');
|
||||
|
||||
function exec_ogp_module() {
|
||||
global $db;
|
||||
|
||||
$user_id = isset($_SESSION['user_id']) ? (int)$_SESSION['user_id'] : 0;
|
||||
$home_id = isset($_REQUEST['home_id']) ? (int)$_REQUEST['home_id'] : 0;
|
||||
$mod_id = isset($_REQUEST['mod_id']) ? (int)$_REQUEST['mod_id'] : 0;
|
||||
$ip = isset($_REQUEST['ip']) ? (string)$_REQUEST['ip'] : '';
|
||||
$port = isset($_REQUEST['port']) ? (string)$_REQUEST['port'] : '';
|
||||
|
||||
if ($home_id <= 0 || $user_id <= 0) {
|
||||
print_failure(get_lang('no_rights'));
|
||||
echo create_back_button("addonsmanager","user_addons");
|
||||
return;
|
||||
}
|
||||
|
||||
$home_info = scm_get_home_for_user($db, $home_id, $user_id);
|
||||
if ($home_info === false) {
|
||||
print_failure(get_lang('no_rights'));
|
||||
echo create_back_button("addonsmanager","user_addons");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!scm_ensure_workshop_schema($db)) {
|
||||
print_failure('Failed to initialize Workshop Content storage.');
|
||||
return;
|
||||
}
|
||||
|
||||
$message = '';
|
||||
$is_error = false;
|
||||
$entered_ids = '';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$posted_home_id = isset($_POST['home_id']) ? (int)$_POST['home_id'] : 0;
|
||||
$csrf_token = isset($_POST['workshop_csrf']) ? (string)$_POST['workshop_csrf'] : '';
|
||||
$entered_ids = isset($_POST['workshop_ids']) ? (string)$_POST['workshop_ids'] : '';
|
||||
$selected_ids = isset($_POST['selected_ids']) ? $_POST['selected_ids'] : array();
|
||||
$action = isset($_POST['workshop_action']) ? (string)$_POST['workshop_action'] : '';
|
||||
|
||||
if ($posted_home_id !== $home_id) {
|
||||
$is_error = true;
|
||||
$message = 'Invalid server context for workshop action.';
|
||||
}
|
||||
elseif (!scm_validate_csrf_token($csrf_token)) {
|
||||
$is_error = true;
|
||||
$message = 'Invalid CSRF token for workshop action.';
|
||||
}
|
||||
else {
|
||||
scm_workshop_handle_action($db, $home_info, $user_id, $action, $entered_ids, (array)$selected_ids, $message, $is_error);
|
||||
}
|
||||
}
|
||||
|
||||
$rows = scm_get_workshop_rows($db, $home_id);
|
||||
$csrf_token = scm_get_csrf_token();
|
||||
|
||||
echo "<h2>Workshop Content: ".scm_h($home_info['home_name'])."</h2>";
|
||||
if ($message !== '') {
|
||||
if ($is_error) {
|
||||
print_failure($message);
|
||||
} else {
|
||||
print_success($message);
|
||||
}
|
||||
}
|
||||
?>
|
||||
<table class='center'>
|
||||
<tr><td align='right'><strong>Server Name:</strong></td><td align='left'><?php echo scm_h($home_info['home_name']); ?></td></tr>
|
||||
<tr><td align='right'><strong>Game Name:</strong></td><td align='left'><?php echo scm_h($home_info['game_name']); ?></td></tr>
|
||||
</table>
|
||||
|
||||
<form method='post' action=''>
|
||||
<input type='hidden' name='m' value='addonsmanager' />
|
||||
<input type='hidden' name='p' value='workshop_content' />
|
||||
<input type='hidden' name='home_id' value='<?php echo (int)$home_id; ?>' />
|
||||
<input type='hidden' name='mod_id' value='<?php echo (int)$mod_id; ?>' />
|
||||
<input type='hidden' name='ip' value='<?php echo scm_h($ip); ?>' />
|
||||
<input type='hidden' name='port' value='<?php echo scm_h($port); ?>' />
|
||||
<input type='hidden' name='workshop_csrf' value='<?php echo scm_h($csrf_token); ?>' />
|
||||
|
||||
<table class='center'>
|
||||
<tr>
|
||||
<td align='right'><strong>Enter Workshop IDs</strong></td>
|
||||
<td align='left'>
|
||||
<input type='text' name='workshop_ids' size='72' value='<?php echo scm_h($entered_ids); ?>' placeholder='1234567890, 9876543210, 555555555' />
|
||||
</td>
|
||||
<td align='left'>
|
||||
<button type='submit' name='workshop_action' value='install_new'>Install New</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<br>
|
||||
<table class='center'>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Workshop ID</th>
|
||||
<th>Title</th>
|
||||
<th>State</th>
|
||||
<th>Last Installed</th>
|
||||
<th>Last Updated</th>
|
||||
<th>Last Error</th>
|
||||
</tr>
|
||||
<?php if (empty($rows)): ?>
|
||||
<tr><td colspan='7' class='info'>No Workshop IDs saved for this server yet.</td></tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ((array)$rows as $row): ?>
|
||||
<tr>
|
||||
<td><input type='checkbox' name='selected_ids[]' value='<?php echo scm_h($row['workshop_item_id']); ?>'></td>
|
||||
<td><?php echo scm_h($row['workshop_item_id']); ?></td>
|
||||
<td><?php echo scm_h($row['title']); ?></td>
|
||||
<td><?php echo scm_h($row['install_state']); ?></td>
|
||||
<td><?php echo scm_h($row['last_installed_at']); ?></td>
|
||||
<td><?php echo scm_h($row['last_updated_at']); ?></td>
|
||||
<td><?php echo scm_h($row['last_error']); ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</table>
|
||||
<br>
|
||||
<table class='center'>
|
||||
<tr>
|
||||
<td><button type='submit' name='workshop_action' value='update_selected'>Update Selected</button></td>
|
||||
<td><button type='submit' name='workshop_action' value='remove_selected'>Remove Selected</button></td>
|
||||
<td><button type='submit' name='workshop_action' value='update_all'>Update All</button></td>
|
||||
</tr>
|
||||
</table>
|
||||
</form>
|
||||
|
||||
<form method='get' action=''>
|
||||
<input type='hidden' name='m' value='addonsmanager' />
|
||||
<input type='hidden' name='p' value='user_addons' />
|
||||
<input type='hidden' name='home_id' value='<?php echo (int)$home_id; ?>' />
|
||||
<input type='hidden' name='mod_id' value='<?php echo (int)$mod_id; ?>' />
|
||||
<input type='hidden' name='ip' value='<?php echo scm_h($ip); ?>' />
|
||||
<input type='hidden' name='port' value='<?php echo scm_h($port); ?>' />
|
||||
<input type='submit' value='Back to Server Content' />
|
||||
</form>
|
||||
<?php
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -77,19 +77,12 @@ function exec_ogp_module()
|
|||
$mod_key = $game_home['mod_key'];
|
||||
$token = $db->getApiToken($_SESSION['user_id']);
|
||||
|
||||
switch ($_POST['action']) {
|
||||
case "stop":
|
||||
$command = "wget -qO- \"${panelURL}/ogp_api.php?gamemanager/stop&token=${token}&ip=${ip}&port=${port}&mod_key=${mod_key}\" --no-check-certificate > /dev/null 2>&1";
|
||||
break;
|
||||
case "start":
|
||||
$command = "wget -qO- \"${panelURL}/ogp_api.php?gamemanager/start&token=${token}&ip=${ip}&port=${port}&mod_key=${mod_key}\" --no-check-certificate > /dev/null 2>&1";
|
||||
break;
|
||||
case "restart":
|
||||
$command = "wget -qO- \"${panelURL}/ogp_api.php?gamemanager/restart&token=${token}&ip=${ip}&port=${port}&mod_key=${mod_key}\" --no-check-certificate > /dev/null 2>&1";
|
||||
break;
|
||||
case "steam_auto_update":
|
||||
$command = "wget -qO- \"${panelURL}/ogp_api.php?gamemanager/update&token=${token}&ip=${ip}&port=${port}&mod_key=${mod_key}&type=steam\" --no-check-certificate > /dev/null 2>&1";
|
||||
break;
|
||||
$command = build_cron_scheduler_command($panelURL, $token, $game_home, $_POST['action']);
|
||||
if($command === false)
|
||||
{
|
||||
print_failure(get_lang('bad_inputs'));
|
||||
$view->refresh('?m=cron&p=cron',2);
|
||||
return;
|
||||
}
|
||||
|
||||
$remote = new OGPRemoteLibrary( $game_home['agent_ip'], $game_home['agent_port'],
|
||||
|
|
|
|||
|
|
@ -47,10 +47,17 @@ function reloadJobs($server_homes, $remote_servers, $getAllJobs = true)
|
|||
list($wget,$wget_args,$url,$wget_nocert,$gt,$devnull,$err2out) = explode(" ", $command, 7);
|
||||
|
||||
parse_str(parse_url(trim($url,'"'), PHP_URL_QUERY), $url_query);
|
||||
|
||||
if(!isset($url_query['ip']) or !isset($url_query['port']))
|
||||
$home_info = false;
|
||||
if(isset($url_query['ip']) && isset($url_query['port']))
|
||||
{
|
||||
$home_info = $db->getGameHomeByIP($url_query['ip'], $url_query['port']);
|
||||
}
|
||||
elseif(isset($url_query['home_id']))
|
||||
{
|
||||
$home_info = $db->getGameHome((int)$url_query['home_id'], true);
|
||||
}
|
||||
if(!$home_info)
|
||||
continue;
|
||||
$home_info = $db->getGameHomeByIP($url_query['ip'], $url_query['port']);
|
||||
if(!$getAllJobs && !hasAccess($home_info))
|
||||
continue;
|
||||
|
||||
|
|
@ -63,6 +70,8 @@ function reloadJobs($server_homes, $remote_servers, $getAllJobs = true)
|
|||
$action = "start";
|
||||
}else if($action == "gamemanager/restart"){
|
||||
$action = "restart";
|
||||
}else if($action == "server_content/run_scheduled_action" && isset($url_query['action']) && $url_query['action'] != ""){
|
||||
$action = $url_query['action'];
|
||||
}
|
||||
|
||||
$jobsArray[$rhost_id][$jobId] = array( 'job' => $job,
|
||||
|
|
@ -76,7 +85,7 @@ function reloadJobs($server_homes, $remote_servers, $getAllJobs = true)
|
|||
'home_id' => $home_info['home_id'],
|
||||
'ip' => $home_info['ip'],
|
||||
'port' => $home_info['port'],
|
||||
'mod_key' => $url_query['mod_key']);
|
||||
'mod_key' => isset($url_query['mod_key']) ? $url_query['mod_key'] : '');
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -99,6 +108,50 @@ function reloadJobs($server_homes, $remote_servers, $getAllJobs = true)
|
|||
return array($jobsArray, $remote_servers_offline);
|
||||
}
|
||||
|
||||
function get_server_content_scheduled_actions() {
|
||||
return array(
|
||||
'server_content_check_updates',
|
||||
'server_content_check_workshop_updates',
|
||||
'server_content_install_updates_if_stopped',
|
||||
'server_content_install_updates_next_restart',
|
||||
'server_content_install_updates_now',
|
||||
'server_content_install_updates_and_restart',
|
||||
'server_content_notify_updates_only',
|
||||
'server_content_update_all',
|
||||
'server_content_validate_files',
|
||||
'server_content_backup_before_update',
|
||||
);
|
||||
}
|
||||
|
||||
function build_cron_scheduler_command($panelURL, $token, $game_home, $action) {
|
||||
$ip = $game_home['ip'];
|
||||
$port = $game_home['port'];
|
||||
$mod_key = isset($game_home['mod_key']) ? $game_home['mod_key'] : '';
|
||||
$home_id = isset($game_home['home_id']) ? (int)$game_home['home_id'] : 0;
|
||||
if(in_array($action, get_server_content_scheduled_actions()))
|
||||
{
|
||||
$options = array('triggered_by' => 'scheduler');
|
||||
if($action == 'server_content_install_updates_and_restart')
|
||||
{
|
||||
$options['safe_restart'] = true;
|
||||
$options['restart_delay_seconds'] = 60;
|
||||
}
|
||||
$options_json = urlencode(json_encode($options));
|
||||
return "wget -qO- \"{$panelURL}/ogp_api.php?server_content/run_scheduled_action&token={$token}&home_id={$home_id}&action={$action}&options={$options_json}\" --no-check-certificate > /dev/null 2>&1";
|
||||
}
|
||||
switch ($action) {
|
||||
case "stop":
|
||||
return "wget -qO- \"{$panelURL}/ogp_api.php?gamemanager/stop&token={$token}&ip={$ip}&port={$port}&mod_key={$mod_key}\" --no-check-certificate > /dev/null 2>&1";
|
||||
case "start":
|
||||
return "wget -qO- \"{$panelURL}/ogp_api.php?gamemanager/start&token={$token}&ip={$ip}&port={$port}&mod_key={$mod_key}\" --no-check-certificate > /dev/null 2>&1";
|
||||
case "restart":
|
||||
return "wget -qO- \"{$panelURL}/ogp_api.php?gamemanager/restart&token={$token}&ip={$ip}&port={$port}&mod_key={$mod_key}\" --no-check-certificate > /dev/null 2>&1";
|
||||
case "steam_auto_update":
|
||||
return "wget -qO- \"{$panelURL}/ogp_api.php?gamemanager/update&token={$token}&ip={$ip}&port={$port}&mod_key={$mod_key}&type=steam\" --no-check-certificate > /dev/null 2>&1";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function updateCronJobTokens($old_token, $token){
|
||||
global $db;
|
||||
$remote_servers = $db->getRemoteServers();
|
||||
|
|
@ -152,6 +205,11 @@ function get_action_selector($action = false, $server_homes = false, $homeid_ip_
|
|||
if( $server_xml->installer == "steamcmd" )
|
||||
$server_actions[] = 'steam_auto_update';
|
||||
}
|
||||
global $db;
|
||||
if($db->isModuleInstalled('addonsmanager'))
|
||||
{
|
||||
$server_actions = array_merge($server_actions, get_server_content_scheduled_actions());
|
||||
}
|
||||
$select_action = '<select name="action" style="width: 100%;">';
|
||||
foreach ((array)$server_actions as $server_action)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -78,19 +78,12 @@ function exec_ogp_module()
|
|||
$mod_key = $game_home['mod_key'];
|
||||
$token = $db->getApiToken($_SESSION['user_id']);
|
||||
|
||||
switch ($_POST['action']) {
|
||||
case "stop":
|
||||
$command = "wget -qO- \"{$panelURL}/ogp_api.php?gamemanager/stop&token={$token}&ip={$ip}&port={$port}&mod_key={$mod_key}\" --no-check-certificate > /dev/null 2>&1";
|
||||
break;
|
||||
case "start":
|
||||
$command = "wget -qO- \"{$panelURL}/ogp_api.php?gamemanager/start&token={$token}&ip={$ip}&port={$port}&mod_key={$mod_key}\" --no-check-certificate > /dev/null 2>&1";
|
||||
break;
|
||||
case "restart":
|
||||
$command = "wget -qO- \"{$panelURL}/ogp_api.php?gamemanager/restart&token={$token}&ip={$ip}&port={$port}&mod_key={$mod_key}\" --no-check-certificate > /dev/null 2>&1";
|
||||
break;
|
||||
case "steam_auto_update":
|
||||
$command = "wget -qO- \"{$panelURL}/ogp_api.php?gamemanager/update&token={$token}&ip={$ip}&port={$port}&mod_key={$mod_key}&type=steam\" --no-check-certificate > /dev/null 2>&1";
|
||||
break;
|
||||
$command = build_cron_scheduler_command($panelURL, $token, $game_home, $_POST['action']);
|
||||
if($command === false)
|
||||
{
|
||||
print_failure(get_lang('bad_inputs'));
|
||||
$view->refresh('?m=cron&p=user_cron',2);
|
||||
return;
|
||||
}
|
||||
|
||||
$job = $_POST['minute']." ".
|
||||
|
|
|
|||
|
|
@ -77,7 +77,9 @@ function exec_ogp_module()
|
|||
"discord_webhook_main" => $_REQUEST['discord_webhook_main'],
|
||||
"discord_webhook_admin" => $_REQUEST['discord_webhook_admin'],
|
||||
// Debug
|
||||
"debug_level" => $_REQUEST['debug_level'] ?? '1'
|
||||
"debug_level" => $_REQUEST['debug_level'] ?? '1',
|
||||
// Server Content
|
||||
"server_content_cache_mode" => $_REQUEST['server_content_cache_mode'] ?? 'disabled',
|
||||
);
|
||||
|
||||
$db->setSettings($settings);
|
||||
|
|
@ -208,8 +210,24 @@ function exec_ogp_module()
|
|||
$ft->add_custom_field('debug_level',
|
||||
create_drop_box_from_array($debug_level_options, 'debug_level', @$row['debug_level'] ?? '1', false));
|
||||
|
||||
// Server Content cache mode
|
||||
// Controls whether the agent may copy cacheable content from other servers
|
||||
// or a shared cache instead of always re-downloading from the source.
|
||||
// Default: disabled (safest – no copying of any content between servers).
|
||||
// See addonsmanager/SERVER_CONTENT_ROADMAP.md for full description.
|
||||
$cache_mode_options = array(
|
||||
'disabled' => 'Disabled (always install from source — default)',
|
||||
'search_existing_servers' => 'Search existing servers (copy cacheable content from local game-server folders)',
|
||||
'shared_cache' => 'Shared cache (store and reuse cacheable content in a shared cache folder)',
|
||||
'shared_cache_and_search' => 'Shared cache + search existing servers',
|
||||
);
|
||||
$current_cache_mode = isset($row['server_content_cache_mode']) && array_key_exists($row['server_content_cache_mode'], $cache_mode_options)
|
||||
? $row['server_content_cache_mode'] : 'disabled';
|
||||
$ft->add_custom_field('server_content_cache_mode',
|
||||
create_drop_box_from_array($cache_mode_options, 'server_content_cache_mode', $current_cache_mode, false));
|
||||
|
||||
// Add option to reset game server order to default
|
||||
$ft->add_field('checkbox','reset_game_server_order','0');
|
||||
$ft->add_field('checkbox','reset_game_server_order','0');
|
||||
|
||||
$ft->end_table();
|
||||
$ft->add_button("submit","update_settings",get_lang('update_settings'));
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
// Module general information
|
||||
$module_title = "Update";
|
||||
$module_version = "1.0";
|
||||
$db_version = 3; // avoid 'duplicate table' error message.
|
||||
$db_version = 4;
|
||||
$module_required = TRUE;
|
||||
$module_menus = array(
|
||||
array( 'subpage' => '', 'name'=>'Update', 'group'=>'admin' )
|
||||
|
|
@ -62,4 +62,14 @@ $install_queries[3] = array(
|
|||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;"
|
||||
);
|
||||
$install_queries[4] = array(
|
||||
"CREATE TABLE IF NOT EXISTS `".OGP_DB_PREFIX."update_patches` (
|
||||
`patch_id` varchar(191) NOT NULL,
|
||||
`status` varchar(32) NOT NULL,
|
||||
`details` text DEFAULT NULL,
|
||||
`updater_version` varchar(80) DEFAULT NULL,
|
||||
`applied_at` datetime NOT NULL,
|
||||
PRIMARY KEY (`patch_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;"
|
||||
);
|
||||
?>
|
||||
|
|
|
|||
183
Panel/modules/update/patch_manager.php
Normal file
183
Panel/modules/update/patch_manager.php
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
<?php
|
||||
/*
|
||||
*
|
||||
* OGP - Open Game Panel
|
||||
* Copyright (C) 2008 - 2018 The OGP Development Team
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
*/
|
||||
|
||||
function gsp_patch_state_fallback_file()
|
||||
{
|
||||
if (defined('GSP_ROOT_DIR')) {
|
||||
return GSP_ROOT_DIR . '/logs/update_patch_state.json';
|
||||
}
|
||||
if (defined('GSP_PANEL_DIR')) {
|
||||
return dirname(GSP_PANEL_DIR) . '/logs/update_patch_state.json';
|
||||
}
|
||||
return dirname(__FILE__) . '/../../logs/update_patch_state.json';
|
||||
}
|
||||
|
||||
function gsp_patch_state_load_local($state_file)
|
||||
{
|
||||
if (!file_exists($state_file)) {
|
||||
return [];
|
||||
}
|
||||
$payload = json_decode(@file_get_contents($state_file), true);
|
||||
if (!is_array($payload) || !isset($payload['patches']) || !is_array($payload['patches'])) {
|
||||
return [];
|
||||
}
|
||||
return $payload['patches'];
|
||||
}
|
||||
|
||||
function gsp_patch_state_save_local($state_file, array $patches)
|
||||
{
|
||||
$dir = dirname($state_file);
|
||||
if (!is_dir($dir)) {
|
||||
@mkdir($dir, 0755, true);
|
||||
}
|
||||
$payload = [
|
||||
'updated_at' => date('Y-m-d H:i:s'),
|
||||
'patches' => $patches,
|
||||
];
|
||||
@file_put_contents($state_file, json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
|
||||
}
|
||||
|
||||
function gsp_patch_load_definitions($patch_dir)
|
||||
{
|
||||
$patches = [];
|
||||
if (!is_dir($patch_dir)) {
|
||||
return $patches;
|
||||
}
|
||||
$files = glob(rtrim($patch_dir, '/') . '/*.php') ?: [];
|
||||
sort($files, SORT_NATURAL);
|
||||
foreach ($files as $file) {
|
||||
$def = include $file;
|
||||
if (!is_array($def) || empty($def['id']) || empty($def['runner'])) {
|
||||
continue;
|
||||
}
|
||||
$def['file'] = $file;
|
||||
$patches[] = $def;
|
||||
}
|
||||
return $patches;
|
||||
}
|
||||
|
||||
function gsp_patch_get_applied_map($db, $state_file)
|
||||
{
|
||||
$map = [];
|
||||
if (isset($db) && is_object($db)) {
|
||||
$rows = $db->resultQuery("SELECT patch_id, status, details, applied_at FROM `OGP_DB_PREFIXupdate_patches`");
|
||||
if (is_array($rows)) {
|
||||
foreach ($rows as $row) {
|
||||
$map[$row['patch_id']] = [
|
||||
'status' => $row['status'],
|
||||
'details' => $row['details'],
|
||||
'applied_at' => $row['applied_at'],
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach (gsp_patch_state_load_local($state_file) as $id => $row) {
|
||||
if (!isset($map[$id])) {
|
||||
$map[$id] = $row;
|
||||
}
|
||||
}
|
||||
return $map;
|
||||
}
|
||||
|
||||
function gsp_patch_record($db, $state_file, $patch_id, $status, $details, $updater_version)
|
||||
{
|
||||
$patch_id = (string)$patch_id;
|
||||
$status = (string)$status;
|
||||
$details = (string)$details;
|
||||
$updater_version = (string)$updater_version;
|
||||
$applied_at = date('Y-m-d H:i:s');
|
||||
|
||||
if (isset($db) && is_object($db)) {
|
||||
$pid = $db->real_escape_string($patch_id);
|
||||
$st = $db->real_escape_string($status);
|
||||
$dt = $db->real_escape_string($details);
|
||||
$uv = $db->real_escape_string($updater_version);
|
||||
$at = $db->real_escape_string($applied_at);
|
||||
$db->query(
|
||||
"INSERT INTO `OGP_DB_PREFIXupdate_patches` (patch_id, status, details, updater_version, applied_at) "
|
||||
. "VALUES ('{$pid}','{$st}','{$dt}','{$uv}','{$at}') "
|
||||
. "ON DUPLICATE KEY UPDATE status=VALUES(status), details=VALUES(details), updater_version=VALUES(updater_version), applied_at=VALUES(applied_at)"
|
||||
);
|
||||
}
|
||||
|
||||
$state = gsp_patch_state_load_local($state_file);
|
||||
$state[$patch_id] = [
|
||||
'status' => $status,
|
||||
'details' => $details,
|
||||
'applied_at' => $applied_at,
|
||||
'updater_version' => $updater_version,
|
||||
];
|
||||
gsp_patch_state_save_local($state_file, $state);
|
||||
}
|
||||
|
||||
function gsp_patch_run_all($db, $patch_dir, callable $logger, $updater_version)
|
||||
{
|
||||
$state_file = gsp_patch_state_fallback_file();
|
||||
$definitions = gsp_patch_load_definitions($patch_dir);
|
||||
$applied = gsp_patch_get_applied_map($db, $state_file);
|
||||
$result = [
|
||||
'success' => true,
|
||||
'patches_available' => count($definitions),
|
||||
'applied' => [],
|
||||
'skipped' => [],
|
||||
'failed_patch' => null,
|
||||
'error' => null,
|
||||
];
|
||||
|
||||
foreach ($definitions as $patch) {
|
||||
$id = (string)$patch['id'];
|
||||
$title = !empty($patch['title']) ? (string)$patch['title'] : $id;
|
||||
if (isset($applied[$id]) && $applied[$id]['status'] === 'applied') {
|
||||
$result['skipped'][] = $id;
|
||||
$logger("Patch {$id} ({$title}) already applied; skipping.");
|
||||
continue;
|
||||
}
|
||||
|
||||
$runner = $patch['runner'];
|
||||
if (!is_callable($runner)) {
|
||||
$msg = "Patch {$id} runner is not callable.";
|
||||
gsp_patch_record($db, $state_file, $id, 'failed', $msg, $updater_version);
|
||||
$result['success'] = false;
|
||||
$result['failed_patch'] = $id;
|
||||
$result['error'] = $msg;
|
||||
$logger($msg);
|
||||
break;
|
||||
}
|
||||
|
||||
$logger("Running patch {$id} ({$title}).");
|
||||
$run = call_user_func($runner, [
|
||||
'root_dir' => defined('GSP_ROOT_DIR') ? GSP_ROOT_DIR : null,
|
||||
'panel_dir' => defined('GSP_PANEL_DIR') ? GSP_PANEL_DIR : null,
|
||||
'website_dir' => defined('GSP_WEBSITE_DIR') ? GSP_WEBSITE_DIR : null,
|
||||
]);
|
||||
if (!is_array($run)) {
|
||||
$run = ['success' => false, 'details' => 'Patch runner returned invalid result.'];
|
||||
}
|
||||
$ok = !empty($run['success']);
|
||||
$details = !empty($run['details']) ? (string)$run['details'] : ($ok ? 'Applied.' : 'Failed.');
|
||||
gsp_patch_record($db, $state_file, $id, $ok ? 'applied' : 'failed', $details, $updater_version);
|
||||
|
||||
if ($ok) {
|
||||
$result['applied'][] = $id;
|
||||
$logger("Patch {$id} applied: {$details}");
|
||||
} else {
|
||||
$result['success'] = false;
|
||||
$result['failed_patch'] = $id;
|
||||
$result['error'] = $details;
|
||||
$logger("Patch {$id} failed: {$details}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
36
Panel/modules/update/patches/001_layout_bootstrap.php
Normal file
36
Panel/modules/update/patches/001_layout_bootstrap.php
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
if (!function_exists('gsp_patch_001_layout_bootstrap')) {
|
||||
function gsp_patch_001_layout_bootstrap($context)
|
||||
{
|
||||
$created = [];
|
||||
$targets = [
|
||||
!empty($context['root_dir']) ? $context['root_dir'] . '/logs' : null,
|
||||
!empty($context['root_dir']) ? $context['root_dir'] . '/backups' : null,
|
||||
!empty($context['root_dir']) ? $context['root_dir'] . '/examples' : null,
|
||||
!empty($context['panel_dir']) ? $context['panel_dir'] : null,
|
||||
!empty($context['website_dir']) ? $context['website_dir'] : null,
|
||||
];
|
||||
foreach ($targets as $dir) {
|
||||
if (empty($dir)) {
|
||||
continue;
|
||||
}
|
||||
if (!is_dir($dir)) {
|
||||
if (!@mkdir($dir, 0755, true) && !is_dir($dir)) {
|
||||
return ['success' => false, 'details' => 'Failed to create directory: ' . $dir];
|
||||
}
|
||||
$created[] = $dir;
|
||||
}
|
||||
}
|
||||
$details = empty($created)
|
||||
? 'Layout already in expected state.'
|
||||
: 'Created directories: ' . implode(', ', $created);
|
||||
return ['success' => true, 'details' => $details];
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => '001_layout_bootstrap',
|
||||
'title' => 'Ensure baseline GSP root directories exist',
|
||||
'runner' => 'gsp_patch_001_layout_bootstrap',
|
||||
];
|
||||
|
|
@ -51,6 +51,9 @@ ogp_api.php?addonsmanager/install (POST/GET {token}{ip}{port}{mod_key}{addon_i
|
|||
|
||||
______________ Steam Workshop
|
||||
ogp_api.php?steam_workshop/install (POST/GET {token}{ip}{port}{mods_list})
|
||||
|
||||
______________ Server Content
|
||||
ogp_api.php?server_content/run_scheduled_action (POST/GET {token}{home_id}{action}{options})
|
||||
*
|
||||
______________ Panel Setting
|
||||
ogp_api.php?setting/get (POST/GET {token}{setting_name})
|
||||
|
|
@ -1474,6 +1477,44 @@ function api_addonsmanager()
|
|||
return array("status" => $status, "message" => $message);
|
||||
}
|
||||
|
||||
function api_server_content()
|
||||
{
|
||||
global $request, $db;
|
||||
if($db->isModuleInstalled('addonsmanager') === FALSE)
|
||||
return array("status" => '349', "message" => "This function is not available because the module is not installed.");
|
||||
|
||||
if($request[0] == "run_scheduled_action")
|
||||
{
|
||||
require_once(MODULES.'addonsmanager/server_content_actions.php');
|
||||
$home_id = (int)$_POST['home_id'];
|
||||
$action = isset($_POST['action']) ? trim((string)$_POST['action']) : '';
|
||||
$options = array();
|
||||
if(isset($_POST['options']))
|
||||
{
|
||||
$decoded_options = json_decode((string)$_POST['options'], true);
|
||||
if(is_array($decoded_options))
|
||||
$options = $decoded_options;
|
||||
}
|
||||
$passthrough_keys = array('triggered_by', 'cron_id', 'safe_restart', 'restart_delay_seconds', 'check_only', 'notify_only');
|
||||
foreach ((array)$passthrough_keys as $key)
|
||||
{
|
||||
if(isset($_POST[$key]))
|
||||
$options[$key] = $_POST[$key];
|
||||
}
|
||||
|
||||
$result = server_content_run_scheduled_action($home_id, $action, $options);
|
||||
$status = isset($result['status']) ? $result['status'] : 'failed';
|
||||
$http_status = ($status === 'failed') ? "500" : "200";
|
||||
return array(
|
||||
"status" => $http_status,
|
||||
"message" => isset($result['message']) ? $result['message'] : 'Server content scheduled action completed.',
|
||||
"result" => $result
|
||||
);
|
||||
}
|
||||
|
||||
return array("status" => "400", "message" => "BAD REQUEST");
|
||||
}
|
||||
|
||||
function api_steam_workshop()
|
||||
{
|
||||
global $request, $db, $user_info, $settings;
|
||||
|
|
|
|||
168
Panel/protocol/PROTOCOL_UPGRADE_REVIEW.md
Normal file
168
Panel/protocol/PROTOCOL_UPGRADE_REVIEW.md
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
# Protocol Upgrade Review (Phase 1, safe path)
|
||||
|
||||
## Scope reviewed
|
||||
- `Panel/protocol/lgsl/`
|
||||
- `Panel/protocol/GameQ/`
|
||||
- `Panel/modules/gamemanager/server_monitor.php`
|
||||
- `Panel/modules/gamemanager/ref_servermonitor.php`
|
||||
- `Panel/modules/gamemanager/start_server.php`
|
||||
- `Panel/modules/gamemanager/restart_server.php`
|
||||
- `Panel/modules/gamemanager/mini_start.php`
|
||||
- `Panel/modules/dashboard/query_ref.php`
|
||||
- `Panel/modules/config_games/server_configs/*.xml`
|
||||
|
||||
## Current protocol folders
|
||||
- `Panel/protocol/lgsl/`
|
||||
- Legacy LGSL implementation and protocol map (`lgsl_protocol.php`).
|
||||
- Monitor helper (`LGSLMonitor.php`).
|
||||
- Player list rendering helper (`functions.php`).
|
||||
- `Panel/protocol/GameQ/`
|
||||
- Modern namespaced GameQ implementation with PSR-4 autoloader.
|
||||
- Monitor helper (`GameQMonitor.php`) + player list rendering helper (`functions.php`).
|
||||
- Very large protocol coverage in `Protocols/`.
|
||||
- Also contains legacy-looking `gameq/` subtree alongside modern files (technical debt signal).
|
||||
|
||||
## How LGSL is currently called
|
||||
- Monitor refresh path:
|
||||
- `gamemanager/ref_servermonitor.php` loads `protocol/lgsl/LGSLMonitor.php` when XML protocol is `lgsl`.
|
||||
- `LGSLMonitor.php` calls `lgsl_query_live(...)`, handles panel query cache (`getServerStatusCache`/`saveServerStatusCache`), and sets `$status/$map/$players/$player_list`.
|
||||
- Start/restart detection path:
|
||||
- `gamemanager/start_server.php` and `restart_server.php` call `lgsl_query_live(..., "sa")` after process launch to decide if server is considered running.
|
||||
- Quick checks:
|
||||
- `gamemanager/mini_start.php` and other helpers call `lgsl_port_conversion(...)` and `lgsl_query_live(...)`.
|
||||
- Connection links:
|
||||
- `server_monitor.php` and dsi/lgsl image flows use `lgsl_port_conversion(...)` and `lgsl_software_link(...)`.
|
||||
|
||||
## GameQ status (usable vs incomplete)
|
||||
- Present and actively used in monitor/start/restart flows (`GameQMonitor.php`, `start_server.php`, `restart_server.php`, `dashboard/query_ref.php`).
|
||||
- Usable for configured XML entries (`protocol=gameq` + `gameq_query_name`).
|
||||
- Inconsistencies/risks:
|
||||
- Mixed API usage exists in module code (`process()` and `requestData()` patterns).
|
||||
- Legacy and modern GameQ structures coexist under `Panel/protocol/GameQ/`.
|
||||
- GameQ is integrated, but implementation consistency is incomplete and should be normalized before broad protocol migration.
|
||||
|
||||
## Files that map game configs to protocol names
|
||||
- Primary source of protocol selection:
|
||||
- `Panel/modules/config_games/server_configs/*.xml` fields:
|
||||
- `<protocol>`
|
||||
- `<lgsl_query_name>`
|
||||
- `<gameq_query_name>`
|
||||
- Supporting editor/schema surfaces:
|
||||
- `Panel/modules/config_games/config_servers.php` (schema order includes protocol tags).
|
||||
- `Panel/modules/config_games/xml_tag_descriptions.php` (documents protocol fields).
|
||||
- `Panel/modules/config_games/xml_config_creator.php` (protocol selector and query-name population).
|
||||
|
||||
## Where player list parsing happens
|
||||
- LGSL:
|
||||
- Query payload from `lgsl_query_live(...)` in `lgsl_protocol.php`.
|
||||
- Player table rendering in `protocol/lgsl/functions.php::print_player_list(...)`.
|
||||
- GameQ:
|
||||
- Query payload from `GameQMonitor.php`.
|
||||
- Player table rendering and field normalization in `protocol/GameQ/functions.php::print_player_list_gameq(...)`.
|
||||
|
||||
## Where map/player/server status data is returned
|
||||
- LGSL live payload shape: `b/s/e/p/t` arrays from `lgsl_query_live(...)`:
|
||||
- status: `b.status`
|
||||
- map/name/player counts/password: `s.*`
|
||||
- player list: `p`
|
||||
- extras (including some bot fields): `e`
|
||||
- GameQ normalized payload (after `normalise` filter):
|
||||
- status: `server.gq_online`
|
||||
- map: `server.gq_mapname`
|
||||
- player counts: `server.gq_numplayers`, `server.gq_maxplayers`
|
||||
- player list: `server.players`
|
||||
|
||||
## Known problems (Phase 1 findings)
|
||||
- Query invocation is duplicated in multiple modules (monitor/start/restart/dashboard/image modules), increasing drift risk.
|
||||
- Start detection currently combines process + query checks but does not explicitly represent a `starting` state in monitor output.
|
||||
- LGSL uses hard exits for invalid parameters (`lgsl_query_live`), which is risky for direct callers without guard logic.
|
||||
- GameQ integration style is not fully standardized across all call sites.
|
||||
- Existing cache model is spread across current status cache and module-specific image caches, without one normalized query cache contract.
|
||||
|
||||
## New wrapper prepared in this phase
|
||||
- Added `Panel/protocol/gsp_query.php`.
|
||||
- Introduces `gsp_query_server($server_info, $options = [])` normalized result contract.
|
||||
- Keeps default provider on LGSL (`lgsl_legacy`) for safety.
|
||||
- Adds provider concept placeholders (no broad provider switch in this phase):
|
||||
- `lgsl_legacy`
|
||||
- `gameq`
|
||||
- `xpaw_source_query`
|
||||
- `minecraft_query`
|
||||
- `custom_script`
|
||||
|
||||
## Proposed query cache table (planning only, no migration applied)
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS OGP_DB_PREFIXserver_query_cache (
|
||||
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
home_id INT NOT NULL,
|
||||
ip VARCHAR(64) NOT NULL,
|
||||
port INT NOT NULL,
|
||||
query_port INT NULL,
|
||||
protocol VARCHAR(64) NULL,
|
||||
provider VARCHAR(64) NULL,
|
||||
online TINYINT(1) NOT NULL DEFAULT 0,
|
||||
server_name VARCHAR(255) NULL,
|
||||
map_name VARCHAR(128) NULL,
|
||||
players INT NULL,
|
||||
max_players INT NULL,
|
||||
bots INT NULL,
|
||||
passworded TINYINT(1) NULL,
|
||||
latency_ms INT NULL,
|
||||
player_list_json MEDIUMTEXT NULL,
|
||||
raw_json MEDIUMTEXT NULL,
|
||||
last_query_at DATETIME NULL,
|
||||
last_success_at DATETIME NULL,
|
||||
last_error TEXT NULL,
|
||||
UNIQUE KEY uniq_home_query (home_id),
|
||||
KEY idx_last_query_at (last_query_at),
|
||||
KEY idx_online (online)
|
||||
);
|
||||
```
|
||||
|
||||
Cache TTL target: **60 seconds** default.
|
||||
|
||||
## Server start detection improvement plan (no behavior change yet)
|
||||
- After start command, mark status as **starting**.
|
||||
- Poll on a short interval until timeout:
|
||||
1. Check agent process state.
|
||||
2. Check network port open.
|
||||
3. Check query response (if protocol supported).
|
||||
- Final states:
|
||||
- Process + query OK: **Online**
|
||||
- Process OK but query unavailable: **Running, query unavailable**
|
||||
- Process missing at timeout: **Failed to start**
|
||||
|
||||
## Game Manager integration plan (`server_monitor.php`)
|
||||
Future `Live Server Status` panel should include:
|
||||
- State: Online / Offline / Starting / Running query unavailable
|
||||
- Server name
|
||||
- Current map
|
||||
- Players / max players
|
||||
- Player list
|
||||
- Query latency
|
||||
- Last query time
|
||||
- Banner preview
|
||||
- “Get banner code” action
|
||||
|
||||
## Admin query debug/test page plan
|
||||
Future page: `Panel/protocol/query_test.php` (admin-only)
|
||||
- Inputs: IP, port, query port, protocol, provider, timeout
|
||||
- Outputs: normalized result, raw result, errors
|
||||
- Security:
|
||||
- admin-only access gate
|
||||
- CSRF for submit actions
|
||||
- request limits / timeout caps
|
||||
- no anonymous/public proxy behavior
|
||||
|
||||
## Recommended next phase
|
||||
1. Switch one low-risk monitor path to read `gsp_query_server()` output in parallel with existing behavior (feature-flag style).
|
||||
2. Standardize GameQ call style (single API usage pattern) and document supported protocol mappings.
|
||||
3. Add normalized cache write/read adapter (without removing existing caches yet).
|
||||
4. Add explicit start-state model and timeout policy constants.
|
||||
5. Begin unified banner module implementation against normalized cache payloads.
|
||||
|
||||
## Safety statement
|
||||
- No protocol engines were removed.
|
||||
- LGSL remains in place.
|
||||
- GameQ remains in place.
|
||||
- Existing server monitor behavior remains intact in this phase.
|
||||
157
Panel/protocol/gsp_query.php
Normal file
157
Panel/protocol/gsp_query.php
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
<?php
|
||||
/*
|
||||
* GSP query wrapper (Phase 1 scaffolding)
|
||||
*
|
||||
* Normalizes server query results while keeping LGSL as the default provider.
|
||||
* This file intentionally avoids changing existing monitor paths in this phase.
|
||||
*/
|
||||
|
||||
if (!function_exists('gsp_query_provider_names')) {
|
||||
function gsp_query_provider_names()
|
||||
{
|
||||
return array(
|
||||
// LGSL remains default for legacy/older game coverage.
|
||||
'lgsl_legacy',
|
||||
// TODO: Use for games where current GameQ support is proven reliable.
|
||||
'gameq',
|
||||
// TODO: Prefer for modern Source/Steam query games in a later phase.
|
||||
'xpaw_source_query',
|
||||
// TODO: Prefer dedicated Minecraft query handling in a later phase.
|
||||
'minecraft_query',
|
||||
// TODO: Allow custom scripts for unusual game protocols.
|
||||
'custom_script',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('gsp_query_default_result')) {
|
||||
function gsp_query_default_result()
|
||||
{
|
||||
return array(
|
||||
'success' => false,
|
||||
'online' => false,
|
||||
'provider' => 'lgsl_legacy',
|
||||
'protocol' => '',
|
||||
'game' => '',
|
||||
'server_name' => '',
|
||||
'map' => '',
|
||||
'players' => 0,
|
||||
'max_players' => 0,
|
||||
'bots' => 0,
|
||||
'passworded' => false,
|
||||
'latency_ms' => null,
|
||||
'address' => '',
|
||||
'port' => 0,
|
||||
'query_port' => 0,
|
||||
'player_list' => array(),
|
||||
'raw' => array(),
|
||||
'error' => '',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('gsp_query_normalize_player_list')) {
|
||||
function gsp_query_normalize_player_list($players)
|
||||
{
|
||||
$normalized = array();
|
||||
foreach ((array)$players as $player) {
|
||||
$normalized[] = array(
|
||||
'name' => isset($player['name']) ? (string)$player['name'] : '',
|
||||
'score' => isset($player['score']) ? (int)$player['score'] : 0,
|
||||
'time' => isset($player['time']) ? $player['time'] : '',
|
||||
'ping' => isset($player['ping']) ? (int)$player['ping'] : 0,
|
||||
'raw' => (array)$player,
|
||||
);
|
||||
}
|
||||
return $normalized;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('gsp_query_server')) {
|
||||
function gsp_query_server($server_info, $options = array())
|
||||
{
|
||||
$result = gsp_query_default_result();
|
||||
$server = (array)$server_info;
|
||||
$options = (array)$options;
|
||||
|
||||
$provider = isset($options['provider']) ? (string)$options['provider'] : (isset($server['query_provider']) ? (string)$server['query_provider'] : 'lgsl_legacy');
|
||||
$result['provider'] = $provider;
|
||||
|
||||
$ip = isset($server['ip']) ? trim((string)$server['ip']) : '';
|
||||
$port = isset($server['port']) ? (int)$server['port'] : 0;
|
||||
$query_ip = $ip;
|
||||
if (!empty($server['use_nat']) && !empty($server['agent_ip'])) {
|
||||
$query_ip = trim((string)$server['agent_ip']);
|
||||
}
|
||||
|
||||
$result['address'] = ($ip !== '' && $port > 0) ? $ip . ':' . $port : '';
|
||||
$result['port'] = $port;
|
||||
|
||||
if ($provider !== 'lgsl_legacy') {
|
||||
$result['error'] = "Query provider not implemented yet: {$provider}";
|
||||
return $result;
|
||||
}
|
||||
|
||||
$query_name = '';
|
||||
if (isset($server['lgsl_query_name'])) {
|
||||
$query_name = (string)$server['lgsl_query_name'];
|
||||
} elseif (isset($server['query_name'])) {
|
||||
$query_name = (string)$server['query_name'];
|
||||
}
|
||||
$query_name = trim($query_name);
|
||||
$result['protocol'] = $query_name;
|
||||
|
||||
if ($query_name === '') {
|
||||
$result['error'] = 'Missing LGSL query name.';
|
||||
return $result;
|
||||
}
|
||||
|
||||
if ($query_ip === '' || preg_match("/[^0-9a-z\\.\\-\\[\\]\\:]/i", $query_ip)) {
|
||||
$result['error'] = 'Invalid query IP/hostname.';
|
||||
return $result;
|
||||
}
|
||||
|
||||
if ($port <= 0) {
|
||||
$result['error'] = 'Invalid server port.';
|
||||
return $result;
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/lgsl/lgsl_protocol.php';
|
||||
$protocols = lgsl_protocol_list();
|
||||
if (!isset($protocols[$query_name])) {
|
||||
$result['error'] = "Unsupported LGSL protocol type: {$query_name}";
|
||||
return $result;
|
||||
}
|
||||
|
||||
list($c_port, $default_q_port, $s_port) = lgsl_port_conversion($query_name, $port, "", "");
|
||||
$q_port = isset($server['query_port']) && (int)$server['query_port'] > 0 ? (int)$server['query_port'] : (int)$default_q_port;
|
||||
$result['query_port'] = $q_port;
|
||||
|
||||
if ($q_port <= 0) {
|
||||
$result['error'] = 'Invalid query port for LGSL query.';
|
||||
return $result;
|
||||
}
|
||||
|
||||
$raw = lgsl_query_live($query_name, $query_ip, $c_port, $q_port, $s_port, "sep");
|
||||
if (!is_array($raw) || !isset($raw['b']) || !isset($raw['b']['status'])) {
|
||||
$result['error'] = 'LGSL query returned an invalid payload.';
|
||||
return $result;
|
||||
}
|
||||
|
||||
$result['raw'] = $raw;
|
||||
$result['success'] = true;
|
||||
$result['online'] = ((string)$raw['b']['status'] === '1' || (int)$raw['b']['status'] === 1);
|
||||
$result['game'] = isset($raw['s']['game']) ? (string)$raw['s']['game'] : '';
|
||||
$result['server_name'] = isset($raw['s']['name']) ? (string)$raw['s']['name'] : '';
|
||||
$result['map'] = isset($raw['s']['map']) ? (string)$raw['s']['map'] : '';
|
||||
$result['players'] = isset($raw['s']['players']) ? (int)$raw['s']['players'] : 0;
|
||||
$result['max_players'] = isset($raw['s']['playersmax']) ? (int)$raw['s']['playersmax'] : 0;
|
||||
$result['bots'] = isset($raw['e']['bots']) ? (int)$raw['e']['bots'] : 0;
|
||||
$result['passworded'] = !empty($raw['s']['password']);
|
||||
$result['latency_ms'] = isset($raw['t']['ping']) ? (int)$raw['t']['ping'] : null;
|
||||
$result['player_list'] = isset($raw['p']) ? gsp_query_normalize_player_list($raw['p']) : array();
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
?>
|
||||
Loading…
Add table
Add a link
Reference in a new issue