Merge pull request #151 from GameServerPanel/copilot/harden-repository-layout-update-process
This commit is contained in:
commit
4176bf3bfa
7 changed files with 1635 additions and 1297 deletions
2
.github/module-map.md
vendored
2
.github/module-map.md
vendored
|
|
@ -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. |
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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',
|
||||
];
|
||||
Loading…
Add table
Add a link
Reference in a new issue