Merge pull request #151 from GameServerPanel/copilot/harden-repository-layout-update-process

This commit is contained in:
Frank Harris 2026-05-18 09:58:22 -05:00 committed by GitHub
commit 4176bf3bfa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 1635 additions and 1297 deletions

View file

@ -33,7 +33,7 @@ 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. | Sets roles consumed by storefront admin guard and provisioning ACLs. |
| `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. |

View file

@ -1,6 +1,7 @@
# Changelog
## 2026-05-18
- **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.
- **Panel registration stability + captcha fallback hardening:** Fixed a fatal syntax error in `modules/register/register-exec.php`, removed hardcoded/legacy registration redirects, added structured registration logging to `modules/register/logs/register.log` (auto-creates missing log dir), added duplicate username checks, added optional `users_pass_hash` write for PHP 8.3-compatible auth upgrades, and implemented graceful reCAPTCHA fallback when keys are missing/legacy-invalid or the widget reports an error so the themed registration flow no longer crashes with raw PHP errors.

View file

@ -16,3 +16,4 @@
- Add an admin/serverlist UI badge that shows detected service OS variant (Windows/Linux/Any) from XML metadata next to each purchasable service row.
- 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 admin preview/diff panel for Apache path repairs so staff can review exact vhost line changes before confirming `Fix Apache Paths`.

File diff suppressed because it is too large Load diff

View file

@ -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;"
);
?>

View 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;
}

View 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',
];