Panel/modules/gamemanager/home_handling_functions.php
copilot-swe-agent[bot] 5b2162fb28
fix: standardize billing order status values and apply full-day expiry grace rule
- home_handling_functions.php: display expiration as Y-m-d only (not Y-m-d H:i);
  apply full-day grace by comparing date-only (midnight) in PHP so a server
  expiring on today's date remains Active the entire day
- cron-shop.php Step B & C: change server_expiration_date comparisons from
  < DATE_SUB(NOW(), ...) to DATE(server_expiration_date) < DATE_SUB(CURDATE(), ...)
  ensuring the full expiration day is honoured
- user_games/check_expire.php: change all three expiration queries from
  <= time() to < strtotime(date('Y-m-d')) (today-midnight) for full-day grace
- billing/test_integration.php: replace status='paid' (old order status) with
  status IN ('Active','paid') for backward compat during migration
- user_games/billing_integration.php: clarify comment that billing_invoices.status
  uses the payment lifecycle ('paid'/'unpaid'/'due'), separate from billing_orders.status
- Add modules/billing/sql/normalize_billing_order_status.sql migration script
  to convert installed->Active, paid->Active, suspended->Expired in existing rows
- Update CHANGELOG.md

Agent-Logs-Url: https://github.com/GameServerPanel/GSP/sessions/439845e0-926e-4b49-9cd0-810457b73c12

Co-authored-by: iaretechnician <2749183+iaretechnician@users.noreply.github.com>
2026-05-05 17:24:41 +00:00

587 lines
20 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/*
*
* OGP - Open Game Panel
* Copyright (C) 2008 - 2018 The OGP Development Team
*
* 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.
*
*/
function get_query_port($server_xml, $server_port) {
if ($server_xml->query_port) {
if ($server_xml->query_port['type'] == 'add') {
return $server_port + $server_xml->query_port;
}
if ($server_xml->query_port['type'] == 'subtract') {
return $server_port - $server_xml->query_port;
}
}
return $server_port;
}
function get_start_cmd($remote,$server_xml,$home_info,$mod_id,$ip,$port,$db)
{
$last_param = json_decode($home_info['last_param'], True);
$os = $remote->what_os();
$isAdmin = false;
if(hasValue($_SESSION) && hasValue($_SESSION['user_id'])){
$isAdmin = $db->isAdmin($_SESSION['user_id']);
}
$cli_param_data['GAME_TYPE'] = $home_info['mods'][$mod_id]['mod_key'];
$cli_param_data['IP'] = $ip;
$cli_param_data['PORT'] = $port;
$cli_param_data['HOSTNAME'] = $home_info['home_name'];
$cli_param_data['PID_FILE'] = "ogp_game_startup.pid";
// Linux
if( preg_match("/Linux/", $os) )
{
if(preg_match("/_win(32|64)?$/", $home_info['game_key']))
{
$home_path_wine = $remote->exec("winepath -w ".$home_info['home_path']);
$home_path_wine = str_replace("\\","\\\\", $home_path_wine);
$home_path_wine = trim($home_path_wine);
$cli_param_data['BASE_PATH'] = $home_path_wine;
$cli_param_data['HOME_PATH'] = $home_path_wine;
$cli_param_data['SAVE_PATH'] = $home_path_wine;
$cli_param_data['OUTPUT_PATH'] = $home_path_wine;
$cli_param_data['USER_PATH'] = $home_path_wine;
}
else
{
$cli_param_data['BASE_PATH'] = $home_info['home_path'];
$cli_param_data['HOME_PATH'] = $home_info['home_path'];
$cli_param_data['SAVE_PATH'] = $home_info['home_path'];
$cli_param_data['OUTPUT_PATH'] = $home_info['home_path'];
$cli_param_data['USER_PATH'] = $home_info['home_path'];
}
}
// Windows
elseif( preg_match("/CYGWIN/", $os) )
{
$home_path_win = $remote->exec("cygpath -w ".$home_info['home_path']);
$home_path_win = str_replace("\\","\\\\", $home_path_win);
$home_path_win = trim($home_path_win);
$cli_param_data['BASE_PATH'] = $home_path_win;
$cli_param_data['HOME_PATH'] = $home_path_win;
$cli_param_data['SAVE_PATH'] = $home_path_win;
$cli_param_data['OUTPUT_PATH'] = $home_path_win;
$cli_param_data['USER_PATH'] = $home_path_win;
}
if ($server_xml->protocol == "gameq")
{
$cli_param_data['QUERY_PORT'] = get_query_port ($server_xml, $port);
}
elseif ($server_xml->protocol == "lgsl")
{
require('protocol/lgsl/lgsl_protocol.php');
$get_ports = lgsl_port_conversion((string)$server_xml->lgsl_query_name, $port, "", "");
$cli_param_data['QUERY_PORT'] = $get_ports['1'];
}
elseif ($server_xml->protocol == "teamspeak3")
{
$cli_param_data['QUERY_PORT'] = "10011";
}
$cli_param_data['MAP'] = ($last_param === NULL or !isset($last_param['map'])) ? "" : $last_param['map'];
$cli_param_data['PLAYERS'] = ($last_param === NULL or !isset($last_param['players'])) ?
isset($home_info['mods'][$mod_id]['max_players']) ?
$home_info['mods'][$mod_id]['max_players'] : "1" : $last_param['players'];
$cli_param_data['CONTROL_PASSWORD'] = $home_info['control_password'];
$start_cmd = "";
// If the template is empty then these are not needed.
if ( $server_xml->cli_template )
{
$start_cmd = $server_xml->cli_template;
if ( $server_xml->cli_params )
{
foreach ( $server_xml->cli_params->cli_param as $cli )
{
// If s is found the param is seperated with space
$add_space = preg_match( "/s/", $cli['options'] ) > 0 ? " " : "";
$cli_value = $cli_param_data[(string) $cli['id'] ];
// If q is found we add quotes around the value.
if ( preg_match( "/q/", $cli['options'] ) > 0 )
{
$cli_value = "\"".$cli_value."\"";
}
$start_cmd = preg_replace( "/%".$cli['id']."%/",
$cli['cli_string'].$add_space.$cli_value, $start_cmd );
}
}
if ( $server_xml->reserve_ports )
{
foreach ( $server_xml->reserve_ports->port as $reserve_port )
{
// If s is found the param is seperated with space
$add_space = preg_match( "/s/", $reserve_port['options'] ) > 0 ? " " : "";
$cli_value = $reserve_port['type'] == "add" ? $port + (string) $reserve_port:
$port - (string) $reserve_port;
// If q is found we add quotes around the value.
if ( preg_match( "/q/", $reserve_port['options'] ) > 0 )
{
$cli_value = "\"".$cli_value."\"";
}
$start_cmd = preg_replace( "/%".$reserve_port['id']."%/",
$reserve_port['cli_string'].$add_space.$cli_value, $start_cmd );
}
}
}
if ( $isAdmin )
{
$home_info['access_rights'] = "ufpet";
}
$param_access_enabled = preg_match("/p/",$home_info['access_rights']) > 0 ? TRUE : FALSE;
if ($param_access_enabled && $last_param !== NULL and isset($server_xml->server_params->param) )
{
foreach($server_xml->server_params->param as $param)
{
foreach ((array)$last_param as $paramKey => $paramValue)
{
if (!isset($paramValue))
$paramValue = (string)$param->default;
if ($param['key'] == $paramKey)
{
if (0 == strlen($paramValue))
continue;
if ($param['key'] == $paramValue) // it's a checkbox
$new_param = $paramKey;
elseif($param->option == "ns" or $param->options == "ns")
$new_param = $paramKey.clean_server_param_value($paramValue, $server_xml->cli_allow_chars);
elseif($param->option == "q" or $param->options == "q")
$new_param = $paramKey . '"' . clean_server_param_value($paramValue, $server_xml->cli_allow_chars) . '"';
elseif($param->option == "s" or $param->options == "s")
$new_param = $paramKey . ' ' . clean_server_param_value($paramValue, $server_xml->cli_allow_chars);
else
$new_param = $paramKey . ' "' . clean_server_param_value($paramValue, $server_xml->cli_allow_chars) . '"';
if ($param['id'] == NULL || $param['id'] == "")
$start_cmd .= ' '.$new_param;
else
$start_cmd = preg_replace( "/%".$param['id']."%/", $new_param, $start_cmd );
}
}
if ($param['id'] != NULL && $param['id'] != ""){
$start_cmd = preg_replace( "/%".$param['id']."%/", '', $start_cmd );
}
}
}
$extra_param_access_enabled = preg_match("/e/",$home_info['access_rights']) > 0 ? TRUE:FALSE;
if ( array_key_exists('extra', (array)$last_param) && $extra_param_access_enabled )
$extra_default = $last_param['extra'];
else
$extra_default = $home_info['mods'][$mod_id]['extra_params'];
$start_cmd .= " ".str_replace("\\\\", "\\", clean_server_param_value($extra_default, $server_xml->cli_allow_chars));
return $start_cmd;
}
// This function is used to batch stop/restart servers in background.
function exec_operation( $action, $home_id, $mod_id, $ip, $port, $override = false )
{
if(!is_numeric($port))
return FALSE;
if(!preg_match("/[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/",$ip))
return FALSE;
global $db;
$isAdmin = false;
if(hasValue($_SESSION) && hasValue($_SESSION['user_id'])){
$isAdmin = $db->isAdmin($_SESSION['user_id']);
}
if($override || $isAdmin)
$home_info = $db->getGameHome($home_id);
else
$home_info = $db->getUserGameHome($_SESSION['user_id'],$home_id);
if( $home_info === FALSE )
return FALSE;
$server_xml = read_server_config(SERVER_CONFIG_LOCATION."/".$home_info['home_cfg_file']);
if ( !$server_xml )
return FALSE;
require_once('includes/lib_remote.php');
$remote = new OGPRemoteLibrary($home_info['agent_ip'],$home_info['agent_port'],$home_info['encryption_key'],$home_info['timeout']);
$os = $remote->what_os();
if ( $action != "stop" )
{
if( $server_xml->replace_texts )
{
$server_home = $home_info;
if( isset($server_xml->lgsl_query_name) )
require_once('protocol/lgsl/lgsl_protocol.php');
require_once("modules/gamemanager/cfg_text_replace.php");
}
}
$screen_running = $remote->is_screen_running(OGP_SCREEN_TYPE_HOME,$home_info['home_id']) === 1;
if ( $action == "stop" AND $screen_running )
{
$remote_retval = $remote->remote_stop_server($home_info['home_id'],
$ip, $port, $server_xml->control_protocol,
$home_info['control_password'],$server_xml->control_protocol_type, $home_info['home_path']);
$db->logger(get_lang_f('server_stopped', $home_info['home_name'] ) . "($ip:$port)");
if ( $remote_retval === -1 )
return FALSE;
elseif( $remote_retval === -2 )
return FALSE;
else
{
$firewall_settings = $db->getFirewallSettings($home_info['remote_server_id']);
if ($firewall_settings['status'] == "enable")
{
$ip_ports = $db->getHomeIpPorts($home_info['home_id']);
if (!is_array($ip_ports)) {
$ip_ports = [];
}
foreach ((array)$ip_ports as $ip_port)
{
if ($server_xml->protocol == "gameq")
{
$query_port = get_query_port($server_xml, $ip_port['port']);
}
elseif ($server_xml->protocol == "lgsl")
{
require('protocol/lgsl/lgsl_protocol.php');
$get_ports = lgsl_port_conversion((string)$server_xml->lgsl_query_name, $ip_port['port'], "", "");
$query_port = $get_ports['1'];
}
elseif ($server_xml->protocol == "teamspeak3")
{
$query_port = "10011";
}
set_firewall($remote, $firewall_settings, 'deny', $ip_port['port'], $ip_port['ip']);
if(isset($query_port) and $query_port != "" and $query_port != $ip_port['port'])
set_firewall($remote, $firewall_settings, 'deny', $query_port, $ip_port['ip']);
}
}
return TRUE;
}
}
elseif ( $action == "restart" AND $screen_running )
{
$start_cmd = get_start_cmd($remote,$server_xml,$home_info,$mod_id,$ip,$port,$db);
// Do text replacements in cfg file
if( $server_xml->replace_texts )
{
if (is_array($home_info['mods'][$mod_id])) {
foreach($home_info['mods'][$mod_id] as $key => $value)
{
$home_info[$key] = $value;
}
}
$server_home = $home_info;
if( isset($server_xml->lgsl_query_name) )
require_once('protocol/lgsl/lgsl_protocol.php');
require_once("modules/gamemanager/cfg_text_replace.php");
}
// Run pre-start commands
if(isset($server_xml->pre_start) && !empty($server_xml->pre_start)){
$preStart = trim($server_xml->pre_start);
}else{
$preStart = "";
}
// Environment variables
if(isset($server_xml->environment_variables) && !empty($server_xml->environment_variables)){
$envVars = trim($server_xml->environment_variables);
}else{
$envVars = "";
}
// Additional files to lock
if(isset($server_xml->lock_files) && !empty($server_xml->lock_files)){
$lockFiles = trim($server_xml->lock_files);
}else{
$lockFiles = "";
}
if(!empty($lockFiles)){
// Linux only call
if(preg_match("/Linux/", $os)){
$lockedFilesStatus = $remote->lock_additional_home_files($home_info['home_path'], $lockFiles, "lock");
}
}
$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 : ""));
$db->logger(get_lang_f('server_restarted', $home_info['home_name']) . "($ip:$port)");
if ( $remote_retval === -1 )
return FALSE;
else if ( $remote_retval === -2 )
return FALSE;
else
{
$ip_id = $db->getIpIdByIp($ip);
$db->delServerStatusCache($ip_id,$port);
}
}
elseif ( $action == "start" AND ! $screen_running )
{
$start_cmd = get_start_cmd($remote,$server_xml,$home_info,$mod_id,$ip,$port,$db);
// Do text replacements in cfg file
if( $server_xml->replace_texts )
{
if (is_array($home_info['mods'][$mod_id])) {
foreach($home_info['mods'][$mod_id] as $key => $value)
{
$home_info[$key] = $value;
}
}
$server_home = $home_info;
if( isset($server_xml->lgsl_query_name) )
require_once('protocol/lgsl/lgsl_protocol.php');
require_once("modules/gamemanager/cfg_text_replace.php");
}
// Run pre-start commands
if(isset($server_xml->pre_start) && !empty($server_xml->pre_start)){
$preStart = trim($server_xml->pre_start);
}else{
$preStart = "";
}
// Environment variables
if(isset($server_xml->environment_variables) && !empty($server_xml->environment_variables)){
$envVars = trim($server_xml->environment_variables);
}else{
$envVars = "";
}
// Additional files to lock
if(isset($server_xml->lock_files) && !empty($server_xml->lock_files)){
$lockFiles = trim($server_xml->lock_files);
}else{
$lockFiles = "";
}
if(!empty($lockFiles)){
// Linux only call
if(preg_match("/Linux/", $os)){
$lockedFilesStatus = $remote->lock_additional_home_files($home_info['home_path'], $lockFiles, "lock");
}
}
$start_retval = $remote->universal_start($home_info['home_id'],
$home_info['home_path'],
$server_xml->server_exec_name,
$server_xml->exe_location,
$start_cmd, $port, $ip,
$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 : "")
);
$db->logger(get_lang('server_started') . " (".$home_info['home_name']." $ip:$port)");
if( $start_retval == AGENT_ERROR_NOT_EXECUTABLE or $start_retval <= 0)
return FALSE;
else
{
$firewall_settings = $db->getFirewallSettings($home_info['remote_server_id']);
if ($firewall_settings['status'] == "enable")
{
$ip_ports = $db->getHomeIpPorts($home_info['home_id']);
if (!is_array($ip_ports)) {
$ip_ports = [];
}
foreach ((array)$ip_ports as $ip_port)
{
if ($server_xml->protocol == "gameq")
{
$query_port = get_query_port($server_xml, $ip_port['port']);
}
elseif ($server_xml->protocol == "lgsl")
{
require('protocol/lgsl/lgsl_protocol.php');
$get_ports = lgsl_port_conversion((string)$server_xml->lgsl_query_name, $ip_port['port'], "", "");
$query_port = $get_ports['1'];
}
elseif ($server_xml->protocol == "teamspeak3")
{
$query_port = "10011";
}
set_firewall($remote, $firewall_settings, 'allow', $ip_port['port'], $ip_port['ip']);
if(isset($query_port) and $query_port != "" and $query_port != $ip_port['port'])
set_firewall($remote, $firewall_settings, 'allow', $query_port, $ip_port['ip']);
}
}
$ip_id = $db->getIpIdByIp($ip);
$db->delServerStatusCache($ip_id,$port);
return TRUE;
}
}
}
function get_monitor_buttons($server_home, $server_xml)
{
global $db;
$buttons = array();
$installed_modules = $db->getInstalledModules();
if (!is_array($installed_modules)) {
$installed_modules = [];
}
foreach ((array)$installed_modules as $installed_module)
{
$buttons_file = "modules/$installed_module[folder]/monitor_buttons.php";
if(file_exists($buttons_file))
{
require($buttons_file);
if (is_array($module_buttons)) {
$buttons = array_merge($buttons, $module_buttons);
}
unset($module_buttons);
}
}
$buttons_html = "";
if (is_array($buttons)) {
foreach ((array)$buttons as $button)
$buttons_html .= $button."\n";
}
return $buttons_html;
}
/**
* Returns an HTML-formatted expiration label for the given server home_id.
*
* Source of truth: billing_orders.end_date (DATETIME, NULL means no date set).
* billing_orders.home_id is the direct FK to server_homes.home_id.
* Only rows where home_id != '0' are considered (home_id = '0' means not yet
* provisioned).
*
* Color thresholds:
* green more than 10 days remaining (shows actual date, YYYY-MM-DD)
* yellow 410 days remaining (shows "X days remaining")
* red 13 days remaining (shows "X days remaining")
* red less than 1 day but not yet expired (shows "Less than 1 day remaining")
* red already expired/suspended (shows "Suspended")
* red no order or NULL/invalid end_date (shows "No expiration date found")
*
* Expiration grace rule: a server whose end_date falls on today is treated as
* active for the full calendar day. The server only shows "Suspended" once
* today's date is strictly after the expiration date (i.e. tomorrow onwards).
*
* @param int $home_id The server home ID.
* @return string Safe HTML string ready for inline display.
*/
function get_server_billing_expiration_html(int $home_id): string
{
global $db;
// Query billing_orders directly by home_id — the canonical FK relationship.
// billing_orders.home_id is VARCHAR(255), so we quote the value in the query.
// We exclude rows where home_id = '0' (not yet provisioned).
//
// Only canonical active statuses are considered:
// 'Active' provisioned and current (set by payment capture or admin creation)
// 'Invoiced' renewal invoice generated; service still running while unpaid
//
// Terminal statuses ('Expired', 'in-cart', 'cancelled', 'refunded') and legacy
// statuses ('installed', 'paid', 'suspended') are not matched here.
// Run normalize_billing_order_status.sql to migrate any legacy rows first.
//
// OGP_DB_PREFIX is replaced at runtime by the panel DB wrapper (str_replace).
$rows = $db->resultQuery(
"SELECT end_date
FROM OGP_DB_PREFIXbilling_orders
WHERE home_id = '" . intval($home_id) . "'
AND home_id != '0'
AND status IN ('Active','Invoiced')
ORDER BY end_date DESC
LIMIT 1"
);
// resultQuery returns FALSE both on error and when 0 rows are found.
// Either way, there is no billing order for this server.
if ($rows === false) {
return "<span style='color:red;'>No expiration date found</span>";
}
// end_date may be NULL (e.g. order created but payment not yet captured).
if (empty($rows[0]['end_date'])) {
return "<span style='color:red;'>No expiration date found</span>";
}
// Parse end_date using DateTime for PHP 8.3+ compatibility.
try {
$end_dt = new DateTime($rows[0]['end_date']);
} catch (\Exception $e) {
return "<span style='color:red;'>No expiration date found</span>";
}
// Compare dates only (strip time) so a server expiring on today's date is
// still shown as active for the full calendar day (grace rule).
$today_midnight = (new DateTime())->setTime(0, 0, 0);
$end_midnight = (clone $end_dt)->setTime(0, 0, 0);
$diff = $today_midnight->diff($end_midnight);
if ($end_midnight < $today_midnight) {
// Expiration date is before today — treat as suspended.
return "<span style='color:red;'>Suspended</span>";
}
// $diff->days is the total number of whole days between today and end date.
$days_remaining = (int)$diff->days;
$display_date = htmlentities($end_dt->format('Y-m-d'), ENT_QUOTES, 'UTF-8');
if ($days_remaining > 10) {
// More than 10 days: show the actual expiration date in green.
return "<span style='color:green;'>" . $display_date . "</span>";
} elseif ($days_remaining >= 4) {
// 410 days: yellow warning (darkgoldenrod for WCAG contrast).
return "<span style='color:#B8860B;'>" . htmlentities($days_remaining, ENT_QUOTES, 'UTF-8') . " days remaining</span>";
} elseif ($days_remaining >= 1) {
// 13 days: urgent red warning.
return "<span style='color:red;'>" . htmlentities($days_remaining, ENT_QUOTES, 'UTF-8') . " days remaining</span>";
} else {
// Less than one full day but not yet expired.
return "<span style='color:red;'>Less than 1 day remaining</span>";
}
}
?>