Merge pull request #142 from GameServerPanel/copilot/gsp-automatic-server-install-trigger
Auto-trigger Game Monitor install path on provisioning + monthly-only billing pricing + LiteFM PHP 8.3 hardening
This commit is contained in:
commit
e0fdb8cdd2
13 changed files with 215 additions and 356 deletions
|
|
@ -1,6 +1,8 @@
|
|||
# Changelog
|
||||
|
||||
## 2026-05-08
|
||||
- **Auto-install trigger + monthly-only billing pricing:** Refactored Game Monitor update/install into a shared callable (`modules/gamemanager/update_actions.php`) reused by billing provisioning so new paid/free/admin-created homes auto-trigger the same install/update path used by `m=gamemanager&p=update&update=refresh` without manual clicks. Billing now treats monthly pricing (`price_monthly`) as canonical across admin service config, add-to-cart, free checkout, PayPal capture, and provisioning end-date math (31-day months), while preserving legacy daily/yearly columns for backward compatibility.
|
||||
- **LiteFM PHP 8.3 compatibility and install-pending UX:** Removed deprecated `${var}` interpolation usage, guarded missing `fm_cwd_*` session keys and `dirname()` null paths, and replaced directory-not-found warning output with a clear message when server files are not installed yet.
|
||||
- **Provisioning + billing admin UX reliability pass:** Hardened automatic server provisioning to reserve ports from `arrange_ports` (exact `home_cfg_id`, then `home_cfg_id=0` fallback), prevent duplicate `home_ip_ports` assignment, keep order/invoice/home linkage intact even when install is pending, and apply safe default mod resolution so base installs are not blocked by missing explicit mod choices. Refreshed billing admin service management with row-level save actions, sortable columns (including Game Name enabled-first toggle), clearer save feedback, cleaner unstable-update caution styling, login theme polish, and updated storefront timestamp footer metadata.
|
||||
- **Steam Workshop reliability + UI simplification:** Removed customer CLI/update scripting instructions from Workshop user flow, reduced per-server behavior options to supported modes only, switched remaining Steam Workshop SQL references to prefix helpers (no `OGP_DB_PREFIX` strings), hardened queued-update agent processing (`queued → updating → installed/failed`) with clearer error persistence, and refreshed monitor/support documentation links to open game-specific docs (fallback to docs index) in a new tab.
|
||||
- **Billing docs routing refresh:** Updated docs browser links/icons to root-relative storefront paths (`/docs.php`, `/docs/...`) and removed stale hardcoded panel host guidance from getting-started documentation.
|
||||
|
|
|
|||
|
|
@ -9,3 +9,4 @@
|
|||
- Add a storefront visual-regression check at 375px and 430px breakpoints covering login, order, and cart pages to prevent mobile overflow regressions.
|
||||
- Complete a full pass over all `modules/billing/docs/*` game guides to standardize OS/Workshop/RCON capability statements against current XML-backed server support.
|
||||
- Add an automated billing provisioning integration test fixture that verifies arrange_ports exact/fallback allocation, duplicate-port protection, and home_id linkage after paid/free checkout.
|
||||
- Add a billing UI badge/filter that distinguishes "pending install" vs "installed" states directly in customer/server order views.
|
||||
|
|
|
|||
|
|
@ -29,19 +29,7 @@ function billing_generate_password(int $bytes = 12): string
|
|||
|
||||
function billing_normalize_duration(string $duration): array
|
||||
{
|
||||
$duration = strtolower(trim($duration));
|
||||
switch ($duration) {
|
||||
case 'day':
|
||||
case 'daily':
|
||||
return ['invoice_duration' => 'day', 'rate_type' => 'daily', 'days' => 1];
|
||||
case 'year':
|
||||
case 'yearly':
|
||||
return ['invoice_duration' => 'year', 'rate_type' => 'yearly', 'days' => 365];
|
||||
case 'month':
|
||||
case 'monthly':
|
||||
default:
|
||||
return ['invoice_duration' => 'month', 'rate_type' => 'monthly', 'days' => 31];
|
||||
}
|
||||
return ['invoice_duration' => 'month', 'rate_type' => 'monthly', 'days' => 31];
|
||||
}
|
||||
|
||||
function billing_money_to_cents(float $amount): int
|
||||
|
|
@ -60,23 +48,17 @@ function billing_rate_from_service(mysqli $db, string $table_prefix, int $servic
|
|||
return 0.0;
|
||||
}
|
||||
|
||||
$stmt = $db->prepare("SELECT price_daily, price_monthly, price_year FROM {$table_prefix}billing_services WHERE service_id = ? LIMIT 1");
|
||||
$stmt = $db->prepare("SELECT price_monthly FROM {$table_prefix}billing_services WHERE service_id = ? LIMIT 1");
|
||||
if (!$stmt) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
$stmt->bind_param('i', $service_id);
|
||||
$stmt->execute();
|
||||
$stmt->bind_result($price_daily, $price_monthly, $price_year);
|
||||
$stmt->bind_result($price_monthly);
|
||||
$rate = 0.0;
|
||||
if ($stmt->fetch()) {
|
||||
if ($rate_type === 'daily') {
|
||||
$rate = floatval($price_daily);
|
||||
} elseif ($rate_type === 'yearly') {
|
||||
$rate = floatval($price_year);
|
||||
} else {
|
||||
$rate = floatval($price_monthly);
|
||||
}
|
||||
$rate = floatval($price_monthly);
|
||||
}
|
||||
$stmt->close();
|
||||
|
||||
|
|
@ -179,19 +161,13 @@ $slot_min_qty = 1;
|
|||
$slot_max_qty = 1;
|
||||
$durationInfo = billing_normalize_duration($invoice_duration);
|
||||
if ($service_id > 0) {
|
||||
$stmt = $db->prepare("SELECT service_name, price_daily, price_monthly, price_year, slot_min_qty, slot_max_qty FROM {$table_prefix}billing_services WHERE service_id = ? LIMIT 1");
|
||||
$stmt = $db->prepare("SELECT service_name, price_monthly, slot_min_qty, slot_max_qty FROM {$table_prefix}billing_services WHERE service_id = ? LIMIT 1");
|
||||
if ($stmt) {
|
||||
$stmt->bind_param('i', $service_id);
|
||||
$stmt->execute();
|
||||
$stmt->bind_result($service_name, $price_daily, $price_monthly, $price_year, $slot_min_qty, $slot_max_qty);
|
||||
$stmt->bind_result($service_name, $price_monthly, $slot_min_qty, $slot_max_qty);
|
||||
if ($stmt->fetch()) {
|
||||
if ($durationInfo['rate_type'] === 'daily') {
|
||||
$base_rate = floatval($price_daily);
|
||||
} elseif ($durationInfo['rate_type'] === 'yearly') {
|
||||
$base_rate = floatval($price_year);
|
||||
} else {
|
||||
$base_rate = floatval($price_monthly);
|
||||
}
|
||||
$base_rate = floatval($price_monthly);
|
||||
// constrain slots
|
||||
if ($max_players < $slot_min_qty) $max_players = $slot_min_qty;
|
||||
if ($max_players > $slot_max_qty) $max_players = $slot_max_qty;
|
||||
|
|
|
|||
|
|
@ -21,8 +21,8 @@
|
|||
.muted { color: #999; font-size: 0.85em; }
|
||||
.flash-ok { background: #d4edda; border: 1px solid #c3e6cb; padding: 10px 12px; margin-bottom: 10px; border-radius: 6px; color: #155724; }
|
||||
.flash-err { background: #f8d7da; border: 1px solid #f5c6cb; padding: 10px 12px; margin-bottom: 10px; border-radius: 6px; color: #721c24; }
|
||||
.servers-cell { text-align: left; }
|
||||
.server-cb-label { display: block; white-space: nowrap; margin: 2px 0; }
|
||||
.servers-cell { text-align: left; min-width: 240px; max-width: 280px; }
|
||||
.server-cb-label { display: block; white-space: normal; margin: 2px 0; }
|
||||
.action-cell { text-align: center; min-width: 120px; }
|
||||
.btn-row-save, .btn-save-all {
|
||||
border: 1px solid #3e7ab8;
|
||||
|
|
@ -65,7 +65,7 @@
|
|||
*
|
||||
* Columns that are admin-editable and NEVER overwritten by sync:
|
||||
* enabled, slot_min_qty, slot_max_qty,
|
||||
* price_daily, price_monthly, price_year,
|
||||
* price_monthly,
|
||||
* remote_server_id, description, img_url
|
||||
*/
|
||||
|
||||
|
|
@ -310,7 +310,7 @@ $flashType = 'ok';
|
|||
$sort = strtolower((string)($_GET['sort'] ?? $_POST['sort'] ?? 'game'));
|
||||
$dir = strtolower((string)($_GET['dir'] ?? $_POST['dir'] ?? 'asc')) === 'desc' ? 'desc' : 'asc';
|
||||
$gameMode = strtolower((string)($_GET['game_mode'] ?? $_POST['game_mode'] ?? 'name'));
|
||||
if (!in_array($sort, ['game', 'config', 'enabled', 'day', 'month', 'year', 'servers'], true)) {
|
||||
if (!in_array($sort, ['game', 'config', 'enabled', 'month', 'servers'], true)) {
|
||||
$sort = 'game';
|
||||
}
|
||||
if (!in_array($gameMode, ['name', 'enabled'], true)) {
|
||||
|
|
@ -367,9 +367,7 @@ if (isset($_POST['save_services']) || isset($_POST['save_row'])) {
|
|||
continue;
|
||||
}
|
||||
$enabled = isset($svcData['enabled']) ? 1 : 0;
|
||||
$priceDaily = number_format((float)($svcData['price_daily'] ?? 0), 2, '.', '');
|
||||
$priceMonthly = number_format((float)($svcData['price_monthly'] ?? 0), 2, '.', '');
|
||||
$priceYear = number_format((float)($svcData['price_year'] ?? 0), 2, '.', '');
|
||||
$slotMin = max(1, (int)($svcData['slot_min_qty'] ?? 1));
|
||||
$slotMax = max(1, (int)($svcData['slot_max_qty'] ?? 1));
|
||||
if ($slotMax < $slotMin) { $slotMax = $slotMin; }
|
||||
|
|
@ -396,9 +394,7 @@ if (isset($_POST['save_services']) || isset($_POST['save_row'])) {
|
|||
$ok = $db->query(
|
||||
"UPDATE `{$table_prefix}billing_services`
|
||||
SET enabled = {$enabled},
|
||||
price_daily = '{$priceDaily}',
|
||||
price_monthly = '{$priceMonthly}',
|
||||
price_year = '{$priceYear}',
|
||||
slot_min_qty = {$slotMin},
|
||||
slot_max_qty = {$slotMax},
|
||||
description = '{$description}',
|
||||
|
|
@ -453,7 +449,7 @@ while ($rsRes && ($row = $rsRes->fetch_assoc())) {
|
|||
$services = [];
|
||||
$svcRes = $db->query(
|
||||
"SELECT bs.service_id, bs.service_name, bs.enabled,
|
||||
bs.price_daily, bs.price_monthly, bs.price_year,
|
||||
bs.price_monthly,
|
||||
bs.slot_min_qty, bs.slot_max_qty,
|
||||
bs.remote_server_id, bs.description, bs.img_url,
|
||||
ch.home_cfg_file
|
||||
|
|
@ -474,15 +470,9 @@ if (!empty($services)) {
|
|||
case 'enabled':
|
||||
$cmp = ((int)($a['enabled'] ?? 0)) <=> ((int)($b['enabled'] ?? 0));
|
||||
break;
|
||||
case 'day':
|
||||
$cmp = ((float)($a['price_daily'] ?? 0)) <=> ((float)($b['price_daily'] ?? 0));
|
||||
break;
|
||||
case 'month':
|
||||
$cmp = ((float)($a['price_monthly'] ?? 0)) <=> ((float)($b['price_monthly'] ?? 0));
|
||||
break;
|
||||
case 'year':
|
||||
$cmp = ((float)($a['price_year'] ?? 0)) <=> ((float)($b['price_year'] ?? 0));
|
||||
break;
|
||||
case 'servers':
|
||||
$countA = trim((string)($a['remote_server_id'] ?? '')) === '' ? 0 : count(array_filter(explode(',', (string)$a['remote_server_id']), 'strlen'));
|
||||
$countB = trim((string)($b['remote_server_id'] ?? '')) === '' ? 0 : count(array_filter(explode(',', (string)$b['remote_server_id']), 'strlen'));
|
||||
|
|
@ -549,18 +539,10 @@ if (!empty($services)) {
|
|||
</th>
|
||||
<th>Min Slots</th>
|
||||
<th>Max Slots</th>
|
||||
<th>
|
||||
<?php $p = sort_link_params('day', $sort, $dir, $gameMode); ?>
|
||||
<a class="sort-link <?php echo $sort === 'day' ? 'sort-active' : ''; ?>" href="/adminserverlist.php?<?php echo h(http_build_query($p)); ?>">Price / Day ($)</a>
|
||||
</th>
|
||||
<th>
|
||||
<?php $p = sort_link_params('month', $sort, $dir, $gameMode); ?>
|
||||
<a class="sort-link <?php echo $sort === 'month' ? 'sort-active' : ''; ?>" href="/adminserverlist.php?<?php echo h(http_build_query($p)); ?>">Price / Month ($)</a>
|
||||
</th>
|
||||
<th>
|
||||
<?php $p = sort_link_params('year', $sort, $dir, $gameMode); ?>
|
||||
<a class="sort-link <?php echo $sort === 'year' ? 'sort-active' : ''; ?>" href="/adminserverlist.php?<?php echo h(http_build_query($p)); ?>">Price / Year ($)</a>
|
||||
</th>
|
||||
<th>Description</th>
|
||||
<th>Image</th>
|
||||
<th>
|
||||
|
|
@ -615,24 +597,12 @@ if (!empty($services)) {
|
|||
value="<?php echo (int)$svc['slot_max_qty']; ?>">
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<input type="number" step="0.01" min="0" class="price-input"
|
||||
name="svc[<?php echo $sid; ?>][price_daily]"
|
||||
value="<?php echo h(number_format((float)$svc['price_daily'], 2, '.', '')); ?>">
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<input type="number" step="0.01" min="0" class="price-input"
|
||||
name="svc[<?php echo $sid; ?>][price_monthly]"
|
||||
value="<?php echo h(number_format((float)$svc['price_monthly'], 2, '.', '')); ?>">
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<input type="number" step="0.01" min="0" class="price-input"
|
||||
name="svc[<?php echo $sid; ?>][price_year]"
|
||||
value="<?php echo h(number_format((float)$svc['price_year'], 2, '.', '')); ?>">
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<input type="text" class="desc-input"
|
||||
name="svc[<?php echo $sid; ?>][description]"
|
||||
|
|
@ -716,6 +686,7 @@ if (!empty($services)) {
|
|||
<ul>
|
||||
<li>A service will only appear in the store when <strong>Enabled</strong> is checked
|
||||
<em>and</em> at least one server is selected.</li>
|
||||
<li><strong>Price / Month ($)</strong> is the canonical billing price used by cart, checkout, and provisioning.</li>
|
||||
<li>The <strong>Game Name</strong> and <strong>Config XML</strong> columns are sourced
|
||||
from <code><?php echo h("{$table_prefix}config_homes"); ?></code> and are read-only
|
||||
here. To change them, update the game XML config in the panel.</li>
|
||||
|
|
|
|||
|
|
@ -124,19 +124,7 @@ function cap_invoice_ids_from_custom_id(?string $customId): array {
|
|||
}
|
||||
|
||||
function cap_get_duration_metadata(array $invoice): array {
|
||||
$duration = strtolower((string)($invoice['invoice_duration'] ?? $invoice['rate_type'] ?? 'month'));
|
||||
switch ($duration) {
|
||||
case 'day':
|
||||
case 'daily':
|
||||
return ['invoice_duration' => 'day', 'rate_type' => 'daily', 'days' => 1];
|
||||
case 'year':
|
||||
case 'yearly':
|
||||
return ['invoice_duration' => 'year', 'rate_type' => 'yearly', 'days' => 365];
|
||||
case 'month':
|
||||
case 'monthly':
|
||||
default:
|
||||
return ['invoice_duration' => 'month', 'rate_type' => 'monthly', 'days' => 31];
|
||||
}
|
||||
return ['invoice_duration' => 'month', 'rate_type' => 'monthly', 'days' => 31];
|
||||
}
|
||||
|
||||
function cap_get_end_date(array $invoice, ?string $fromDate = null): string {
|
||||
|
|
|
|||
|
|
@ -111,19 +111,7 @@ require_once __DIR__ . '/classes/BillingService.php';
|
|||
$repo = new BillingRepository($mysqli, $table_prefix);
|
||||
$newOrderIds = [];
|
||||
$duration_meta = static function (array $invoice): array {
|
||||
$duration = strtolower((string)($invoice['invoice_duration'] ?? $invoice['rate_type'] ?? 'month'));
|
||||
switch ($duration) {
|
||||
case 'day':
|
||||
case 'daily':
|
||||
return ['invoice_duration' => 'day', 'rate_type' => 'daily', 'days' => 1];
|
||||
case 'year':
|
||||
case 'yearly':
|
||||
return ['invoice_duration' => 'year', 'rate_type' => 'yearly', 'days' => 365];
|
||||
case 'month':
|
||||
case 'monthly':
|
||||
default:
|
||||
return ['invoice_duration' => 'month', 'rate_type' => 'monthly', 'days' => 31];
|
||||
}
|
||||
return ['invoice_duration' => 'month', 'rate_type' => 'monthly', 'days' => 31];
|
||||
};
|
||||
|
||||
foreach ($invoices as $inv) {
|
||||
|
|
|
|||
|
|
@ -33,23 +33,9 @@ class BillingService
|
|||
{
|
||||
$qty = max(1, $qty);
|
||||
$players = max(1, $players);
|
||||
|
||||
switch ($rateType) {
|
||||
case 'daily':
|
||||
$basePrice = (float)($service['price_daily'] ?? 0);
|
||||
$periodDays = $qty;
|
||||
break;
|
||||
case 'yearly':
|
||||
$basePrice = (float)($service['price_year'] ?? 0);
|
||||
$periodDays = $qty * 365;
|
||||
break;
|
||||
case 'monthly':
|
||||
default:
|
||||
$rateType = 'monthly';
|
||||
$basePrice = (float)($service['price_monthly'] ?? 0);
|
||||
$periodDays = $qty * 31;
|
||||
break;
|
||||
}
|
||||
$rateType = 'monthly';
|
||||
$basePrice = (float)($service['price_monthly'] ?? 0);
|
||||
$periodDays = $qty * 31;
|
||||
|
||||
// price_monthly etc is the per-player per-period rate
|
||||
$ratePerPlayer = $basePrice;
|
||||
|
|
@ -163,9 +149,7 @@ class BillingService
|
|||
$periodEnd = $invoiceRow['period_end'] ?? null;
|
||||
|
||||
if (!$periodEnd) {
|
||||
$rateType = $invoiceRow['rate_type'] ?? 'monthly';
|
||||
$periodMap = ['daily' => '+1 day', 'monthly' => '+31 days', 'yearly' => '+365 days'];
|
||||
$periodEnd = date('Y-m-d H:i:s', strtotime($periodMap[$rateType] ?? '+31 days'));
|
||||
$periodEnd = date('Y-m-d H:i:s', strtotime('+31 days'));
|
||||
}
|
||||
|
||||
// If current expiry is in the future, extend from it; otherwise reset from period_end
|
||||
|
|
@ -177,9 +161,7 @@ class BillingService
|
|||
if ($periodStart && $periodEndVal) {
|
||||
$currentPeriodSecs = strtotime($periodEndVal) - strtotime($periodStart);
|
||||
} else {
|
||||
$rateType2 = $invoiceRow['rate_type'] ?? 'monthly';
|
||||
$periodSecMap = ['daily' => 86400, 'monthly' => 31 * 86400, 'yearly' => 365 * 86400];
|
||||
$currentPeriodSecs = $periodSecMap[$rateType2] ?? (31 * 86400);
|
||||
$currentPeriodSecs = 31 * 86400;
|
||||
}
|
||||
$newExpiry = date('Y-m-d H:i:s', strtotime($currentExpiry) + max(86400, $currentPeriodSecs));
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<?php
|
||||
require_once __DIR__ . '/../../includes/lib_remote.php';
|
||||
require_once __DIR__ . '/../config_games/server_config_parser.php';
|
||||
require_once __DIR__ . '/../gamemanager/update_actions.php';
|
||||
|
||||
if (!function_exists('billing_generate_provision_password')) {
|
||||
function billing_generate_provision_password(int $bytes = 12)
|
||||
|
|
@ -389,99 +390,24 @@ function exec_ogp_module()
|
|||
}
|
||||
}
|
||||
|
||||
//Read the Game Config from the XML file
|
||||
if (!$order_failed) {
|
||||
$server_xml = read_server_config(SERVER_CONFIG_LOCATION."/".$home_info['home_cfg_file']);
|
||||
if ($server_xml === false) {
|
||||
$order_failed = true;
|
||||
$order_failure_reason = "Could not read server XML for home #{$home_id}.";
|
||||
}
|
||||
}
|
||||
|
||||
//Get Values from XML
|
||||
$mod_xml = false;
|
||||
$modkey = '';
|
||||
$installer_name = '';
|
||||
if (!$order_failed) {
|
||||
$selected_mod = $home_info['mods'][$mod_id] ?? reset($home_info['mods']);
|
||||
if (empty($selected_mod) || empty($selected_mod['mod_key'])) {
|
||||
$order_failed = true;
|
||||
$order_failure_reason = "No valid mod profile found for home #{$home_id}.";
|
||||
} else {
|
||||
$modkey = (string)$selected_mod['mod_key'];
|
||||
$mod_xml = xml_get_mod($server_xml, $modkey);
|
||||
if ($mod_xml === false && isset($server_xml->mods->mod[0])) {
|
||||
$mod_xml = $server_xml->mods->mod[0];
|
||||
$modkey = (string)$mod_xml['key'];
|
||||
}
|
||||
if ($mod_xml === false) {
|
||||
$order_failed = true;
|
||||
$order_failure_reason = "No installable mod profile exists in XML for home #{$home_id}.";
|
||||
} else {
|
||||
$installer_name = (string)$mod_xml->installer_name;
|
||||
$resolved_mod_cfg_id = intval($selected_mod['mod_cfg_id'] ?? $resolved_mod_cfg_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Get Preinstall commands from xml
|
||||
$precmd = !$order_failed ? $server_xml->pre_install : '';
|
||||
|
||||
|
||||
//Get Postinstall commands from xml
|
||||
$postcmd = !$order_failed ? $server_xml->post_install : '';
|
||||
|
||||
|
||||
//Enable FTP account in remote server
|
||||
if (!$order_failed && $ftp == "enabled")
|
||||
{
|
||||
$remote->ftp_mgr("useradd", $home_info['home_id'], $home_info['ftp_password'], $home_info['home_path']);
|
||||
$db->changeFtpStatus('enabled',$home_info['home_id']);
|
||||
}
|
||||
|
||||
//Install files for this service in the remote server
|
||||
if (!$order_failed) {
|
||||
$exec_folder_path = clean_path($home_info['home_path'] . "/" . $server_xml->exe_location );
|
||||
$exec_path = clean_path($exec_folder_path . "/" . $server_xml->server_exec_name );
|
||||
}
|
||||
|
||||
if (!$order_failed && (string)$server_xml->installer === "steamcmd" && !empty((string)$installer_name) )
|
||||
{
|
||||
if( preg_match("/win32/", $server_xml->game_key) OR preg_match("/win64/", $server_xml->game_key) )
|
||||
$cfg_os = "windows";
|
||||
elseif( preg_match("/linux/", $server_xml->game_key) )
|
||||
$cfg_os = "linux";
|
||||
|
||||
// Some games like L4D2 require anonymous login
|
||||
if(!empty($mod_xml->installer_login)){
|
||||
$login = $mod_xml->installer_login;
|
||||
$pass = '';
|
||||
}else{
|
||||
$login = $settings['steam_user'];
|
||||
$pass = $settings['steam_pass'];
|
||||
}
|
||||
|
||||
$modname = ( $installer_name == '90' and !preg_match("/(cstrike|valve)/", $modkey) ) ? $modkey : '';
|
||||
$betaname = isset($mod_xml->betaname) ? $mod_xml->betaname : '';
|
||||
$betapwd = isset($mod_xml->betapwd) ? $mod_xml->betapwd : '';
|
||||
$arch = isset($mod_xml->steam_bitness) ? $mod_xml->steam_bitness : '';
|
||||
|
||||
$remote->steam_cmd( $home_id,$home_info['home_path'],$installer_name,$modname,
|
||||
$betaname,$betapwd,$login,$pass,$settings['steam_guard'],
|
||||
$exec_folder_path,$exec_path,$precmd,$postcmd,$cfg_os,'',$arch);
|
||||
}
|
||||
elseif (!$order_failed)
|
||||
{
|
||||
// No SteamCMD installer — run pre/post install scripts only.
|
||||
if (!empty((string)$precmd)) {
|
||||
$result = $remote->exec((string)$precmd);
|
||||
if ($result === NULL)
|
||||
$db->logger("Script-only install: pre_install script returned no output for home_id $home_id");
|
||||
}
|
||||
if (!empty((string)$postcmd)) {
|
||||
$result = $remote->exec((string)$postcmd);
|
||||
if ($result === NULL)
|
||||
$db->logger("Script-only install: post_install script returned no output for home_id $home_id");
|
||||
if (!$order_failed) {
|
||||
$autoInstall = gamemanager_trigger_update_install(
|
||||
$db,
|
||||
$home_info,
|
||||
intval($mod_id),
|
||||
array('settings' => $settings)
|
||||
);
|
||||
$mod_id = intval($autoInstall['mod_id'] ?? $mod_id);
|
||||
if (empty($autoInstall['ok'])) {
|
||||
$order_failed = true;
|
||||
$order_failure_reason = "Server files have not been installed yet. " . ($autoInstall['message'] ?? 'Auto install could not be started.');
|
||||
}
|
||||
}
|
||||
if (!$order_failed) {
|
||||
|
|
@ -529,54 +455,18 @@ function exec_ogp_module()
|
|||
}
|
||||
$end_date_str = date('Y-m-d H:i:s', $existing_end);
|
||||
}
|
||||
elseif ($order['invoice_duration'] == "day")
|
||||
else
|
||||
{
|
||||
|
||||
if(empty($order['end_date']) || $order['end_date'] === NULL){
|
||||
$end_date = strtotime('+'.$order['qty'].' day');
|
||||
$qty_days = max(1, intval($order['qty'])) * 31;
|
||||
if (empty($order['end_date']) || $order['end_date'] === NULL) {
|
||||
$end_date = strtotime('+' . $qty_days . ' day');
|
||||
} else {
|
||||
$current_end = strtotime($order['end_date']);
|
||||
if ($current_end === false) {
|
||||
$current_end = time();
|
||||
}
|
||||
$end_date = strtotime('+' . $qty_days . ' day', $current_end);
|
||||
}
|
||||
else{
|
||||
//this is a renewel, start from end of previous order
|
||||
$current_end = strtotime($order['end_date']);
|
||||
if ($current_end === false) {
|
||||
$current_end = time(); // fallback to now if date is invalid
|
||||
}
|
||||
$end_date = strtotime('+'.$order['qty'].' day', $current_end);
|
||||
}
|
||||
|
||||
}
|
||||
elseif ($order['invoice_duration'] == "month")
|
||||
{
|
||||
// this is a new order
|
||||
if(empty($order['end_date']) || $order['end_date'] === NULL){
|
||||
$end_date = strtotime('+'.(intval($order['qty']) * 31).' day');
|
||||
|
||||
}
|
||||
else{
|
||||
//this is a renewel, start from end of previous order
|
||||
$current_end = strtotime($order['end_date']);
|
||||
if ($current_end === false) {
|
||||
$current_end = time(); // fallback to now if date is invalid
|
||||
}
|
||||
$end_date = strtotime('+'.(intval($order['qty']) * 31).' day', $current_end);
|
||||
}
|
||||
}
|
||||
elseif ($order['invoice_duration'] == "year")
|
||||
{
|
||||
// this is a new order
|
||||
if(empty($order['end_date']) || $order['end_date'] === NULL){
|
||||
$end_date = strtotime('+'.$order['qty'].' year');
|
||||
}
|
||||
else{
|
||||
//this is a renewel, start from end of previous order
|
||||
$current_end = strtotime($order['end_date']);
|
||||
if ($current_end === false) {
|
||||
$current_end = time(); // fallback to now if date is invalid
|
||||
}
|
||||
$end_date = strtotime('+'.$order['qty'].' year', $current_end);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
if (!isset($end_date_str)) {
|
||||
$end_date_str = date('Y-m-d H:i:s', $end_date);
|
||||
|
|
|
|||
|
|
@ -176,7 +176,7 @@ if ($db && $user_id > 0) {
|
|||
<h3>What Happens Next?</h3>
|
||||
<ul>
|
||||
<li><strong>✓ Payment Confirmed:</strong> Your payment has been captured by PayPal</li>
|
||||
<li><strong>⚙️ Server Provisioning:</strong> Your game server(s) will be automatically created when you log into the panel</li>
|
||||
<li><strong>⚙️ Server Provisioning:</strong> Your game server(s) are queued for automatic install now; if a node is unavailable they remain clearly marked as pending install</li>
|
||||
<li><strong>📧 Email Notification:</strong> You'll receive a confirmation email with your order details</li>
|
||||
<li><strong>🎮 Access Your Servers:</strong> Log into the Game Server Panel to manage your new servers</li>
|
||||
</ul>
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Last Updated at 9:18pm on 2026-08-05
|
||||
Last Updated at 10:02pm on 2026-05-08
|
||||
|
|
|
|||
130
modules/gamemanager/update_actions.php
Normal file
130
modules/gamemanager/update_actions.php
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
<?php
|
||||
|
||||
if (!function_exists('gamemanager_choose_mod_id')) {
|
||||
function gamemanager_choose_mod_id(array $home_info, int $preferred_mod_id = 0): int
|
||||
{
|
||||
$mods = $home_info['mods'] ?? array();
|
||||
if (!is_array($mods) || empty($mods)) {
|
||||
return 0;
|
||||
}
|
||||
if ($preferred_mod_id > 0 && isset($mods[$preferred_mod_id])) {
|
||||
return $preferred_mod_id;
|
||||
}
|
||||
$keys = array_keys($mods);
|
||||
return intval(reset($keys));
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('gamemanager_trigger_update_install')) {
|
||||
function gamemanager_trigger_update_install($db, array $home_info, int $mod_id, array $options = array()): array
|
||||
{
|
||||
$home_id = intval($home_info['home_id'] ?? 0);
|
||||
$mod_id = gamemanager_choose_mod_id($home_info, $mod_id);
|
||||
if ($home_id <= 0) {
|
||||
return array('ok' => false, 'pending' => true, 'message' => 'Invalid home_id.', 'mod_id' => $mod_id);
|
||||
}
|
||||
if ($mod_id <= 0 || empty($home_info['mods'][$mod_id])) {
|
||||
return array('ok' => false, 'pending' => true, 'message' => "No mod profile configured for home #{$home_id}.", 'mod_id' => $mod_id);
|
||||
}
|
||||
|
||||
$server_xml = read_server_config(SERVER_CONFIG_LOCATION . "/" . $home_info['home_cfg_file']);
|
||||
if (!$server_xml) {
|
||||
return array('ok' => false, 'pending' => true, 'message' => "Could not read server config XML for home #{$home_id}.", 'mod_id' => $mod_id);
|
||||
}
|
||||
|
||||
$remote = new OGPRemoteLibrary($home_info['agent_ip'], $home_info['agent_port'], $home_info['encryption_key'], $home_info['timeout']);
|
||||
if ($remote->status_chk() === 0) {
|
||||
return array('ok' => false, 'pending' => true, 'message' => 'Agent is offline.', 'mod_id' => $mod_id);
|
||||
}
|
||||
if ($remote->is_screen_running(OGP_SCREEN_TYPE_HOME, $home_id) == 1) {
|
||||
return array('ok' => false, 'pending' => false, 'message' => 'Server is running and cannot be updated.', 'mod_id' => $mod_id);
|
||||
}
|
||||
|
||||
$log_txt = '';
|
||||
$update_active = $remote->get_log(OGP_SCREEN_TYPE_UPDATE, $home_id, clean_path($home_info['home_path']), $log_txt);
|
||||
if ($update_active == 1) {
|
||||
return array('ok' => true, 'started' => true, 'already_running' => true, 'message' => 'Update already in progress.', 'mod_id' => $mod_id);
|
||||
}
|
||||
|
||||
$modkey = $home_info['mods'][$mod_id]['mod_key'] ?? '';
|
||||
$mod_xml = xml_get_mod($server_xml, $modkey);
|
||||
if (!$mod_xml) {
|
||||
return array('ok' => false, 'pending' => true, 'message' => "Mod key '{$modkey}' not found in XML.", 'mod_id' => $mod_id);
|
||||
}
|
||||
|
||||
$installer_name = isset($mod_xml->installer_name) ? (string)$mod_xml->installer_name : (string)$modkey;
|
||||
$precmd = $home_info['mods'][$mod_id]['precmd'] == ""
|
||||
? ($home_info['mods'][$mod_id]['def_precmd'] == "" ? $server_xml->pre_install : $home_info['mods'][$mod_id]['def_precmd'])
|
||||
: $home_info['mods'][$mod_id]['precmd'];
|
||||
$postcmd = $home_info['mods'][$mod_id]['postcmd'] == ""
|
||||
? ($home_info['mods'][$mod_id]['def_postcmd'] == "" ? $server_xml->post_install : $home_info['mods'][$mod_id]['def_postcmd'])
|
||||
: $home_info['mods'][$mod_id]['postcmd'];
|
||||
$exec_folder_path = clean_path($home_info['home_path'] . "/" . $server_xml->exe_location);
|
||||
$exec_path = clean_path($exec_folder_path . "/" . $server_xml->server_exec_name);
|
||||
|
||||
$master_server_home_id = intval($options['master_server_home_id'] ?? 0);
|
||||
if ($master_server_home_id > 0) {
|
||||
if ($db->getMasterServer($home_info['remote_server_id'], $home_info['home_cfg_id']) != $master_server_home_id) {
|
||||
return array('ok' => false, 'pending' => false, 'message' => 'Attempting update from non-master server.', 'mod_id' => $mod_id);
|
||||
}
|
||||
if ($master_server_home_id == $home_id) {
|
||||
return array('ok' => false, 'pending' => false, 'message' => 'Cannot update from own self.', 'mod_id' => $mod_id);
|
||||
}
|
||||
$ms_info = $db->getGameHome($master_server_home_id);
|
||||
$steam_out = $remote->masterServerUpdate($home_id, $home_info['home_path'], $master_server_home_id, $ms_info['home_path'], $exec_folder_path, $exec_path, $precmd, $postcmd);
|
||||
if ($steam_out === 0) {
|
||||
return array('ok' => false, 'pending' => true, 'message' => 'Failed to start master update.', 'mod_id' => $mod_id);
|
||||
}
|
||||
return array('ok' => true, 'started' => true, 'message' => 'Update started.', 'mod_id' => $mod_id);
|
||||
}
|
||||
|
||||
$use_steamcmd = ((string)$server_xml->installer === "steamcmd");
|
||||
if ($use_steamcmd && !empty((string)$installer_name)) {
|
||||
$cfg_os = '';
|
||||
if (preg_match("/win32/", $server_xml->game_key) || preg_match("/win64/", $server_xml->game_key)) {
|
||||
$cfg_os = "windows";
|
||||
} elseif (preg_match("/linux/", $server_xml->game_key)) {
|
||||
$cfg_os = "linux";
|
||||
}
|
||||
|
||||
$settings = is_array($options['settings'] ?? null) ? $options['settings'] : $db->getSettings();
|
||||
if (!empty($mod_xml->installer_login)) {
|
||||
$login = (string)$mod_xml->installer_login;
|
||||
$pass = '';
|
||||
} else {
|
||||
$login = (string)($settings['steam_user'] ?? '');
|
||||
$pass = (string)($settings['steam_pass'] ?? '');
|
||||
}
|
||||
|
||||
$modname = ($installer_name == '90') ? $modkey : '';
|
||||
$betaname = isset($mod_xml->betaname) ? (string)$mod_xml->betaname : '';
|
||||
$betapwd = isset($mod_xml->betapwd) ? (string)$mod_xml->betapwd : '';
|
||||
$arch = isset($mod_xml->steam_bitness) ? (string)$mod_xml->steam_bitness : '';
|
||||
$lockFiles = (isset($server_xml->lock_files) && !empty($server_xml->lock_files)) ? trim((string)$server_xml->lock_files) : "";
|
||||
$steam_out = $remote->steam_cmd($home_id, $home_info['home_path'], $installer_name, $modname,
|
||||
$betaname, $betapwd, $login, $pass, $settings['steam_guard'] ?? '',
|
||||
$exec_folder_path, $exec_path, $precmd, $postcmd, $cfg_os, $lockFiles, $arch);
|
||||
if ($steam_out === 0) {
|
||||
return array('ok' => false, 'pending' => true, 'message' => 'Failed to start SteamCMD update.', 'mod_id' => $mod_id);
|
||||
}
|
||||
return array('ok' => true, 'started' => true, 'message' => 'Update started.', 'mod_id' => $mod_id);
|
||||
}
|
||||
|
||||
$ran_scripts = false;
|
||||
if (!empty((string)$precmd)) {
|
||||
$remote->exec((string)$precmd);
|
||||
$ran_scripts = true;
|
||||
}
|
||||
if (!empty((string)$postcmd)) {
|
||||
$remote->exec((string)$postcmd);
|
||||
$ran_scripts = true;
|
||||
}
|
||||
return array(
|
||||
'ok' => true,
|
||||
'started' => $ran_scripts,
|
||||
'completed' => !$ran_scripts,
|
||||
'message' => $ran_scripts ? 'Script install started.' : 'No installer command was required.',
|
||||
'mod_id' => $mod_id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -24,6 +24,7 @@
|
|||
|
||||
require_once("includes/lib_remote.php");
|
||||
require_once("modules/config_games/server_config_parser.php");
|
||||
require_once("modules/gamemanager/update_actions.php");
|
||||
|
||||
function exec_ogp_module() {
|
||||
|
||||
|
|
@ -90,111 +91,27 @@ function exec_ogp_module() {
|
|||
// Start update.
|
||||
else if ($_GET['update'] == 'update' && $update_active != 1)
|
||||
{
|
||||
$installer_name = $modkey;
|
||||
|
||||
if ( isset( $mod_xml->installer_name ) )
|
||||
{
|
||||
$installer_name = $mod_xml->installer_name;
|
||||
$start_result = gamemanager_trigger_update_install(
|
||||
$db,
|
||||
$home_info,
|
||||
intval($mod_id),
|
||||
array(
|
||||
'master_server_home_id' => isset($_REQUEST['master_server_home_id']) ? intval($_REQUEST['master_server_home_id']) : 0,
|
||||
'settings' => $db->getSettings(),
|
||||
)
|
||||
);
|
||||
$mod_id = intval($start_result['mod_id'] ?? $mod_id);
|
||||
if (empty($start_result['ok'])) {
|
||||
print_failure(!empty($start_result['message']) ? $start_result['message'] : get_lang("failed_to_start_steam_update"));
|
||||
return;
|
||||
}
|
||||
|
||||
$precmd = $home_info['mods'][$mod_id]['precmd'] == "" ?
|
||||
( $home_info['mods'][$mod_id]['def_precmd'] == "" ? $server_xml->pre_install :
|
||||
$home_info['mods'][$mod_id]['def_precmd'] ) : $home_info['mods'][$mod_id]['precmd'];
|
||||
|
||||
$postcmd = $home_info['mods'][$mod_id]['postcmd'] == "" ?
|
||||
( $home_info['mods'][$mod_id]['def_postcmd'] == "" ? $server_xml->post_install :
|
||||
$home_info['mods'][$mod_id]['def_precmd'] ) : $home_info['mods'][$mod_id]['postcmd'];
|
||||
|
||||
$exec_folder_path = clean_path($home_info['home_path'] . "/" . $server_xml->exe_location );
|
||||
$exec_path = clean_path($exec_folder_path . "/" . $server_xml->server_exec_name );
|
||||
|
||||
if( isset( $_REQUEST['master_server_home_id'] ) )
|
||||
{
|
||||
$ms_home_id = $_REQUEST['master_server_home_id'];
|
||||
|
||||
if ($db->getMasterServer($home_info['remote_server_id'], $home_info['home_cfg_id']) == $ms_home_id) {
|
||||
if ($ms_home_id !== $home_id) {
|
||||
$ms_info = $db->getGameHome($ms_home_id);
|
||||
$steam_out = $remote->masterServerUpdate( $home_id,$home_info['home_path'],$ms_home_id,$ms_info['home_path'],$exec_folder_path,$exec_path,$precmd,$postcmd );
|
||||
} else {
|
||||
print_failure(get_lang('cannot_update_from_own_self'));
|
||||
$view->refresh('?m=gamemanager&p=game_monitor', 2);
|
||||
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
$db->logger(get_lang_f('update_attempt_from_nonmaster_server', $_SESSION['users_login'], $home_id, $ms_home_id));
|
||||
print_failure(get_lang('attempting_nonmaster_update'));
|
||||
$view->refresh('?m=gamemanager&p=game_monitor', 2);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
elseif ($use_steamcmd && !empty((string)$installer_name))
|
||||
{
|
||||
if( preg_match("/win32/", $server_xml->game_key) OR preg_match("/win64/", $server_xml->game_key) )
|
||||
$cfg_os = "windows";
|
||||
elseif( preg_match("/linux/", $server_xml->game_key) )
|
||||
$cfg_os = "linux";
|
||||
|
||||
$settings = $db->getSettings();
|
||||
|
||||
// Some games like L4D2 require anonymous login
|
||||
if($mod_xml->installer_login){
|
||||
$login = $mod_xml->installer_login;
|
||||
$pass = '';
|
||||
}else{
|
||||
$login = $settings['steam_user'];
|
||||
$pass = $settings['steam_pass'];
|
||||
}
|
||||
|
||||
$modname = ( $installer_name == '90' ) ? $modkey : '';
|
||||
$betaname = isset($mod_xml->betaname) ? $mod_xml->betaname : '';
|
||||
$betapwd = isset($mod_xml->betapwd) ? $mod_xml->betapwd : '';
|
||||
$arch = isset($mod_xml->steam_bitness) ? $mod_xml->steam_bitness : '';
|
||||
|
||||
// Additional files to lock
|
||||
if(isset($server_xml->lock_files) && !empty($server_xml->lock_files)){
|
||||
$lockFiles = trim($server_xml->lock_files);
|
||||
}else{
|
||||
$lockFiles = "";
|
||||
}
|
||||
|
||||
$steam_out = $remote->steam_cmd( $home_id,$home_info['home_path'],$installer_name,$modname,
|
||||
$betaname,$betapwd,$login,$pass,$settings['steam_guard'],
|
||||
$exec_folder_path,$exec_path,$precmd,$postcmd,$cfg_os,$lockFiles,$arch);
|
||||
}
|
||||
else
|
||||
{
|
||||
// No SteamCMD installer — run pre/post install scripts only.
|
||||
$ran_scripts = false;
|
||||
if (!empty((string)$precmd)) {
|
||||
$remote->exec((string)$precmd);
|
||||
$ran_scripts = true;
|
||||
}
|
||||
if (!empty((string)$postcmd)) {
|
||||
$remote->exec((string)$postcmd);
|
||||
$ran_scripts = true;
|
||||
}
|
||||
if ($ran_scripts) {
|
||||
print_success( get_lang("update_started") );
|
||||
} else {
|
||||
print_success( get_lang("update_completed") );
|
||||
}
|
||||
if (!empty($start_result['started'])) {
|
||||
print_success(get_lang("update_started"));
|
||||
} else {
|
||||
print_success(get_lang("update_completed"));
|
||||
$view->refresh("?m=gamemanager&p=game_monitor&home_id=$home_id", 3);
|
||||
return;
|
||||
}
|
||||
|
||||
if( $steam_out === 0 )
|
||||
{
|
||||
print_failure( get_lang("failed_to_start_steam_update") );
|
||||
return;
|
||||
}
|
||||
else if ( $steam_out === 1 )
|
||||
{
|
||||
print_success( get_lang("update_started") );
|
||||
}
|
||||
}
|
||||
// Refresh update page.
|
||||
else
|
||||
|
|
|
|||
|
|
@ -64,16 +64,30 @@ function exec_ogp_module()
|
|||
|
||||
// We must always add the home directory to the fm_cwd so that user
|
||||
// can not go out of the homedir.
|
||||
$path = clean_path($home_cfg['home_path']."/".@$_SESSION['fm_cwd_'.$home_id]);
|
||||
$cwd_session_key = 'fm_cwd_' . $home_id;
|
||||
if (!isset($_SESSION[$cwd_session_key]) || !is_string($_SESSION[$cwd_session_key])) {
|
||||
$_SESSION[$cwd_session_key] = '';
|
||||
}
|
||||
$path = clean_path($home_cfg['home_path']."/".$_SESSION[$cwd_session_key]);
|
||||
if (!$remote->rfile_exists($path))
|
||||
{
|
||||
while(!$remote->rfile_exists($path))
|
||||
{
|
||||
$_SESSION['fm_cwd_'.$home_id] = dirname($_SESSION['fm_cwd_'.$home_id]);
|
||||
$path = clean_path($home_cfg['home_path']."/".@$_SESSION['fm_cwd_'.$home_id]);
|
||||
$current_cwd = isset($_SESSION[$cwd_session_key]) ? (string)$_SESSION[$cwd_session_key] : '';
|
||||
if ($current_cwd === '' || $current_cwd === '.' || $current_cwd === DIRECTORY_SEPARATOR) {
|
||||
print_failure("Server files have not been installed yet.");
|
||||
echo "<table class='center'><tr><td><a href='?m=gamemanager&p=game_monitor&home_id=".$home_cfg['home_id']."'><< ".get_lang('back')."</a></td></tr></table>";
|
||||
return;
|
||||
}
|
||||
$parent_cwd = dirname($current_cwd);
|
||||
if (!is_string($parent_cwd) || $parent_cwd === '.' || $parent_cwd === DIRECTORY_SEPARATOR) {
|
||||
$parent_cwd = '';
|
||||
}
|
||||
$_SESSION[$cwd_session_key] = $parent_cwd;
|
||||
$path = clean_path($home_cfg['home_path']."/".$_SESSION[$cwd_session_key]);
|
||||
if($path == clean_path($home_cfg['home_path']."/"))
|
||||
{
|
||||
print_failure(get_lang_f("dir_not_found",$path));
|
||||
print_failure("Server files have not been installed yet.");
|
||||
echo "<table class='center'><tr><td><a href='?m=gamemanager&p=game_monitor&home_id=".$home_cfg['home_id']."'><< ".get_lang('back')."</a></td></tr></table>";
|
||||
return;
|
||||
}
|
||||
|
|
@ -214,7 +228,7 @@ function exec_ogp_module()
|
|||
{
|
||||
$remote->shell_action('remove_recursive', $files);
|
||||
$files = str_replace('" "','"<br>"',$files);
|
||||
$db->logger( get_lang("remove") . ": ${files}" );
|
||||
$db->logger( get_lang("remove") . ": {$files}" );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -349,7 +363,7 @@ function exec_ogp_module()
|
|||
if($items != '')
|
||||
{
|
||||
$retval = $remote->compress_files($items,$path,$archive_name,$archive_type);
|
||||
$archive = clean_path( "${path}/${archive_name}.${archive_type}" );
|
||||
$archive = clean_path( "{$path}/{$archive_name}.{$archive_type}" );
|
||||
if( $retval == 0 )
|
||||
{
|
||||
do{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue