site changes by codex

This commit is contained in:
Frank Harris 2025-11-20 08:10:31 -05:00
parent f0b7f96987
commit dc24d43921
34 changed files with 1736 additions and 247 deletions

157
.github/agent.md vendored Normal file
View file

@ -0,0 +1,157 @@
# GSP (GameServerPanel) Copilot Instructions
**Repo of truth:** `GameServerPanel/GSP`, branch `Panel-unstable`.
**Prime directive:** Read this document first. Keep `.github/agent.md` identical to this file—any edit here must be mirrored there in the same commit.
## Deployment model & paths
- `modules/billing/` houses the public storefront. Those files are always present inside the panel repo and get deployed either (a) as the root of a dedicated virtual host or (b) through the panel module loader (`home.php?m=billing`).
- Because the storefront and the control panel live in the same tree, you may include panel helpers when needed. Use the dedicated bridge include (`modules/billing/includes/panel_bridge.php`) instead of sprinkling ad-hoc `../../includes/...` calls.
- We keep Apache/Nginx vhosts pointed at `modules/billing/`, so every storefront URL must look root-relative (see critical section below). Never expose `/modules/billing` in any URL sent to a browser or external service.
- Before touching billing logic or module wiring, skim `.github/module-map.md` to remember how the panel modules depend on each other.
## CRITICAL: Website file paths and URLs (modules/billing)
- **The billing website files in `modules/billing/` are deployed at the WEBSITE ROOT when live.**
- **Never output `/modules/billing/` in any link, redirect, script tag, or webhook URL. All user-facing URLs must be root-relative**, e.g. `/payment_success.php`, `/cart.php`.
- Continue to use root-relative URLs inside HTML/JS and when building PayPal return/cancel links. The deployment tooling rewrites the document root; hardcoding `/modules/billing` breaks both standalone hosting and module embedding.
### Examples of CORRECT usage
```php
$returnUrl = $siteBase . '/payment_success.php';
header('Location: /order.php');
<form action="/add_to_cart.php" method="POST">
<a href="/my_account.php">My Account</a>
```
### Examples of WRONG usage (NEVER DO THIS)
```php
$returnUrl = $siteBase . '/modules/billing/payment_success.php';
header('Location: /modules/billing/cart.php');
<a href="/modules/billing/my_account.php">My Account</a>
```
### Exception backend includes only
- Server-side includes may use absolute filesystem paths, but route those through the bridge helpers when panel context is required:
- ✅ `require_once(__DIR__ . '/includes/config.inc.php');`
- ✅ `require_once(__DIR__ . '/includes/panel_bridge.php');`
- Avoid copy/pasting panel bootstrap code; lean on the helpers already shipped inside `modules/billing/includes/`.
## 1) What to read first
- `.github/module-map.md` living diagram of how the panel, billing site, daemons, and cron jobs talk to each other.
- `modules/billing/` storefront runtime, payment handlers, provisioning bridge.
- `modules/config_games/server_configs/` authoritative XML metadata for every supported game.
- `modules/` control-panel modules (billing runs here too when embedded).
- `includes/` & `ogp_api.php` database layer, shared helpers, remote agent operations.
## 2) Planning mode (default)
While scoping multi-file work, do **not** emit PHP/SQL/XML or run shell commands unless a maintainer explicitly says “Generate code now.” Plans should cover:
- Impacted files and rationale.
- Data mappings (tables/fields) you will touch.
- Risks, rollback notes, validation/tests.
## 3) Scope & principles
- **Single session across panel + storefront.** Every billing page must call `session_name('opengamepanel_web')` before `session_start()`. Always keep `$_SESSION['user_id']`, `$_SESSION['users_login']`, `$_SESSION['users_group']`, and `$_SESSION['website_user_id']` in sync so that logging into either surface signs the visitor into both.
- **Auth reuse.** Preferred order when verifying credentials: `users_pass_hash` (modern hash) → legacy `users_passwd` (MD5). Upgrading to a modern hash is allowed so long as panel logins keep working.
- **Bridge for panel helpers.** Use `modules/billing/includes/panel_bridge.php` to load panel classes (`OGPDatabase`, `OGPRemoteLibrary`, XML parsers) when the storefront needs to provision servers or read panel-only metadata. Do not reinvent ad-hoc copies of panel logic.
- **Storefront runtime.** Public pages continue to use mysqli with credentials from `modules/billing/includes/config.inc.php`. Provisioning steps may request an `OGPDatabase` handle from the bridge.
- **Provisioning pipeline.** Always funnel server creation or renewals through the shared provisioner (`modules/billing/includes/provisioner.php`). This helper wraps the old `create_servers.php` logic and ensures PayPal captures, cron jobs, and panel clicks all follow the same code path.
- **Catalog = XML.** Never hardcode game metadata. Parse `modules/config_games/server_configs/*.xml` at runtime; new XMLs must show up automatically.
- **Regions/Nodes = live DB.** Pull nodes/locations from the panel DB (`gsp_remote_servers`, etc.). Respect admin enable/disable flags and never mirror node lists into flat files.
- **Game XML wiki parity.** We ship a PHP-rendered version of https://github.com/OpenGamePanel/OGP-Website/wiki/XML-Notes inside `modules/billing/` (linked from the storefront admin area). Keep it updated so maintainers can edit XMLs without leaving the repo.
## 4) Functional requirements
### 4.1 Catalog (from XML)
- Parse every XML under `modules/config_games/server_configs/`.
- Normalize: game key, display name, install/update commands, default ports, mod metadata.
- XML pages (`modules/billing/docs.php` and the new XML-notes mirror) must stay in sync so AI-powered edits can cross-reference expectations.
### 4.2 Authentication & sessions
- Website registration must create/maintain panel users. Set both the legacy `users_passwd` and the modern hash column.
- Login flow must hydrate the shared session variables so `home.php` immediately recognizes the visitor.
- All storefront guards should treat `$_SESSION['user_id']` as the source of truth, falling back to `website_user_id` only for older sessions.
### 4.3 Checkout → PayPal → Provisioning
- Flow: add to cart → invoices (`billing_invoices`) → PayPal order (`api/create_order.php`) → capture (`api/capture_order.php`) → immediately hand off to `BillingProvisioner`.
- Mark invoices paid **only** after verifying PayPal response/webhook. Support multiple servers per payment: loop through every paid invoice and either create a new order or extend an existing service.
- For renewals, extend `end_date` from its current value and keep status at `installed`. For new services set status `installing`, invoke the provisioner, then switch to `installed` on success.
- Provisioner is responsible for calling `modules/billing/create_servers.php` logic, adding homes, assigning ports, enabling FTP, and logging/notifications. Never bypass it.
### 4.4 Regions/Nodes (multi-remote)
- `remote_servers` and `remote_server_ips` tables remain the source for available locations. Admin tooling (`adminserverlist.php`) must let staff toggle availability and restrict services per location.
- When a node is globally disabled it must disappear (or show as unavailable) in ordering and admin tools.
### 4.5 Billing automation (website-side)
- Cron/workers under `modules/billing/cron-shop.php` still suspend/delete expired services. Renewals triggered via PayPal must update `billing_orders.status` and `end_date` consistently so cron jobs can pick up where they expect.
- Keep audit logs in `modules/billing/logs/` whenever automatic provisioning, renewals, refunds, or coupon adjustments happen.
## 5) Data model alignment (no DDL during planning)
- Use panel tables as the source of truth (`gsp_billing_orders`, `gsp_billing_services`, `gsp_billing_invoices`, `gsp_game_mods`, etc.).
- Multi-remote fields (`remote_server_id`, IP IDs) already exist—never introduce duplicates in the storefront DB.
- When you truly need schema changes, follow the naming conventions, provide migrations under `modules/billing/*.sql`, and describe the plan first.
## 6) Coding standards & security
- Parameterize SQL or escape inputs with mysqli real_escape-string helpers.
- Harden sessions (regenerate IDs on login, honor `modules/billing/timestamp.txt` for public timestamps).
- CSRF-protect every POST/DELETE-like operation in the storefront admin.
- Verify PayPal signatures, never trust client-side status.
- XML parsing: disable external entities, enforce file size limits.
- Observability: keep per-request IDs in `logs/` to trace provisioning attempts.
- Licensing: leave upstream license headers intact.
## 7) Validation checklist
- Read `.github/module-map.md`, `modules/billing/`, panel helpers, and XMLs before proposing architecture changes.
- Confirm catalog pages only use XML metadata.
- Confirm node selectors reflect current DB state (respect enabled flags).
- Test that logging into either the panel (`index.php`) or storefront (`modules/billing/login.php`) logs you into both.
- PayPal capture should mark invoices paid, create/extend orders, and schedule provisioning instantly. Verify multi-item carts create all services.
- `BillingProvisioner` must be exercised via PayPal capture, panel module (`create_servers.php`), and any admin “retry” buttons.
- Documentation admin links must expose the XML-notes PHP mirror and the game docs browsers.
- Timestamp footer requirement (see below) satisfied whenever site content changes.
## 8) Deliverables for Copilot
- Concise change plan with:
- Files to touch and why.
- Data tables/fields involved.
- UX notes (new buttons, admin affordances, etc.).
- Risks, rollback, and testing.
- Update `CHANGELOG.md` with a short, high-signal entry.
- Append one actionable line to `docs/COPILOT_TODO.md` if UI follow-ups remain.
- Keep `.github/module-map.md` current whenever inter-module behavior changes.
## 9) Prohibited while in planning mode
- No PHP/SQL/XML snippets.
- No shell commands or tooling setup instructions.
- No auto-generated diff dumps.
---
## Additional UI requirement: "Last updated" footer on key pages
When making small content or page edits to the website, ensure the following pages display a human-friendly "Last updated" timestamp at the very bottom of the page (visible to site visitors):
- `modules/billing/index.php`
- `index.php` (site root)
- `modules/dashboard/dashboard.php`
Requirements:
- The text must read exactly: "Last updated at YYYY-MM-DD HH:MM:SS" (24-hour time) where the timestamp reflects the deliberate edit time of the page (see acceptance criteria below).
- Place the timestamp in the page footer area so it does not break layout on mobile or desktop. Keep styling minimal and consistent with the existing footer typography.
- Use the server/local timezone for the timestamp and include the date and time in the format above. Do not include timezone abbreviations in the UI; internal logs may record timezone if needed.
Acceptance criteria:
- Visiting each page shows the "Last updated at" line at the very bottom of the rendered HTML.
- The timestamp matches the time the page's source was last edited (file modification time) or the annotated edit time used by the deployment process. The project maintainer must decide which of these sources is canonical; document the choice in the change plan.
- The line is visible and readable on small screens and does not overlap other UI elements.
Testing checklist:
- Manually open each page and confirm the timestamp is present.
- After making a small edit and deploying, confirm the timestamp updates to the new edit time.
- If using automated deploys, ensure the deploy process preserves or updates the canonical timestamp source (e.g., touch file, update metadata) so the displayed value is accurate.
Maintainer update requirement:
- The canonical human-friendly timestamp is stored in `modules/billing/timestamp.txt` and MUST be updated whenever site files or content are edited and deployed.
- Format and wording: use a single-line plain-text entry such as: "Last Updated at 7:25am on 2025-15-11". This exact text (including capitalization) is what appears in theme footers.
- Update process: include the `timestamp.txt` change in the same commit/PR as any content change that should alter the "Last Updated" time, or ensure your deployment process updates the file automatically (for example, a post-deploy hook that writes the current deploy time in the agreed format).
- Rationale: themes are non-PHP files and may not support SSI on all servers; keeping a single canonical plain-text file reduces duplication and avoids server-side includes.
**End of Copilot Instructions.**

View file

@ -1,150 +1,126 @@
# GSP (GameServerPanel) — Copilot Instructions (No-Code) # GSP (GameServerPanel) Copilot Instructions
**Repo of truth:** `GameServerPanel/GSP`, branch `Panel-unstable`. **Repo of truth:** `GameServerPanel/GSP`, branch `Panel-unstable`.
**Prime directive:** Read this document first. Propose changes that align with our repo and specs. Only search for external info if something contradicts this file. **Prime directive:** Read this document first. Keep `.github/agent.md` identical to this file—any edit here must be mirrored there in the same commit.
## Standalone website mode ## Deployment model & paths
- When working on website features, treat the `_website/` folder as a standalone website root. All website-focused changes (pages, runtime, data persistence, webhooks, and admin UI for the storefront) should live inside `_website/` and be referenced relative to that folder. - `modules/billing/` houses the public storefront. Those files are always present inside the panel repo and get deployed either (a) as the root of a dedicated virtual host or (b) through the panel module loader (`home.php?m=billing`).
- Do NOT modify files outside `_website/` (the panel codebase) unless a maintainer explicitly asks for cross-repo or panel-side changes. If a change necessarily touches panel files, call it out clearly in the plan and get maintainer approval first. - Because the storefront and the control panel live in the same tree, you may include panel helpers when needed. Use the dedicated bridge include (`modules/billing/includes/panel_bridge.php`) instead of sprinkling ad-hoc `../../includes/...` calls.
- All redirects, data directories, and public-facing endpoints implemented for the storefront must be scoped under `_website/` (absolute or root-relative to the `_website` site root), not the panel root or external panel dashboard pages. - We keep Apache/Nginx vhosts pointed at `modules/billing/`, so every storefront URL must look root-relative (see critical section below). Never expose `/modules/billing` in any URL sent to a browser or external service.
- Before touching billing logic or module wiring, skim `.github/module-map.md` to remember how the panel modules depend on each other.
## CRITICAL: Website file paths and URLs (modules/billing) ## CRITICAL: Website file paths and URLs (modules/billing)
- **The billing website files in `modules/billing/` will be deployed at the WEBSITE ROOT when live.** - **The billing website files in `modules/billing/` are deployed at the WEBSITE ROOT when live.**
- **NEVER EVER use `/modules/billing/` in any URL, link, redirect, or file path within the billing website code.** - **Never output `/modules/billing/` in any link, redirect, script tag, or webhook URL. All user-facing URLs must be root-relative**, e.g. `/payment_success.php`, `/cart.php`.
- **All URLs must be root-relative (starting with `/` but NOT including `/modules/billing/`):** - Continue to use root-relative URLs inside HTML/JS and when building PayPal return/cancel links. The deployment tooling rewrites the document root; hardcoding `/modules/billing` breaks both standalone hosting and module embedding.
- ✅ CORRECT: `/payment_success.php`, `/cart.php`, `/order.php`
- ❌ WRONG: `/modules/billing/payment_success.php`, `modules/billing/cart.php`
- **This is a CRITICAL requirement that has been violated multiple times. Read this section carefully before making ANY changes to billing website files.**
### Examples of CORRECT usage: ### Examples of CORRECT usage
```php ```php
// PayPal return URLs
$returnUrl = $siteBase . '/payment_success.php'; $returnUrl = $siteBase . '/payment_success.php';
$cancelUrl = $siteBase . '/payment_cancel.php';
// Header redirects
header('Location: /cart.php');
header('Location: /order.php'); header('Location: /order.php');
// Links
<a href="/my_account.php">My Account</a>
<a href="/serverlist.php">Browse Servers</a>
// Form actions
<form action="/add_to_cart.php" method="POST"> <form action="/add_to_cart.php" method="POST">
<a href="/my_account.php">My Account</a>
``` ```
### Examples of WRONG usage (NEVER DO THIS): ### Examples of WRONG usage (NEVER DO THIS)
```php ```php
// ❌ WRONG - includes modules/billing path
$returnUrl = $siteBase . '/modules/billing/payment_success.php'; $returnUrl = $siteBase . '/modules/billing/payment_success.php';
header('Location: /modules/billing/cart.php'); header('Location: /modules/billing/cart.php');
<a href="/modules/billing/my_account.php">My Account</a> <a href="/modules/billing/my_account.php">My Account</a>
``` ```
### Exception - Backend includes only: ### Exception backend includes only
- Backend PHP includes CAN use `__DIR__` or relative paths for file inclusion: - Server-side includes may use absolute filesystem paths, but route those through the bridge helpers when panel context is required:
- ✅ `require_once(__DIR__ . '/includes/config.inc.php')` - ✅ `require_once(__DIR__ . '/includes/config.inc.php');`
- ✅ `require_once(__DIR__ . '/../../includes/database_mysqli.php')` - ✅ `require_once(__DIR__ . '/includes/panel_bridge.php');`
- But these are for SERVER-SIDE file inclusion, NOT for user-facing URLs/redirects/links. - Avoid copy/pasting panel bootstrap code; lean on the helpers already shipped inside `modules/billing/includes/`.
## 1) What to read first (paths & context) ## 1) What to read first
- `_website/` — canonical website storefront and Checkout/Webhooks flow. - `.github/module-map.md` living diagram of how the panel, billing site, daemons, and cron jobs talk to each other.
- `modules/config_games/server_configs/` — authoritative game catalog XMLs (all supported games live here). - `modules/billing/` storefront runtime, payment handlers, provisioning bridge.
- `modules/` — panel modules (legacy `billing/` exists; its **schema** is authoritative for multi-remote, but the **pages** are deprecated). - `modules/config_games/server_configs/` authoritative XML metadata for every supported game.
- `modules/billing/` — frontend website for selling gameservers to customers. Can interface with panel from same machine or external web host via MySQL tables. Uses `gameservers_website` session namespace (separate from panel sessions). - `modules/` control-panel modules (billing runs here too when embedded).
- `includes/` — panel configuration and DB connectors. - `includes/` & `ogp_api.php` database layer, shared helpers, remote agent operations.
- `ogp_api.php` — internal API entry point for panel-side actions.
- `api/` — Payment-related API code if present in this branch (previously under `paypal/` or `payments/`).
## 2) No-Code Planning Mode (default) ## 2) Planning mode (default)
- Do **not** emit PHP, SQL, XML, or shell commands unless a maintainer explicitly asks: **“Generate code now.”** While scoping multi-file work, do **not** emit PHP/SQL/XML or run shell commands unless a maintainer explicitly says “Generate code now.” Plans should cover:
- While in planning mode, produce only: - Impacted files and rationale.
- Impacted paths and files, - Data mappings (tables/fields) you will touch.
- Step-by-step plans with acceptance criteria, - Risks, rollback notes, validation/tests.
- Risks, rollbacks, and test/validation checklists,
- Data mappings that reference existing tables/fields.
## 3) Scope & principles ## 3) Scope & principles
- **Website ↔ Panel on the same host.** Website uses the **panel DB for authentication** and the **panel's internal APIs** for provisioning. **Sessions remain separate** (website session ≠ panel session). - **Single session across panel + storefront.** Every billing page must call `session_name('opengamepanel_web')` before `session_start()`. Always keep `$_SESSION['user_id']`, `$_SESSION['users_login']`, `$_SESSION['users_group']`, and `$_SESSION['website_user_id']` in sync so that logging into either surface signs the visitor into both.
- **Billing module is STANDALONE AND RELOCATABLE.** The `modules/billing/` directory is a **complete standalone website** that: - **Auth reuse.** Preferred order when verifying credentials: `users_pass_hash` (modern hash) → legacy `users_passwd` (MD5). Upgrading to a modern hash is allowed so long as panel logins keep working.
- Can be deployed on the **same machine as the panel** OR on a **completely separate external web host** - **Bridge for panel helpers.** Use `modules/billing/includes/panel_bridge.php` to load panel classes (`OGPDatabase`, `OGPRemoteLibrary`, XML parsers) when the storefront needs to provision servers or read panel-only metadata. Do not reinvent ad-hoc copies of panel logic.
- Must **NEVER** use `require_once` to include panel files (like `includes/database_mysqli.php`, `includes/functions.php`, or any panel helper files) - **Storefront runtime.** Public pages continue to use mysqli with credentials from `modules/billing/includes/config.inc.php`. Provisioning steps may request an `OGPDatabase` handle from the bridge.
- Must use **ONLY standard PHP libraries** (mysqli, json, curl, session, etc.) - **Provisioning pipeline.** Always funnel server creation or renewals through the shared provisioner (`modules/billing/includes/provisioner.php`). This helper wraps the old `create_servers.php` logic and ensures PayPal captures, cron jobs, and panel clicks all follow the same code path.
- Connects directly to MySQL using `mysqli_connect()` with credentials from `modules/billing/includes/config.inc.php` - **Catalog = XML.** Never hardcode game metadata. Parse `modules/config_games/server_configs/*.xml` at runtime; new XMLs must show up automatically.
- All database operations use native mysqli functions: `mysqli_query()`, `mysqli_real_escape_string()`, `mysqli_fetch_assoc()`, etc. - **Regions/Nodes = live DB.** Pull nodes/locations from the panel DB (`gsp_remote_servers`, etc.). Respect admin enable/disable flags and never mirror node lists into flat files.
- Must **NOT** use panel-specific functions like `$db->query()`, `createDatabaseConnection()`, `get_lang()`, etc. - **Game XML wiki parity.** We ship a PHP-rendered version of https://github.com/OpenGamePanel/OGP-Website/wiki/XML-Notes inside `modules/billing/` (linked from the storefront admin area). Keep it updated so maintainers can edit XMLs without leaving the repo.
- All file paths for includes use `__DIR__` relative paths (e.g., `require_once(__DIR__ . '/includes/config.inc.php')`)
- All URLs/redirects/links use root-relative paths WITHOUT `/modules/billing/` prefix (see CRITICAL section above)
- **Catalog = XML.** Enable **every game** present under `modules/config_games/server_configs/`. The website reads those XMLs for ports, params, install/update metadata. New XMLs should become available without code changes.
- **Regions/Nodes = panel DB.** Regions and nodes are configured in the panel and must be **queried live** from the panel DB. Never hardcode or mirror region lists on the website.
- **Slotless model.** Pricing/UX must not enforce slot caps. If an engine requires a player count parameter, set a safe high default and surface engine limits transparently if they exist.
- **Auth compatibility.** Panel users use legacy MD5 in `ogp_users`. The website should prefer a modern hashing shadow and upgrade transparently on successful login, **without breaking panel login**.
- **Checkout/Webhooks.** Follow the working **PayPal Checkout** flow in `_website/`. Use **REST Webhooks** only. Mark orders paid **only** after webhook verification.
- **Legacy billing module.** Treat `modules/billing/` **pages** as deprecated. Reuse the **existing tables/fields** introduced there for **multi-remote** support. Do not invent parallel schema.
## 4) Functional requirements (design-level only) ## 4) Functional requirements
### 4.1 Catalog (from XML) ### 4.1 Catalog (from XML)
- Parse all XMLs under `modules/config_games/server_configs/`. - Parse every XML under `modules/config_games/server_configs/`.
- Normalize game key, display name, required ports, startup parameters, install/update routines, and any engine constraints. - Normalize: game key, display name, install/update commands, default ports, mod metadata.
- Support hot-add: new XMLs become available to the storefront after a repo update. - XML pages (`modules/billing/docs.php` and the new XML-notes mirror) must stay in sync so AI-powered edits can cross-reference expectations.
### 4.2 Authentication & sessions ### 4.2 Authentication & sessions
- Website registration creates a panel user (legacy-compatible) and stores a **modern hash shadow** linked 1:1 to that user. - Website registration must create/maintain panel users. Set both the legacy `users_passwd` and the modern hash column.
- Login prefers the modern hash; on MD5 success, upgrade silently to the modern hash. - Login flow must hydrate the shared session variables so `home.php` immediately recognizes the visitor.
- Maintain **separate sessions** for website and panel. - All storefront guards should treat `$_SESSION['user_id']` as the source of truth, falling back to `website_user_id` only for older sessions.
### 4.3 Checkout → Webhooks → Provisioning ### 4.3 Checkout → PayPal → Provisioning
- Mirror `_website/` structure and flows for Checkout. - Flow: add to cart → invoices (`billing_invoices`) → PayPal order (`api/create_order.php`) → capture (`api/capture_order.php`) → immediately hand off to `BillingProvisioner`.
- On verified webhook events: transition order state to paid, create service records, and **provision** a panel Home using internal panel APIs. - Mark invoices paid **only** after verifying PayPal response/webhook. Support multiple servers per payment: loop through every paid invoice and either create a new order or extend an existing service.
- Derive ports and startup parameters **from the XML metadata**. - For renewals, extend `end_date` from its current value and keep status at `installed`. For new services set status `installing`, invoke the provisioner, then switch to `installed` on success.
- Provisioner is responsible for calling `modules/billing/create_servers.php` logic, adding homes, assigning ports, enabling FTP, and logging/notifications. Never bypass it.
### 4.4 Regions/Nodes (multi-remote) ### 4.4 Regions/Nodes (multi-remote)
- At checkout or during provisioning, present or auto-select regions/nodes by reading **the panel DB**. - `remote_servers` and `remote_server_ips` tables remain the source for available locations. Admin tooling (`adminserverlist.php`) must let staff toggle availability and restrict services per location.
- If a node is hidden/disabled in the panel, it must not appear in the website UI. - When a node is globally disabled it must disappear (or show as unavailable) in ordering and admin tools.
### 4.5 Billing automation (website-side) ### 4.5 Billing automation (website-side)
- Reconcile renewals and invoke panel APIs to suspend/reactivate/terminate services. - Cron/workers under `modules/billing/cron-shop.php` still suspend/delete expired services. Renewals triggered via PayPal must update `billing_orders.status` and `end_date` consistently so cron jobs can pick up where they expect.
- Operations must be idempotent and observable (logs/metrics defined at design time). - Keep audit logs in `modules/billing/logs/` whenever automatic provisioning, renewals, refunds, or coupon adjustments happen.
## 5) Data model alignment (no DDL) ## 5) Data model alignment (no DDL during planning)
- Use the **panel DB as the source of truth**. - Use panel tables as the source of truth (`gsp_billing_orders`, `gsp_billing_services`, `gsp_billing_invoices`, `gsp_game_mods`, etc.).
- **Multi-remote** tables and fields already exist (introduced by the legacy billing work). Reuse them. - Multi-remote fields (`remote_server_id`, IP IDs) already exist—never introduce duplicates in the storefront DB.
- Only propose new fields/tables if strictly necessary; when doing so, reference existing naming conventions and provide a migration plan (still no SQL while in planning mode). - When you truly need schema changes, follow the naming conventions, provide migrations under `modules/billing/*.sql`, and describe the plan first.
## 6) Coding standards & security (what to enforce when code is requested) ## 6) Coding standards & security
- **Repository-first:** Before proposing file names, endpoints, or structures, search `Panel-unstable` to reuse existing helpers, patterns, and locations. - Parameterize SQL or escape inputs with mysqli real_escape-string helpers.
- **Strictness:** Prefer strict comparisons; parameterized DB access; centralized input validation and output escaping. - Harden sessions (regenerate IDs on login, honor `modules/billing/timestamp.txt` for public timestamps).
- **Session & CSRF:** Harden website sessions and require CSRF tokens on state-changing requests. - CSRF-protect every POST/DELETE-like operation in the storefront admin.
- **Webhooks:** Verify signatures and event types server-side; never trust client redirects for payment state. - Verify PayPal signatures, never trust client-side status.
- **XML:** Harden parsing (no external entities; size/complexity limits). Treat XML as untrusted input even though its in-repo. - XML parsing: disable external entities, enforce file size limits.
- **Observability:** Define success/failure metrics, audit logs for state changes, and trace IDs for provisioning flows. - Observability: keep per-request IDs in `logs/` to trace provisioning attempts.
- **Licensing:** Preserve upstream notices and ensure our additions stay license-compatible. - Licensing: leave upstream license headers intact.
## 7) Validation checklist (pre-PR / pre-merge) ## 7) Validation checklist
- Read `_website/`, `modules/config_games/server_configs/`, `modules/`, `includes/`, `api/` (if present), and `ogp_api.php` to anchor proposals to actual code. - Read `.github/module-map.md`, `modules/billing/`, panel helpers, and XMLs before proposing architecture changes.
- Catalog uses only the XML metadata; no hardcoded ports/params. - Confirm catalog pages only use XML metadata.
- Regions/nodes are read live from the panel DB; no duplicates on the website. - Confirm node selectors reflect current DB state (respect enabled flags).
- Auth plan preserves panel compatibility and modernizes website hashing; **sessions remain separate**. - Test that logging into either the panel (`index.php`) or storefront (`modules/billing/login.php`) logs you into both.
- Checkout mirrors `_website/`; uses **REST Webhooks**; paid state changes occur only after verification. - PayPal capture should mark invoices paid, create/extend orders, and schedule provisioning instantly. Verify multi-item carts create all services.
- Provisioning calls panel internals (e.g., `ogp_api.php`), respects selected/auto node, and records mappings consistently. - `BillingProvisioner` must be exercised via PayPal capture, panel module (`create_servers.php`), and any admin “retry” buttons.
- Legacy billing module pages are not extended; its schema is reused for multi-remote. - Documentation admin links must expose the XML-notes PHP mirror and the game docs browsers.
- Security items from §6 are addressed in the plan: CSRF, webhook verification, strict comparisons, hardened XML. - Timestamp footer requirement (see below) satisfied whenever site content changes.
## 8) Deliverables for Copilot (when planning) ## 8) Deliverables for Copilot
- A concise change plan that lists: - Concise change plan with:
- Files to create/modify/remove and their locations, - Files to touch and why.
- Data sources and mappings to existing tables/fields, - Data tables/fields involved.
- UX notes (e.g., region selector vs auto-placement), - UX notes (new buttons, admin affordances, etc.).
- Risks, rollback approach, and test coverage, - Risks, rollback, and testing.
- Acceptance criteria aligned to these instructions. - Update `CHANGELOG.md` with a short, high-signal entry.
- Update `CHANGELOG.md` with a brief, high-signal entry (date, scope, rationale). - Append one actionable line to `docs/COPILOT_TODO.md` if UI follow-ups remain.
- Append a single line item to `docs/COPILOT_TODO.md` for any UI follow-ups or next steps. - Keep `.github/module-map.md` current whenever inter-module behavior changes.
## 9) Prohibited while in planning ## 9) Prohibited while in planning mode
- No PHP/SQL/XML. - No PHP/SQL/XML snippets.
- No shell commands or system setup steps. - No shell commands or tooling setup instructions.
- No scaffolding diffs or auto-generated file dumps. - No auto-generated diff dumps.
--- ---
@ -178,4 +154,4 @@ Maintainer update requirement:
- Rationale: themes are non-PHP files and may not support SSI on all servers; keeping a single canonical plain-text file reduces duplication and avoids server-side includes. - Rationale: themes are non-PHP files and may not support SSI on all servers; keeping a single canonical plain-text file reduces duplication and avoids server-side includes.
**End of Copilot Instructions (No-Code).** **End of Copilot Instructions.**

78
.github/module-map.md vendored Normal file
View file

@ -0,0 +1,78 @@
# GSP Module & Interaction Map
This file captures how the control panel, storefront, agents, and helper scripts talk to one another. Read it before diving into any subsystem—most regressions last time came from touching one module without realizing who consumed its data.
## Core runtime (shared by every module)
| Area | Key files | Responsibilities | Downstream callers |
| --- | --- | --- | --- |
| Database bootstrap | `includes/functions.php`, `includes/database_mysqli.php` | Creates the `OGPDatabase` instance and exposes helpers such as `resultQuery()`, `addGameHome()`, and logging. | Every panel page, `modules/billing/includes/panel_bridge.php`, cron jobs. |
| Session helpers | `includes/helpers.php` (`startSession()`) | Sets `session_name('opengamepanel_web')`, sanitizes request vars, loads locales. | `index.php`, `home.php`, provisioning pages, storefront session bridge. |
| Remote control | `includes/lib_remote.php` | Wraps agent RPC (install/update, FTP user management, rsync, SteamCMD). | `modules/gamemanager/*`, `modules/billing/create_servers.php`, cron jobs. |
| XML parser | `modules/config_games/server_config_parser.php` | Converts `modules/config_games/server_configs/*.xml` into PHP arrays used for provisioning and pricing metadata. | `modules/gamemanager`, `modules/billing` (catalog + provisioner), cron installers. |
| API surface | `ogp_api.php`, `includes/api_functions.php` | HTTP API for third-party tooling. Exposes operations such as starting/stopping homes, querying stats. | Mobile apps, automated provisioning, selected billing workflows. |
| Cron/automation | `scripts/` (`cron-shop.php`, `status/*`, etc.) | Suspends/unsuspends services, refreshes status caches, runs backups. | Triggered via system cron or panel scheduler. |
## High-level flows
1. **Auth/session** Driven by `index.php` (panel) and `modules/billing/login.php` (storefront). Both set `$_SESSION['user_id']`, `users_login`, `users_group`, and `website_user_id`. The shared session cookie `opengamepanel_web` means logging into either surface immediately authenticates the other.
2. **Catalog** `modules/config_games` hosts XML definitions. Panel modules (`gamemanager`, `config_games`) and storefront pages (`serverlist.php`, `order.php`, documentation pages, and the XML-notes mirror) parse these files for display and provisioning metadata.
3. **Provisioning** Orders land in `gsp_billing_orders`. `modules/billing/includes/provisioner.php` reuses `modules/billing/create_servers.php` logic to allocate homes, assign nodes/IPs, configure mods, and kick off SteamCMD/rsync/manual installers. The same provisioner is invoked by:
- PayPal capture endpoint (`modules/billing/api/capture_order.php`).
- Panel module page `home.php?m=billing&p=provision_servers`.
- Cron/repair actions in `modules/billing/cron-shop.php`.
4. **Renewals** `cron-shop.php` inspects `billing_orders.end_date` and toggles `status` between `installed`, `invoiced`, `suspended`, and `deleted`. PayPal renewals extend `end_date` in `capture_order.php` and immediately flip `status` back to `installed`.
5. **Documentation** `modules/billing/docs.php`, per-game folders under `modules/billing/docs/`, and the XML wiki mirror (PHP port of `XML-Notes`) are used by both admins and AI helpers to craft game templates.
## Panel modules (selected)
| Module | Key files | Primary responsibilities | Upstream/Downstream dependencies |
| --- | --- | --- | --- |
| `dashboard` | `modules/dashboard/dashboard.php` | Landing page once authenticated. Pulls stats from homes, invoices, and support modules. Shows "Last updated" footer based on `modules/billing/timestamp.txt`. | Reads `billing_orders`, `game_homes`, `tickets`. |
| `gamemanager` | `modules/gamemanager/server_monitor.php`, `modules/gamemanager/game_monitor.php` | Shows owned homes, start/stop, update, reinstall, port usage. Uses XML to know command lines. | Relies on `lib_remote`, `config_games`, `user_games` assignments. |
| `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. |
| `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. |
| `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. |
## Storefront (modules/billing)
| Area | Key files | Notes |
| --- | --- | --- |
| Public pages | `index.php`, `serverlist.php`, `order.php`, `cart.php`, `payment_success.php`, `docs.php` | All include `bootstrap.php`, header/footer, shared CSS. Links remain root-relative. |
| Auth | `login.php`, `register.php`, `reset_password.php`, `forgot_password.php`, `includes/login_required.php`, `includes/admin_auth.php` | Share `opengamepanel_web` session, call into panel DB to validate roles. |
| Admin | `admin.php`, `adminserverlist.php`, `admin_orders.php`, `admin_coupons.php`, `admin_config.php`, `my_orders_panel.php` | Manage services, coupons, prices, and provisioning. `adminserverlist.php` controls service availability per node. |
| PayPal API | `api/create_order.php`, `api/capture_order.php`, `webhook.php`, `logs/payment_capture.log` | Implements REST checkout. Once capture is confirmed, writes invoices/orders, updates coupons, and kicks `BillingProvisioner`. |
| Provisioning bridge | `create_servers.php`, `includes/provisioner.php`, `includes/panel_bridge.php` | Shared between panel module and storefront backend. Encapsulates whole server creation/renewal pipeline. |
| Cron helpers | `cron-shop.php`, `diag_remote.php` | Automations for renewals, diagnostics, health checks. |
| Documentation | `docs.php`, `docs/*`, `docs/admin_xml_notes.php` (PHP mirror of XML wiki) | Provide guidance for editing XML and game configs directly inside repo. |
| Logs/data | `logs/`, `data/`, `timestamp.txt` | Payment JSON archives, debug traces, and "Last updated" canonical string. |
## External/agent side
| Component | Location | Purpose |
| --- | --- | --- |
| Remote agent | `modules/gamemanager` talks to standalone agent binaries configured per `remote_servers`. | Executes installs, updates, start/stop commands. Provisioner relies on it for SteamCMD and rsync workflows. |
| Apache/Nginx vhosts | `/etc/apache2/sites-available` (not in repo) | Point either the storefront domain or panel subpath at `modules/billing/`. Required for shared session cookie scope. |
## Data touchpoints
- **Users** `gsp_users` table is shared. Registration uses `modules/billing/register.php`, admin pages use `modules/user_admin`. Password upgrades must not break panel logins.
- **Billing tables** `gsp_billing_services`, `gsp_billing_orders`, `gsp_billing_invoices`, `gsp_billing_coupons`. Admin edits (pricing, enable/disable, locations) are done via `adminserverlist.php`; automation uses `cron-shop.php`.
- **Homes/Mods/IPs** Stored in `gsp_game_homes`, `gsp_game_mods`, `gsp_remote_server_ips`. Provisioner writes to these tables; `gamemanager`, `litefm`, `ftp`, and `user_games` read them.
- **Logging** `$db->logger()` writes to `ogp_logs`. Storefront-specific logs live in `modules/billing/logs/` for quick inspection (payment capture, provisioning outcomes, coupon usage).
## Usage tips
1. **Need a DB object inside `modules/billing`?** Include `includes/panel_bridge.php` and call `billing_get_panel_db()`. It sets up constants, loads helpers, and caches the `OGPDatabase` instance so multi-call flows (e.g., capture → provision → email) reuse it.
2. **Want to change provisioning?** Update `modules/billing/includes/provisioner.php` once. `create_servers.php`, PayPal webhooks, cron jobs, and admin repair flows all use it.
3. **Working on XML or documentation?** Update the XML file under `modules/config_games/server_configs/`, regenerate docs if needed, and keep the PHP XML-notes mirror (`modules/billing/docs/xml_notes.php`) accurate so the admin link stays trustworthy.
4. **Need to know who uses a table?** Search `.github/module-map.md` first; the table above lists the canonical readers/writers for each major schema.
_Last updated: 2025-11-20._

View file

@ -205,7 +205,7 @@ The billing module is designed to be standalone and relocatable:
- Does NOT include panel files (like includes/functions.php) - Does NOT include panel files (like includes/functions.php)
- Connects directly to MySQL using mysqli_connect() - Connects directly to MySQL using mysqli_connect()
- Can be deployed on same machine as panel OR external web host - Can be deployed on same machine as panel OR external web host
- Sessions are separate: "gameservers_website" namespace - Sessions are separate: "opengamepanel_web" namespace
--- ---
@ -245,3 +245,4 @@ The billing module is now functional with:
4. All files validated for syntax correctness 4. All files validated for syntax correctness
The changes are minimal, surgical, and follow the repository guidelines for standalone billing module architecture. The changes are minimal, surgical, and follow the repository guidelines for standalone billing module architecture.

View file

@ -151,7 +151,7 @@ When game documentation is finished:
## Technical Notes ## Technical Notes
### Session Management ### Session Management
- **CRITICAL:** Always use `session_name("gameservers_website")` before `session_start()` - **CRITICAL:** Always use `session_name("opengamepanel_web")` before `session_start()`
- Sessions are separate from panel sessions - Sessions are separate from panel sessions
- User authentication stored in `$_SESSION['website_user_id']` - User authentication stored in `$_SESSION['website_user_id']`
@ -175,3 +175,4 @@ When game documentation is finished:
--- ---
**Last Updated:** December 19, 2024 **Last Updated:** December 19, 2024
**Version:** 2.0 (with Visual TODO System) **Version:** 2.0 (with Visual TODO System)

View file

@ -65,9 +65,9 @@ Implemented comprehensive system to visually identify incomplete game documentat
### 1. PayPal Payment Capture Session Issue (FIXED) ### 1. PayPal Payment Capture Session Issue (FIXED)
**Problem:** Payment capture was failing with `NO_USER_SESSION` error even though user was logged in. **Problem:** Payment capture was failing with `NO_USER_SESSION` error even though user was logged in.
**Root Cause:** The `api/capture_order.php` file was calling `session_start()` without setting the session name first, so it couldn't access the `gameservers_website` session where the user_id is stored. **Root Cause:** The `api/capture_order.php` file was calling `session_start()` without setting the session name first, so it couldn't access the `opengamepanel_web` session where the user_id is stored.
**Solution:** Added `session_name("gameservers_website")` before `session_start()` in `capture_order.php`. **Solution:** Added `session_name("opengamepanel_web")` before `session_start()` in `capture_order.php`.
**File Modified:** `modules/billing/api/capture_order.php` (line ~148) **File Modified:** `modules/billing/api/capture_order.php` (line ~148)
@ -323,3 +323,4 @@ Each game's `index.php` should follow this structure:
--- ---
**End of Summary** **End of Summary**

View file

@ -9,7 +9,7 @@ Successfully implemented login functionality for the website (_website/) that au
Full-featured login page with: Full-featured login page with:
- Modern, responsive UI design - Modern, responsive UI design
- Authentication against panel DB using MD5 (panel-compatible) - Authentication against panel DB using MD5 (panel-compatible)
- Separate website session: `gameservers_website` - Separate website session: `opengamepanel_web`
- Input validation and sanitization - Input validation and sanitization
- Error and success message display - Error and success message display
- Automatic redirect after successful login - Automatic redirect after successful login
@ -71,7 +71,7 @@ Database testing utility that checks:
## Technical Details ## Technical Details
### Session Management ### Session Management
- **Website Session Name:** `gameservers_website` - **Website Session Name:** `opengamepanel_web`
- **Panel Session Name:** `opengamepanel_web` (unchanged) - **Panel Session Name:** `opengamepanel_web` (unchanged)
- **Complete separation:** Users can be logged into one without the other - **Complete separation:** Users can be logged into one without the other
@ -178,3 +178,4 @@ All requirements from the problem statement have been met:
✅ Authenticate against panel DB ✅ Authenticate against panel DB
✅ Create separate login session ✅ Create separate login session
✅ Maintain panel compatibility ✅ Maintain panel compatibility

View file

@ -8,7 +8,7 @@ This implementation adds login functionality to the website that authenticates u
### 1. `_website/login.php` (NEW) ### 1. `_website/login.php` (NEW)
- Full-featured login page with modern UI - Full-featured login page with modern UI
- Authenticates against panel DB using MD5 password hashing (panel-compatible) - Authenticates against panel DB using MD5 password hashing (panel-compatible)
- Creates separate website session using `gameservers_website` session name - Creates separate website session using `opengamepanel_web` session name
- Logs all login attempts via logger() function - Logs all login attempts via logger() function
- Session variables set: - Session variables set:
- `$_SESSION['website_user_id']` - User ID from ogp_users - `$_SESSION['website_user_id']` - User ID from ogp_users
@ -32,7 +32,7 @@ This implementation adds login functionality to the website that authenticates u
## Session Management ## Session Management
### Separate Sessions ### Separate Sessions
- **Website Session**: `gameservers_website` (this implementation) - **Website Session**: `opengamepanel_web` (this implementation)
- **Panel Session**: `opengamepanel_web` (existing panel) - **Panel Session**: `opengamepanel_web` (existing panel)
These sessions are completely separate - users can be logged into one without being logged into the other. These sessions are completely separate - users can be logged into one without being logged into the other.
@ -62,7 +62,7 @@ Requires connection to panel database with access to:
### For Developers: ### For Developers:
Check if user is logged in: Check if user is logged in:
```php ```php
session_name("gameservers_website"); session_name("opengamepanel_web");
session_start(); session_start();
if (isset($_SESSION['website_user_id']) && !empty($_SESSION['website_user_id'])) { if (isset($_SESSION['website_user_id']) && !empty($_SESSION['website_user_id'])) {
@ -107,3 +107,4 @@ This implementation follows the no-code planning guidelines from `.github/copilo
- Login credentials are the same as panel login (same user table) - Login credentials are the same as panel login (same user table)
- Website session does not grant access to panel - separate login required - Website session does not grant access to panel - separate login required
- Logger function from db.php creates logfile.txt for audit trail - Logger function from db.php creates logfile.txt for audit trail

View file

@ -26,6 +26,7 @@ function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }
<a class="gsw-btn" href="./invoices.php">Invoice History</a> <a class="gsw-btn" href="./invoices.php">Invoice History</a>
<a class="gsw-btn" href="admin_coupons.php">Manage Coupons</a> <a class="gsw-btn" href="admin_coupons.php">Manage Coupons</a>
<a class="gsw-btn" href="admin_config.php">Edit Site Config</a> <a class="gsw-btn" href="admin_config.php">Edit Site Config</a>
<a class="gsw-btn" href="docs/xml_notes.php">XML Config Guide</a>
</div> </div>
<hr> <hr>

View file

@ -13,6 +13,7 @@
// Include billing bootstrap (loads config and DB helper) // Include billing bootstrap (loads config and DB helper)
require_once(__DIR__ . '/bootstrap.php'); require_once(__DIR__ . '/bootstrap.php');
$siteBaseUrl = isset($SITE_BASE_URL) ? trim((string)$SITE_BASE_URL) : '';
// Protect this page: require admin // Protect this page: require admin
require_once(__DIR__ . '/includes/admin_auth.php'); require_once(__DIR__ . '/includes/admin_auth.php');
@ -27,6 +28,10 @@ if (!$db) {
include(__DIR__ . '/includes/top.php'); include(__DIR__ . '/includes/top.php');
include(__DIR__ . '/includes/menu.php'); include(__DIR__ . '/includes/menu.php');
echo "<div class='panel mb-12'><strong>Need the XML field reference?</strong> ";
echo "<a href=\"/modules/billing/docs/xml_notes.php\" target=\"_blank\" rel=\"noopener\">Open XML Notes</a>";
echo "</div>";
/* show errors during setup */ /* show errors during setup */
@ini_set('display_errors','1'); @ini_set('display_errors','1');
error_reporting(E_ALL); error_reporting(E_ALL);
@ -79,7 +84,12 @@ if (isset($_POST['update_remote_servers'])) {
/* helper: update one service row from posted array */ /* helper: update one service row from posted array */
function update_service_row(mysqli $db, string $locationCol, int $sid, array $svc){ function update_service_row(mysqli $db, string $locationCol, int $sid, array $svc){
$name = esc_mysqli($db, trim($svc['service_name'] ?? '')); $name = esc_mysqli($db, trim($svc['service_name'] ?? ''));
$price = esc_mysqli($db, trim($svc['price_monthly'] ?? '0.00')); $priceMonthly = number_format((float)($svc['price_monthly'] ?? 0), 2, '.', '');
$priceYearly = number_format((float)($svc['price_year'] ?? 0), 2, '.', '');
$priceDaily = number_format((float)($svc['price_daily'] ?? 0), 2, '.', '');
$priceMonthEsc = esc_mysqli($db, $priceMonthly);
$priceYearEsc = esc_mysqli($db, $priceYearly);
$priceDailyEsc = esc_mysqli($db, $priceDaily);
$img = esc_mysqli($db, trim($svc['img_url'] ?? '')); $img = esc_mysqli($db, trim($svc['img_url'] ?? ''));
$en = !empty($svc['enabled']) ? 1 : 0; $en = !empty($svc['enabled']) ? 1 : 0;
@ -104,7 +114,9 @@ function update_service_row(mysqli $db, string $locationCol, int $sid, array $sv
`{$locationCol}`='{$locListEsc}', `{$locationCol}`='{$locListEsc}',
slot_min_qty={$minSlots}, slot_min_qty={$minSlots},
slot_max_qty={$maxSlots}, slot_max_qty={$maxSlots},
price_monthly='{$price}', price_daily='{$priceDailyEsc}',
price_monthly='{$priceMonthEsc}',
price_year='{$priceYearEsc}',
img_url='{$img}', img_url='{$img}',
enabled={$en} enabled={$en}
WHERE service_id={$sid}"; WHERE service_id={$sid}";
@ -178,7 +190,9 @@ $services = fetch_all_assoc($db, "SELECT service_id, service_name, `{$locat
<th>Service Name <small class="muted">(ID below)</small></th> <th>Service Name <small class="muted">(ID below)</small></th>
<th>Min Slots</th> <th>Min Slots</th>
<th>Max Slots</th> <th>Max Slots</th>
<th>Price (Daily)</th>
<th>Price (Monthly)</th> <th>Price (Monthly)</th>
<th>Price (Year)</th>
<th>Thumbnail URL</th> <th>Thumbnail URL</th>
<th>Preview</th> <th>Preview</th>
<th>Update Row</th> <th>Update Row</th>
@ -196,8 +210,8 @@ $services = fetch_all_assoc($db, "SELECT service_id, service_name, `{$locat
if ($imgUrl !== '') { if ($imgUrl !== '') {
if (is_abs_url($imgUrl)) { if (is_abs_url($imgUrl)) {
$displayUrl = $imgUrl; $displayUrl = $imgUrl;
} elseif ($SITE_BASE_URL !== '') { } elseif ($siteBaseUrl !== '') {
$displayUrl = join_base($SITE_BASE_URL, $imgUrl); $displayUrl = join_base($siteBaseUrl, $imgUrl);
} else { } else {
// Use relative path (local folder) // Use relative path (local folder)
$displayUrl = $imgUrl; $displayUrl = $imgUrl;
@ -227,10 +241,18 @@ $services = fetch_all_assoc($db, "SELECT service_id, service_name, `{$locat
<input type="number" name="service[<?php echo $sid; ?>][slot_max_qty]" value="<?php echo (int)$row['slot_max_qty']; ?>" min="1" step="1" class="w-90"> <input type="number" name="service[<?php echo $sid; ?>][slot_max_qty]" value="<?php echo (int)$row['slot_max_qty']; ?>" min="1" step="1" class="w-90">
</td> </td>
<td>
<input type="text" name="service[<?php echo $sid; ?>][price_daily]" value="<?php echo h(number_format((float)$row['price_daily'], 2, '.', '')); ?>" size="8">
</td>
<td> <td>
<input type="text" name="service[<?php echo $sid; ?>][price_monthly]" value="<?php echo h($row['price_monthly']); ?>" size="8"> <input type="text" name="service[<?php echo $sid; ?>][price_monthly]" value="<?php echo h($row['price_monthly']); ?>" size="8">
</td> </td>
<td>
<input type="text" name="service[<?php echo $sid; ?>][price_year]" value="<?php echo h(number_format((float)$row['price_year'], 2, '.', '')); ?>" size="8">
</td>
<!-- Thumbnail URL input --> <!-- Thumbnail URL input -->
<td> <td>
<input type="text" name="service[<?php echo $sid; ?>][img_url]" value="<?php echo h($row['img_url']); ?>" class="min-w-240"> <input type="text" name="service[<?php echo $sid; ?>][img_url]" value="<?php echo h($row['img_url']); ?>" class="min-w-240">
@ -305,6 +327,17 @@ $services = fetch_all_assoc($db, "SELECT service_id, service_name, `{$locat
</form> </form>
<?php endif; ?> <?php endif; ?>
<div class="panel" style="margin-top:20px;">
<h3>Environment</h3>
<table class="cart-table">
<tr><th>Site Base URL</th><td><?php echo $siteBaseUrl !== '' ? h($siteBaseUrl) : '(empty — using relative paths)'; ?></td></tr>
<tr><th>Data directory</th><td><?php echo isset($SITE_DATA_DIR) ? h($SITE_DATA_DIR) : '(unset)'; ?></td></tr>
<tr><th>PHP SAPI</th><td><?php echo h(PHP_SAPI); ?></td></tr>
<tr><th>Writable?</th><td><?php echo (isset($SITE_DATA_DIR) && is_writable($SITE_DATA_DIR)) ? 'yes' : 'no'; ?></td></tr>
<tr><th>XML Reference</th><td><a href="/modules/billing/docs/xml_notes.php" target="_blank" rel="noopener">Open XML Notes</a></td></tr>
</table>
</div>
<!-- JS: Per-row: enable/disable Primary radios based on whether that location is checked --> <!-- JS: Per-row: enable/disable Primary radios based on whether that location is checked -->
<script> <script>
document.querySelectorAll('.locs-box').forEach(function(box){ document.querySelectorAll('.locs-box').forEach(function(box){

View file

@ -138,7 +138,7 @@ log_payment('PAYMENT_CAPTURED', [
// Start session to get user_id (use billing website session name) // Start session to get user_id (use billing website session name)
if (session_status() === PHP_SESSION_NONE) { if (session_status() === PHP_SESSION_NONE) {
session_name("gameservers_website"); session_name("opengamepanel_web");
session_start(); session_start();
} }
$user_id = isset($_SESSION['website_user_id']) ? intval($_SESSION['website_user_id']) : $user_id = isset($_SESSION['website_user_id']) ? intval($_SESSION['website_user_id']) :
@ -152,8 +152,8 @@ if ($user_id <= 0) {
} }
// Connect to database // Connect to database
$db = mysqli_connect($db_host, $db_user, $db_pass, $db_name); $mysqli = mysqli_connect($db_host, $db_user, $db_pass, $db_name);
if (!$db) { if (!$mysqli) {
log_payment('DB_CONNECTION_FAILED', mysqli_connect_error()); log_payment('DB_CONNECTION_FAILED', mysqli_connect_error());
ob_clean(); ob_clean();
echo json_encode(['error' => 'db_connection_failed', 'request_id' => $requestId]); echo json_encode(['error' => 'db_connection_failed', 'request_id' => $requestId]);
@ -161,8 +161,8 @@ if (!$db) {
} }
$now = date('Y-m-d H:i:s'); $now = date('Y-m-d H:i:s');
$esc_txid = mysqli_real_escape_string($db, $txid); $esc_txid = mysqli_real_escape_string($mysqli, $txid);
$esc_paypal_json = mysqli_real_escape_string($db, $paypal_json); $esc_paypal_json = mysqli_real_escape_string($mysqli, $paypal_json);
// Apply coupon from session to invoices before marking paid // Apply coupon from session to invoices before marking paid
session_start(); session_start();
@ -171,12 +171,12 @@ if ($coupon_id > 0) {
// Get unpaid invoices for this user to apply coupon // Get unpaid invoices for this user to apply coupon
$invoices_query = "SELECT invoice_id, amount FROM {$table_prefix}billing_invoices $invoices_query = "SELECT invoice_id, amount FROM {$table_prefix}billing_invoices
WHERE user_id=$user_id AND status='due'"; WHERE user_id=$user_id AND status='due'";
$invoices_result = mysqli_query($db, $invoices_query); $invoices_result = mysqli_query($mysqli, $invoices_query);
// Get coupon details // Get coupon details
$coupon_query = "SELECT discount_percent FROM {$table_prefix}billing_coupons $coupon_query = "SELECT discount_percent FROM {$table_prefix}billing_coupons
WHERE coupon_id=$coupon_id AND is_active=1 LIMIT 1"; WHERE coupon_id=$coupon_id AND is_active=1 LIMIT 1";
$coupon_result = mysqli_query($db, $coupon_query); $coupon_result = mysqli_query($mysqli, $coupon_query);
if ($coupon_result && mysqli_num_rows($coupon_result) === 1) { if ($coupon_result && mysqli_num_rows($coupon_result) === 1) {
$coupon_row = mysqli_fetch_assoc($coupon_result); $coupon_row = mysqli_fetch_assoc($coupon_result);
@ -194,7 +194,7 @@ if ($coupon_id > 0) {
discount_amount=" . number_format($discount_amt, 2, '.', '') . ", discount_amount=" . number_format($discount_amt, 2, '.', '') . ",
amount=" . number_format($new_amount, 2, '.', '') . " amount=" . number_format($new_amount, 2, '.', '') . "
WHERE invoice_id=$inv_id"; WHERE invoice_id=$inv_id";
mysqli_query($db, $update_coupon_sql); mysqli_query($mysqli, $update_coupon_sql);
log_payment('COUPON_APPLIED', ['invoice_id' => $inv_id, 'discount' => $discount_amt]); log_payment('COUPON_APPLIED', ['invoice_id' => $inv_id, 'discount' => $discount_amt]);
} }
@ -202,7 +202,7 @@ if ($coupon_id > 0) {
$update_usage_sql = "UPDATE {$table_prefix}billing_coupons $update_usage_sql = "UPDATE {$table_prefix}billing_coupons
SET current_uses = current_uses + 1 SET current_uses = current_uses + 1
WHERE coupon_id=$coupon_id"; WHERE coupon_id=$coupon_id";
mysqli_query($db, $update_usage_sql); mysqli_query($mysqli, $update_usage_sql);
// Clear coupon from session // Clear coupon from session
unset($_SESSION['cart_coupon_code']); unset($_SESSION['cart_coupon_code']);
@ -216,45 +216,84 @@ $updateInvoicesSql = "UPDATE {$table_prefix}billing_invoices
WHERE user_id=$user_id AND status='due'"; WHERE user_id=$user_id AND status='due'";
log_payment('UPDATE_INVOICES_SQL', $updateInvoicesSql); log_payment('UPDATE_INVOICES_SQL', $updateInvoicesSql);
$updateResult = mysqli_query($db, $updateInvoicesSql); $updateResult = mysqli_query($mysqli, $updateInvoicesSql);
if (!$updateResult) { if (!$updateResult) {
log_payment('UPDATE_INVOICES_FAILED', mysqli_error($db)); log_payment('UPDATE_INVOICES_FAILED', mysqli_error($mysqli));
mysqli_close($db); mysqli_close($mysqli);
ob_clean(); ob_clean();
echo json_encode(['error' => 'update_invoices_failed', 'request_id' => $requestId]); echo json_encode(['error' => 'update_invoices_failed', 'request_id' => $requestId]);
exit; exit;
} }
$affectedInvoices = mysqli_affected_rows($db); $affectedInvoices = mysqli_affected_rows($mysqli);
log_payment('INVOICES_MARKED_PAID', ['count' => $affectedInvoices]); log_payment('INVOICES_MARKED_PAID', ['count' => $affectedInvoices]);
// Get all invoices we just marked paid // Get all invoices we just marked paid
$getInvoicesSql = "SELECT * FROM {$table_prefix}billing_invoices $getInvoicesSql = "SELECT * FROM {$table_prefix}billing_invoices
WHERE user_id=$user_id AND payment_txid='$esc_txid'"; WHERE user_id=$user_id AND payment_txid='$esc_txid'";
$invoicesResult = mysqli_query($db, $getInvoicesSql); $invoicesResult = mysqli_query($mysqli, $getInvoicesSql);
$ordersCreated = 0; $ordersCreated = 0;
$renewedOrders = 0;
$newOrderIds = [];
while ($inv = mysqli_fetch_assoc($invoicesResult)) { while ($inv = mysqli_fetch_assoc($invoicesResult)) {
$invoice_id = intval($inv['invoice_id']); $invoice_id = intval($inv['invoice_id']);
$existing_order_id = intval($inv['order_id'] ?? 0); $existing_order_id = intval($inv['order_id'] ?? 0);
// Skip if invoice already linked to an order (renewal) // Handle renewals by extending the existing order
if ($existing_order_id > 0) { if ($existing_order_id > 0) {
log_payment('RENEWAL_INVOICE', ['invoice_id' => $invoice_id, 'order_id' => $existing_order_id]); $durationUnit = strtolower(trim($inv['invoice_duration'] ?? 'month'));
$allowedDurations = ['day','month','year'];
if (!in_array($durationUnit, $allowedDurations, true)) {
$durationUnit = 'month';
}
$qty = max(1, intval($inv['qty'] ?? 1));
$orderInfoSql = "SELECT end_date FROM {$table_prefix}billing_orders WHERE order_id=$existing_order_id LIMIT 1";
$orderInfoRes = mysqli_query($mysqli, $orderInfoSql);
$currentEnd = null;
if ($orderInfoRes && mysqli_num_rows($orderInfoRes) === 1) {
$infoRow = mysqli_fetch_assoc($orderInfoRes);
$currentEnd = $infoRow['end_date'] ?? null;
}
$baseTs = time();
if (!empty($currentEnd)) {
$parsed = strtotime($currentEnd);
if ($parsed !== false && $parsed > time()) {
$baseTs = $parsed;
}
}
$newEndDate = date('Y-m-d H:i:s', strtotime("+$qty $durationUnit", $baseTs));
$renewSql = "UPDATE {$table_prefix}billing_orders
SET status='installed', end_date='$newEndDate', paid_ts='$now', payment_txid='$esc_txid'
WHERE order_id=$existing_order_id LIMIT 1";
if (mysqli_query($mysqli, $renewSql)) {
$renewedOrders++;
log_payment('ORDER_RENEWED', [
'order_id' => $existing_order_id,
'invoice_id' => $invoice_id,
'new_end_date' => $newEndDate
]);
} else {
log_payment('ORDER_RENEWAL_FAILED', [
'order_id' => $existing_order_id,
'invoice_id' => $invoice_id,
'error' => mysqli_error($mysqli)
]);
}
continue; continue;
} }
// Create new order // Create new order
$service_id = intval($inv['service_id']); $service_id = intval($inv['service_id']);
$home_name = mysqli_real_escape_string($db, $inv['home_name']); $home_name = mysqli_real_escape_string($mysqli, $inv['home_name']);
$ip = intval($inv['ip']); $ip = intval($inv['ip']);
$max_players = intval($inv['max_players']); $max_players = intval($inv['max_players']);
$qty = intval($inv['qty']); $qty = intval($inv['qty']);
$duration = mysqli_real_escape_string($db, $inv['invoice_duration']); $duration = mysqli_real_escape_string($mysqli, $inv['invoice_duration']);
$amount = floatval($inv['amount']); $amount = floatval($inv['amount']);
$rcon_pw = mysqli_real_escape_string($db, $inv['remote_control_password']); $rcon_pw = mysqli_real_escape_string($mysqli, $inv['remote_control_password']);
$ftp_pw = mysqli_real_escape_string($db, $inv['ftp_password']); $ftp_pw = mysqli_real_escape_string($mysqli, $inv['ftp_password']);
// Calculate end_date // Calculate end_date
$end_date = date('Y-m-d H:i:s', strtotime("+$qty $duration")); $end_date = date('Y-m-d H:i:s', strtotime("+$qty $duration"));
@ -272,25 +311,46 @@ while ($inv = mysqli_fetch_assoc($invoicesResult)) {
log_payment('INSERT_ORDER_SQL', substr($insertOrderSql, 0, 300)); log_payment('INSERT_ORDER_SQL', substr($insertOrderSql, 0, 300));
if (mysqli_query($db, $insertOrderSql)) { if (mysqli_query($mysqli, $insertOrderSql)) {
$new_order_id = mysqli_insert_id($db); $new_order_id = mysqli_insert_id($mysqli);
log_payment('ORDER_CREATED', ['order_id' => $new_order_id, 'invoice_id' => $invoice_id]); log_payment('ORDER_CREATED', ['order_id' => $new_order_id, 'invoice_id' => $invoice_id]);
$newOrderIds[] = $new_order_id;
// Link invoice to order // Link invoice to order
$linkSql = "UPDATE {$table_prefix}billing_invoices SET order_id=$new_order_id WHERE invoice_id=$invoice_id"; $linkSql = "UPDATE {$table_prefix}billing_invoices SET order_id=$new_order_id WHERE invoice_id=$invoice_id";
mysqli_query($db, $linkSql); mysqli_query($mysqli, $linkSql);
$ordersCreated++; $ordersCreated++;
} else { } else {
log_payment('INSERT_ORDER_FAILED', mysqli_error($db)); log_payment('INSERT_ORDER_FAILED', mysqli_error($mysqli));
} }
} }
mysqli_close($db); mysqli_close($mysqli);
$autoProvisionResult = ['provisioned_count' => 0, 'failed_count' => 0, 'orders' => []];
if (!empty($newOrderIds)) {
require_once __DIR__ . '/../includes/panel_bridge.php';
$panelCtx = billing_panel_bootstrap();
if ($panelCtx && isset($panelCtx['db']) && $panelCtx['db'] instanceof OGPDatabase) {
$GLOBALS['db'] = $panelCtx['db'];
$GLOBALS['settings'] = $panelCtx['settings'];
require_once __DIR__ . '/../create_servers.php';
$autoProvisionResult = billing_invoke_provision([
'order_ids' => $newOrderIds,
'user_id' => $user_id,
'is_admin' => true
]);
log_payment('AUTO_PROVISION_COMPLETE', $autoProvisionResult);
} else {
log_payment('AUTO_PROVISION_SKIPPED', 'panel bootstrap failed');
}
}
log_payment('PROCESSING_COMPLETE', [ log_payment('PROCESSING_COMPLETE', [
'invoices_paid' => $affectedInvoices, 'invoices_paid' => $affectedInvoices,
'orders_created' => $ordersCreated, 'orders_created' => $ordersCreated,
'orders_renewed' => $renewedOrders,
'txid' => $txid 'txid' => $txid
]); ]);
@ -301,5 +361,9 @@ echo json_encode([
'order_id' => $paypal_order_id, 'order_id' => $paypal_order_id,
'txid' => $txid, 'txid' => $txid,
'invoices_paid' => $affectedInvoices, 'invoices_paid' => $affectedInvoices,
'orders_created' => $ordersCreated 'orders_created' => $ordersCreated,
'orders_renewed' => $renewedOrders,
'provisioned' => $autoProvisionResult['provisioned_count'] ?? 0
]); ]);

View file

@ -3,6 +3,9 @@
// Central bootstrap for billing website pages. Loads config, provides safe DB helper // Central bootstrap for billing website pages. Loads config, provides safe DB helper
// and ensures $table_prefix is available. // and ensures $table_prefix is available.
// Ensure session sync with panel happens first
require_once __DIR__ . '/includes/session_bridge.php';
// Load configuration (includes/config.inc.php) if present // Load configuration (includes/config.inc.php) if present
$config_path = __DIR__ . '/includes/config.inc.php'; $config_path = __DIR__ . '/includes/config.inc.php';
if (file_exists($config_path)) { if (file_exists($config_path)) {

View file

@ -7,7 +7,7 @@
// Start session with website session name // Start session with website session name
if (session_status() === PHP_SESSION_NONE) { if (session_status() === PHP_SESSION_NONE) {
session_name("gameservers_website"); session_name("opengamepanel_web");
session_start(); session_start();
} }
@ -760,3 +760,4 @@ $siteBase = $protocol . $host;
<?php include(__DIR__ . '/includes/footer.php'); ?> <?php include(__DIR__ . '/includes/footer.php'); ?>
</body> </body>
</html> </html>

View file

@ -1,15 +1,48 @@
<?php <?php
require_once("includes/lib_remote.php"); require_once __DIR__ . '/../../includes/lib_remote.php';
require_once("modules/config_games/server_config_parser.php"); require_once __DIR__ . '/../config_games/server_config_parser.php';
if (!function_exists('billing_invoke_provision')) {
function billing_invoke_provision(array $options = array())
{
$GLOBALS['BILLING_PROVISION_OVERRIDE'] = $options;
ob_start();
exec_ogp_module();
$output = ob_get_clean();
$result = isset($GLOBALS['BILLING_PROVISION_LAST_RESULT']) ? $GLOBALS['BILLING_PROVISION_LAST_RESULT'] : array();
$result['output'] = $output;
unset($GLOBALS['BILLING_PROVISION_OVERRIDE'], $GLOBALS['BILLING_PROVISION_LAST_RESULT']);
return $result;
}
}
function exec_ogp_module() function exec_ogp_module()
{ {
global $db,$view,$settings; global $db,$view,$settings;
$user_id = $_SESSION['user_id'];
$isAdmin = $db->isAdmin( $_SESSION['user_id'] ); $override = isset($GLOBALS['BILLING_PROVISION_OVERRIDE']) ? $GLOBALS['BILLING_PROVISION_OVERRIDE'] : null;
$user_id = isset($override['user_id']) ? intval($override['user_id']) : (isset($_SESSION['user_id']) ? intval($_SESSION['user_id']) : 0);
$isAdmin = isset($override['is_admin']) ? (bool)$override['is_admin'] : $db->isAdmin($user_id);
$provision_all = $override ? !empty($override['provision_all']) : isset($_POST['provision_all']);
$orderIds = array();
if ($override && !empty($override['order_ids'])) {
$orderIds = array_map('intval', (array)$override['order_ids']);
}
if (empty($orderIds)) {
$order_id = null;
if (isset($_POST['order_id'])) {
$order_id = $_POST['order_id'];
}
if(isset($_GET['order_id'])){
$order_id = $_GET['order_id'];
}
if (!empty($order_id)) {
$orderIds = array(intval($order_id));
}
}
// Handle provision_all request - provision all paid orders for this user // Handle provision_all request - provision all paid orders for this user
if (isset($_POST['provision_all'])) { if ($provision_all) {
if ( $isAdmin ){ if ( $isAdmin ){
$orders = $db->resultQuery( "SELECT * FROM OGP_DB_PREFIXbilling_orders WHERE status='paid' ORDER BY order_id" ); $orders = $db->resultQuery( "SELECT * FROM OGP_DB_PREFIXbilling_orders WHERE status='paid' ORDER BY order_id" );
} else { } else {
@ -18,25 +51,19 @@ function exec_ogp_module()
} }
// Handle provision_single or order_id parameter - provision specific order // Handle provision_single or order_id parameter - provision specific order
else { else {
$order_id = null; if (empty($orderIds)) {
if (isset($_POST['order_id'])) {
$order_id = $_POST['order_id'];
}
if(isset($_GET['order_id'])){
$order_id = $_GET['order_id'];
}
if (!$order_id) {
echo "<div class='failure'>No order ID specified.</div>"; echo "<div class='failure'>No order ID specified.</div>";
$GLOBALS['BILLING_PROVISION_LAST_RESULT'] = array('provisioned_count'=>0,'failed_count'=>0,'orders'=>array());
return; return;
} }
$idList = implode(',', array_map('intval', $orderIds));
if ( $isAdmin ){ if ( $isAdmin ){
$orders = $db->resultQuery( "SELECT * FROM OGP_DB_PREFIXbilling_orders WHERE order_id=".$db->realEscapeSingle($order_id)." AND status='paid'" ); $orders = $db->resultQuery( "SELECT * FROM OGP_DB_PREFIXbilling_orders WHERE order_id IN ($idList) AND status='paid'" );
} else { } else {
$orders = $db->resultQuery( "SELECT * FROM OGP_DB_PREFIXbilling_orders WHERE order_id=".$db->realEscapeSingle($order_id)." AND user_id=".$db->realEscapeSingle($user_id)." AND status='paid'" ); $orders = $db->resultQuery( "SELECT * FROM OGP_DB_PREFIXbilling_orders WHERE order_id IN ($idList) AND user_id=".$db->realEscapeSingle($user_id)." AND status='paid'" );
} }
} }
$processed_orders = array();
if( !empty($orders) ) if( !empty($orders) )
{ {
$provisioned_count = 0; $provisioned_count = 0;
@ -45,6 +72,7 @@ function exec_ogp_module()
foreach($orders as $order) foreach($orders as $order)
{ {
$order_id = $order['order_id']; $order_id = $order['order_id'];
$processed_orders[] = intval($order_id);
$service_id = $order['service_id']; $service_id = $order['service_id'];
$home_name = $order['home_name']; $home_name = $order['home_name'];
$remote_control_password = $order['remote_control_password']; $remote_control_password = $order['remote_control_password'];
@ -408,7 +436,14 @@ function exec_ogp_module()
echo "<p>No paid orders found to provision.</p>"; echo "<p>No paid orders found to provision.</p>";
echo "</div>"; echo "</div>";
echo "<p><a href='home.php?m=billing&p=my_orders' class='btn'>View My Orders</a></p>"; echo "<p><a href='home.php?m=billing&p=my_orders' class='btn'>View My Orders</a></p>";
$provisioned_count = 0;
$failed_count = 0;
} }
$GLOBALS['BILLING_PROVISION_LAST_RESULT'] = array(
'provisioned_count' => isset($provisioned_count) ? $provisioned_count : 0,
'failed_count' => isset($failed_count) ? $failed_count : 0,
'orders' => $processed_orders,
);
} }
?> ?>

View file

@ -15,7 +15,7 @@ echo "Session cookie params: " . json_encode(session_get_cookie_params()) . "\n"
echo "Session status (before start): " . session_status() . "\n"; echo "Session status (before start): " . session_status() . "\n";
// Try to start a named session used by _website // Try to start a named session used by _website
session_name('gameservers_website'); session_name('opengamepanel_web');
@session_start(); @session_start();
echo "Session status (after start): " . session_status() . "\n"; echo "Session status (after start): " . session_status() . "\n";
echo "Session id: " . session_id() . "\n"; echo "Session id: " . session_id() . "\n";
@ -70,3 +70,4 @@ echo " - Ensure the site path is served under the expected /_website/ path and t
echo " - If sessions aren't persistent across requests, check webserver user permissions and session.save_path.\n"; echo " - If sessions aren't persistent across requests, check webserver user permissions and session.save_path.\n";
?> ?>

View file

@ -6,7 +6,7 @@
// Start session using the website session name to match the rest of the site // Start session using the website session name to match the rest of the site
if (session_status() === PHP_SESSION_NONE) { if (session_status() === PHP_SESSION_NONE) {
session_name("gameservers_website"); session_name("opengamepanel_web");
session_start(); session_start();
} }
@ -428,3 +428,4 @@ uksort($grouped, function($a, $b) use ($categoryOrder) {
</div> </div>
</body> </body>
</html> </html>

View file

@ -8,12 +8,12 @@ This document summarizes the comprehensive enhancements made to the billing modu
### 1. Documentation Page Login Button Issue ✅ ### 1. Documentation Page Login Button Issue ✅
**Problem:** Documentation page showed "Login" button even when user was logged in. **Problem:** Documentation page showed "Login" button even when user was logged in.
**Root Cause:** docs.php used basic `session_start()` instead of the website's session name. **Root Cause:** docs.php used basic `session_start()` instead of the website's session name.
**Solution:** Changed to use `gameservers_website` session name to match rest of website. **Solution:** Changed to use `opengamepanel_web` session name to match rest of website.
### 2. Cart Page Display Issue ✅ ### 2. Cart Page Display Issue ✅
**Problem:** Cart page didn't display when clicking menu link. **Problem:** Cart page didn't display when clicking menu link.
**Root Cause:** cart.php also used basic `session_start()` causing session inconsistency. **Root Cause:** cart.php also used basic `session_start()` causing session inconsistency.
**Solution:** Changed to use `gameservers_website` session name for consistency. **Solution:** Changed to use `opengamepanel_web` session name for consistency.
### 3. Documentation Content Enhancement ✅ ### 3. Documentation Content Enhancement ✅
**Problem:** Documentation was basic, system-specific, and not comprehensive enough for SEO. **Problem:** Documentation was basic, system-specific, and not comprehensive enough for SEO.
@ -33,7 +33,7 @@ session_start();
// NEW // NEW
if (session_status() === PHP_SESSION_NONE) { if (session_status() === PHP_SESSION_NONE) {
session_name("gameservers_website"); session_name("opengamepanel_web");
session_start(); session_start();
} }
``` ```
@ -179,7 +179,7 @@ The same comprehensive template can be applied to all remaining games:
## Implementation Notes ## Implementation Notes
### Session Name Consistency ### Session Name Consistency
The entire billing module now uses `gameservers_website` session name: The entire billing module now uses `opengamepanel_web` session name:
- login.php ✅ - login.php ✅
- register.php ✅ - register.php ✅
- logout.php ✅ - logout.php ✅
@ -247,3 +247,4 @@ The documentation system is now:
*Created: November 8, 2024* *Created: November 8, 2024*
*Last Updated: November 8, 2024* *Last Updated: November 8, 2024*

View file

@ -0,0 +1,622 @@
## OGP XML Notes / still W.I.P.
_The order of each XML element matters, and this guide presents them in their order of appearance!_
___
### Linux and Windows:
#### Game Config:
This is the first element. There can only be one `<game_config>` element.
```
<game_config>
the whole XML file content here
</game_config>
```
All the following elements should be contained within `<game_config>` element.
___
#### Game Key:
Comes after `<game_config>` element (actually within `<game_config>` element as said above). There can only be one `<game_key>` element. Example:
```
<game_key>space_engineers_win64</game_key>
```
This is a unique key used to identify this specific game server in OGP. You should not use spaces, nor any special character in it, only alpha-numeric value and underscores. It should contain a suffix related to the compatible OS. Available suffixes are `_win32`, `_win64`, `_linux32`, `_linux64`, using one of these suffixes in the game_key will let OGP know which OS it is available on, making it visible or not when you install a new game server, depending on your OS architecture. `_win` and `_linux` work too, but we highly recommend to now use the previously listed suffixes.
___
#### Query Protocol:
Comes after `<game_key>` element. There can only be one `<protocol>` element. Example:
```
<protocol>lgsl</protocol>
```
It defines the query protocol used by OGP. Available protocols are `lgsl`, `gameq`, `rcon` (`rcon2`? `lcon`?)
___
#### LGSL Query Name:
Comes after `<protocol>` element. There can only be one `<lgsl_query_name>` element. Example:
```
<lgsl_query_name>killingfloor2</lgsl_query_name>
```
This is the unique key referencing this specific game server in the LGSL protocol file, used to query the game server.
___
#### GameQ Query Name:
Comes after `<protocol>` element. There can only be one `<gameq_query_name>` element. Example:
```
<gameq_query_name>redorchestra2</gameq_query_name>
```
This is the unique key referencing this specific game server in the GameQ protocol files, used to query the game server.
___
#### Installer:
Comes after `<protocol>` element (or comes after `<lgsl_query_name>` or `<gameq_query_name>` element when used). There can only be one `<installer>` element. Example:
```
<installer>steamcmd</installer>
```
Defines the use of SteamCMD tool to install the game server.
___
#### Game Name:
Comes after `<installer>` element. There can only be one `<game_name>` element. Example:
```
<game_name>Killing Floor 2</game_name>
```
This is the real game server name appearing in the list when installing a new game server.
___
#### Server Executable Name:
Comes after `<game_name>` element. There can only be one `<server_exec_name>` element. Example:
```
<server_exec_name>SpaceEngineersDedicated.exe</server_exec_name>
```
This is the server executable name used in the start command line.
___
#### Query Port:
Comes after `<server_exec_name>` element. There can only be one `<query_port>` element. Example:
```
<query_port type='add'>13</query_port>
```
Difference between the server port (`%PORT%`) and the query port (`%QUERY_PORT%`). In this example the variable %QUERY_PORT% will be 13 added to the port value.
___
#### CLI Template:
Comes after `<server_exec_name>` element. There can only be one `<cli_template>` element. Example:
```
<cli_template>-console %BASE_PATH% -ignorelastsession</cli_template>
```
```
<cli_template>%MAP%%GAMEMODE%%DIFFICULTY%%GAMELENGTH%%PLAYERS%%MUTATOR% %PORT% %IP% %WEB_ADMIN_PORT% %QUERY_PORT%</cli_template>
```
This is the template that will generate the start command line placed after the server executable name.
You can use these variables which are known to OGP:
```
GAME_TYPE
HOSTNAME
IP
MAP
PID_FILE
PLAYERS
PORT
QUERY_PORT
BASE_PATH
HOME_PATH
SAVE_PATH
OUTPUT_PATH
CONTROL_PASSWORD
```
These variable should be between `%` characters, the Panel will then replace them with the proper value when generating the start command line.
You can also use custom variables that you will define later in the XML.
___
#### CLI Parameters:
Comes after `<cli_template>` element. There can only be one `<cli_params>` element. Example:
```
<cli_params>
<cli_param id="MAP" cli_string="" />
<cli_param id="IP" cli_string="-MultiHome=" options="q"/>
<cli_param id="PORT" cli_string="-Port=" options="sq"/>
<cli_param id="PID_FILE" cli_string="" />
<cli_param id="GAME_TYPE" cli_string="" />
<cli_param id="HOME_PATH" cli_string="" />
</cli_params>
```
It defines the known variables used in `<cli_template>`. In this example we can imagine that for **%MAP%** it will generate the map name without space or quotes around it, because there is no `options`, for **%IP%** it will generate `-MultiHome="123.123.123.123"` using the `cli_string` and adding only quotes around the game server IP value because of `options="q"`, and **%PORT%** will generate `-Port= "27015"` using the `cli_string` and adding space and quotes around the game server port because of `options="sq"`.
___
#### Reserve Ports:
Comes after `<cli_template>` or `<cli_params>` element. There can only be one `<reserve_ports>` element. Example:
```
<reserve_ports>
<port type="subtract" id="WEB_ADMIN_PORT" cli_string="-WebAdminPort=">5</port>
<port type="add" id="STEAM_PORT" cli_string="-SteamPort=">19238</port>
<port type="add" id="MY_CUSTOM_PORT">666</port>
</reserve_ports>
```
You can add reserved ports here to use in the generated start command line. These ports will also be managed by OGP if OGP is used to manage the Agent machine firewall. Type can be `add` or `subtract` which is self explanatory. In this example when using the %WEB_ADMIN_PORT% variable in the `<cli_template>` it will generate `-WebAdminPort=XXX`, XXX being 5 subtracted to the Port set for this game server, when using the %STEAM_PORT% variable in the `<cli_template>` it will generate `-SteamPort=XXX` where XXX will be 19238 added to the Port set for this game server. As you can see, the variable %MY_CUSTOM_PORT% have no `cli_string`, this can be used this way to simply open this port (which here would be 666 added to the game server port) in the Agent machine firewall, when OGP is set to control the machine firewall (note: the firewall management may actually not open anything else than the game server port, to be verified).
___
#### CLI Allowed Characters:
Comes after `<reserve_ports>` element. There can only be one `<cli_allow_chars>` element. Example:
```
<cli_allow_chars>;</cli_allow_chars>
```
Used to allow some special characters in the command line. Escaped by default: ```\ " ' | & ; > < ` $ ( ) [ ]```
___
#### Maps Location:
Comes after `<cli_template>` element. There can only be one `<maps_location>` element. Example:
```
<maps_location>folder/maps</maps_location>
```
It sets the path of the map folder for this game server, which will be used to generate a selectable map list available before starting the game server. If folder contains map files it will use their name without the extension, if it contains sub folders with each map inside each own sub folder, it will generate the map list based on the folders names contained in the defined path. The selected map in the list will be used to replace the %MAP% variable in the `<cli_template>`.
___
#### Map List:
Comes after `<cli_template>` element. There can only be one `<map_list>` element. Example:
```
<map_list>maplist.txt</map_list>
```
The map list file path used to generate the selectable map list available before starting the game server. In this example it will look for a file called maplist.txt inside the root of the game server. Map list should have one map per line. The selected map in the list will be used to replace the %MAP% variable in the `<cli_template>`.
___
#### Console Log:
Comes after `<cli_template>` element. There can only be one `<console_log>` element. Example:
```
<console_log>KFGame/Logs/Launch.log</console_log>
```
It defines the path of the log file that will be shown in the LOG page of the game server. Most game servers, especially on Linux, will not need that, when in general, Windows game server will need it to properly show the output log.
___
#### Executable Location:
Comes after `<console_log>` element. There can only be one `<exe_location>` element. Example:
```
<exe_location>Binaries/Win64</exe_location>
```
It defines the path of the game server executable when not in the root folder.
___
#### Max User Amount:
Comes after `<exe_location>` element. There can only be one `<max_user_amount>` element. Example:
```
<max_user_amount>64</max_user_amount>
```
It defines the maximum player number you will be able to set when creating the game server.
___
#### Control Protocol:
Comes after `<max_user_amount>` element. There can only be one `<control_protocol>` element. Example:
```
<control_protocol>rcon2</control_protocol>
```
Can be `rcon`, `rcon2`, or `lcon` (legacy). Note that `rcon` can also have type option to define, which can be `old` or `new`. Example:
```
<control_protocol>rcon</control_protocol>
<control_protocol_type>old</control_protocol_type>
```
___
#### Mods:
Comes after `<control_protocol>` element. There can only be one `<mods>` element. Example:
```
<mods>
<mod key="insurgency">
<name>none</name>
<installer_name>237410</installer_name>
</mod>
</mods>
```
Used to define different mods for the game server, in this example there is only one mod available which will be the default installed one (actually the game server itself here). The `<installer_name>` here is the Steam appID that will be used to install and update the game server. (note: case RSync to explain? Case multi mods to explain?)
___
#### Replace Texts
`<replace_texts>` Comes after `<mods>`.
Contains multiple `<text>` entries like so:
```
<replace_texts>
<text key="control_password">
<default>ServerAdminPassword=.*</default>
<var>ServerAdminPassword=</var>
<filepath>ShooterGame/Saved/Config/LinuxServer/GameUserSettings.ini</filepath>
<options>sq</options>
</text>
<text key="home_name">
<default>SessionName=.*</default>
<var>SessionName=</var>
<filepath>ShooterGame/Saved/Config/LinuxServer/GameUserSettings.ini</filepath>
<options>sq</options>
</text>
</replace_texts>
```
`<default>` within `<text>` is what the line to replace starts with.
`<var>` within `<text>` is the key for what should be kept when the line is replaced with the value entered by the user when replacing text occurs.
`<filepath>` within `<text>` specifies the text file to make the replacement in.
`<options>` within `<text>` specifies how to enter the user's value after the `<var>` key. Possible options are:
```
nothing / no value = placed as is
s = space / separated
q = quoted
sq = space and quotes
sc = space and ends with a comma
sqc = space, quoted, and ends with a comma
```
These replace text will be applied on server start and modify the specified config files with the values generated by the Panel.
___
#### Server Params:
`<server_params>` Comes after `<replace_texts>`.
Contains multiple `<param>` entries like so:
```
<server_params>
<param id="SP" key="?ServerPassword=" type="text">
<option>ns</option>
<caption>Server Password</caption>
<desc>Players must know this password to connect.</desc>
</param>
<param id="DIFFICULTY" key="?Difficulty=" type="select">
<option value=""></option>
<option value="0">Normal</option>
<option value="1">Hard</option>
<options>ns</options>
<caption>Difficulty</caption>
<desc>This sets the server difficulty. Leave empty to configure this parameter in the config files or webadmin</desc>
</param>
<param key="-EnableCheats" type="checkbox_key_value">
<caption>Cheats</caption>
<desc>Enable the cheats to be used from the ingame Admin menu</desc>
</param>
</server_params>
```
`id` attribute on the `<param>` specifies which variable to replace in the `<cli_template>`
`key` attribute specifies what will replace the variable defined by id attribute
type attribute will define what kind of parameter it is, possible values are `text` `select` `checkbox_key_value`:
- `text` will allow to write text value to be added during the replacement of the variables in startup command line. For example, `%SP%` in `<cli_template>` would be replaced with `?ServerPassword=XXX` where XXX would be the value entered by the user in the text box, if nothing is entered the variable is not replaced but removed from `<cli_template>`. The value entered can be modified to fit your needs by using the `<option>` element within the `<param>` element.
- `select` will create a selectable list to choose the parameter value. The `%DIFFICULTY%` variable in `<cli_template>` would be replaced with `?Difficulty=1` if `Hard` would have been selected before starting the server.
- `checkbox_key_value` will add a simple checkbox that, if ticked before starting server, would add the parameter `-EnableCheats` at the end of the startup command line.
Valid options are for the `<options>` element within the `<param>` element within the `<server_params>` element are:
```
ns = no space between key and value
q = quotes wrapped around value after key (no space added)
s = space added after key before value (no quotes added)
anything else = space after key and quotes around the value
```
___
#### Custom Fields:
Comes after `<server_params>` element. There can only be one `<custom_fields>` element. Example:
```
<custom_fields>
<field key="sv_maxrate" type="text">
<default>sv_maxrate.*</default>
<default_value>0</default_value>
<var>sv_maxrate</var>
<filepath>orangebox/cspromod/cfg/server.cfg</filepath>
<options>s</options>
<desc>Max bandwidth rate allowed on server ( bytes per second ), 0 == unlimited.</desc>
</field>
<field key="sv_minrate" type="text">
<default>sv_minrate.*</default>
<default_value>5000</default_value>
<var>sv_minrate</var>
<filepath>orangebox/cspromod/cfg/server.cfg</filepath>
<options>s</options>
<desc>Min bandwidth rate allowed on server ( bytes per second ), 0 == unlimited.</desc>
</field>
<field key="sv_maxcmdrate" type="text">
<default>sv_maxcmdrate.*</default>
<default_value>66</default_value>
<var>sv_maxcmdrate</var>
<filepath>orangebox/cspromod/cfg/server.cfg</filepath>
<options>s</options>
<desc>If sv_mincmdrate is > 0, this sets the maximum value for cl_cmdrate.</desc>
</field>
<field key="sv_mincmdrate" type="text">
<default>sv_mincmdrate.*</default>
<default_value>67</default_value>
<var>sv_mincmdrate</var>
<filepath>orangebox/cspromod/cfg/server.cfg</filepath>
<options>s</options>
<desc>This sets the minimum value for cl_cmdrate. 0 == unlimited.</desc>
</field>
<field key="sv_maxupdaterate" type="text">
<default>sv_maxupdaterate.*</default>
<default_value>66</default_value>
<var>sv_maxupdaterate</var>
<filepath>orangebox/cspromod/cfg/server.cfg</filepath>
<options>s</options>
<desc>Maximum updates per second that the server will allow.</desc>
</field>
<field key="sv_minupdaterate" type="text">
<default>sv_minupdaterate.*</default>
<default_value>67</default_value>
<var>sv_minupdaterate</var>
<filepath>orangebox/cspromod/cfg/server.cfg</filepath>
<options>s</options>
<desc>Minimum updates per second that the server will allow.</desc>
</field>
</custom_fields>
```
Custom Fields available when you go to Custom Fields button from the game server page. These custom fields will be applied on every server start and will replace the specific values in the specified config files.
___
#### List Players Command:
Comes after `<custom_fields>` element. There can only be one `<list_players_command>` element. Example:
```
<list_players_command>status</list_players_command>
```
This is the command to list the players on the server when using `control_protocol`.
___
#### Player Info Regex:
Comes after `<list_players_command>` element. There can only be one `<player_info_regex>` element. Example:
```
<player_info_regex>#\#\s*(\d*)\s*\"(.*)\".*#</player_info_regex>
```
Regex used for player info when using `control_protocol`.
___
#### Player Info:
Comes after `<player_info_regex>` element. There can only be one `<player_info>` element. Example:
```
<player_info>
<index key="1">userid</index>
<index key="2">Name</index>
</player_info>
```
Defines the different keys for player infos when using `control_protocol`.
___
#### Player Commands:
Comes after `<player_info>` element. There can only be one `<player_commands>` element. Example:
```
<player_commands>
<command key="Kick" type="hidden">
<string>kick "%Name%"</string>
</command>
<command key="Ban" type="select">
<option value="0">Permanent</option>
<option value="15">15m</option>
<option value="30">30m</option>
<option value="60">1h</option>
<option value="1440">1D</option>
<option value="10080">1W</option>
<option value="43200">30D</option>
<string>banid %input% %userid% kick</string>
</command>
<command key="Change Nick" type="text">
<default>new nick</default>
<string>sm_rename #%userid% "%input%"</string>
</command>
</player_commands>
```
Defines the commands available when using `control_protocol`.
___
#### Pre Install:
Comes after `<player_commands>` element. There can only be one `<pre_install>` element. It can contain multiple entries one per line. Example:
```
<pre_install>
</pre_install>
```
Script executed before installing the game server.
___
#### Post Install:
Comes after `<pre_install>` element. There can only be one `<post_install>` element. It can contain multiple entries one per line. Example:
```
<post_install>
cp linux64/steamclient.so ./steamclient.so
echo &apos;#!/bin/bash
./bin/AvorionServer $@&apos; &gt; avorion_ogpstarter.sh
chmod +x avorion_ogpstarter.sh
</post_install>
```
Script executed after installing or updating the game server, similar to `pre_install` (which `pre_install is executed before installation/update obviously)`. In this example, after installation or update of the game server, it will copy the file linux64/steamclient.so to ./steamclient.so and generate a bash script in the file avorion_ogpstarter.sh and then make this bash script executable. Note that the bash script must be written to be properly interpreted by the XML (see the `"` character replaced by `&apos;`, `>` replaced by `&gt;`, and so on.. if you write plain bash script it will break the XML in Panel, so keep that in mind and replace special characters with their XML readable counterpart.
___
### Windows:
#### Pre-Start Commands:
Comes after the `<server_params>` element. There can only be one `<pre_start>` element. It can run multiple lines of script that will be executed by the cmd batch environment.
This can be filled with lines of script that will be run in a batch script just before the game server is started. The script will always change directory into the home directory before your commands will run, so you can reference things locally.
```
<pre_start></pre_start>
```
___
#### Environment Variables:
Comes after `<pre_start>` element. There can only be one `<environment_variables>` element. It can contain multiple entries one per line.
```
<environment_variables>
</environment_variables>
```
Used for setting environment variables which may be needed by some servers. This is run in the batch environment, so please make sure you're using Windows commands for your environment SETS.
Special entries:
`{OGP_HOME_DIR}` will be replaced by the home directory for the game server.
___
### Linux:
#### Pre-Start Commands:
Comes after the `<server_params>` element. There can only be one `<pre_start>` element. It can run multiple lines of script that will be executed by the bash shell.
This can be filled with lines of script that will be run in a bash script just before the game server is started. You do NOT need to provide the shebang "#!/bin/bash" in your commands. The script will always change directory into the home directory before your commands will run, so you can reference things locally.
```
<pre_start></pre_start>
```
Example (writes hiya to a file named testingPreStart in the home directory of the server):
```
<pre_start>
echo "hiya" >> testingPreStart
</pre_start>
```
___
#### Environment Variables:
Comes after `<pre_start>` element. There can only be one `<environment_variables>` element. It can contain multiple entries one per line.
```
<environment_variables>
</environment_variables>
```
Used for setting environment variables which may be needed by some servers such as Rust.
Example:
```
<environment_variables>
export LD_LIBRARY_PATH="{OGP_HOME_DIR}/RustDedicated_Data/Plugins/x86_64"
</environment_variables>
```
Special entries:
`{OGP_HOME_DIR}` will be replaced by the home directory for the game server.
___
### Linux and Windows:
#### Locking / Protecting Additional Files:
Comes after `<environment_variables>` element. There can only be one `<lock_files>` element. It can contain multiple entries one per line.
```
<lock_files>
</lock_files>
```
Used for protecting additional game server files by using the system program `chattr`. This is a Linux only element. You can use relative (paths are relative to the home directory for the game server) or absolute paths.
Example:
```
<lock_files>
bin/AvorionServer
</lock_files>
```
The above example adds chattr +i to the bin/AvorionServer executable. This prevents the user from changing it. When SteamCMD or Rsync is run to update the files, everything is unlocked, but the files specified here will be locked post update / install. These files are also locked again (just to make sure) before the server is restarted and started. Entries here will not BREAK or AFFECT the steamcmd or Rsync update process.
Special entries:
`{OGP_HOME_DIR}` will be replaced by the home directory for the game server. However, if you're using this variable, you should just use a local path.
___
#### Configuration Files:
Comes after `<lock_files>` element. There can only be one `<configuration_files>` element.
There can be multiple `<file>` entries within `<configuration_files>` element, the description in the `<file>` element is not mandatory. Example:
```
<configuration_files>
<file description="The Main config file">Path/To/Config/File.ini</file>
<file description="Another Config file">FolderX/GameEngine.ini</file>
<file>FolderZ/GameConfig.ini</file>
</configuration_files>
```
It defines the files directly editable from Panel even without access to the server files directly. This requires the EditConfigFiles extra module.

View file

@ -0,0 +1,147 @@
<?php
require_once(__DIR__ . '/../includes/admin_auth.php');
require_once(__DIR__ . '/../includes/config.inc.php');
include(__DIR__ . '/../includes/top.php');
include(__DIR__ . '/../includes/menu.php');
$sourceFile = __DIR__ . '/XML-Notes.md';
$markdown = file_exists($sourceFile) ? file_get_contents($sourceFile) : 'Source file missing.';
function billing_render_markdown($markdown)
{
$markdown = str_replace("\r\n", "\n", (string)$markdown);
$lines = explode("\n", $markdown);
$html = '';
$inCode = false;
$inList = false;
foreach ($lines as $line) {
$trim = trim($line);
if ($trim === '```') {
if ($inCode) {
$html .= "</code></pre>\n";
$inCode = false;
} else {
if ($inList) {
$html .= "</ul>\n";
$inList = false;
}
$html .= "<pre class=\"code-block\"><code>";
$inCode = true;
}
continue;
}
if ($inCode) {
$html .= htmlspecialchars($line, ENT_QUOTES, 'UTF-8') . "\n";
continue;
}
if ($trim === '___') {
if ($inList) {
$html .= "</ul>\n";
$inList = false;
}
$html .= "<hr>\n";
continue;
}
if (preg_match('/^(#{2,6})\s+(.*)$/', $line, $m)) {
if ($inList) {
$html .= "</ul>\n";
$inList = false;
}
$level = strlen($m[1]);
$text = htmlspecialchars($m[2], ENT_QUOTES, 'UTF-8');
$html .= "<h{$level}>{$text}</h{$level}>\n";
continue;
}
if (strpos($trim, '- ') === 0) {
if (!$inList) {
$html .= "<ul>\n";
$inList = true;
}
$item = htmlspecialchars(substr($trim, 2), ENT_QUOTES, 'UTF-8');
$item = preg_replace('/`([^`]+)`/', '<code>$1</code>', $item);
$html .= "<li>{$item}</li>\n";
continue;
}
if ($trim === '') {
if ($inList) {
$html .= "</ul>\n";
$inList = false;
}
$html .= "<p></p>\n";
continue;
}
if ($inList) {
$html .= "</ul>\n";
$inList = false;
}
$lineHtml = htmlspecialchars($line, ENT_QUOTES, 'UTF-8');
$lineHtml = preg_replace('/`([^`]+)`/', '<code>$1</code>', $lineHtml);
$html .= "<p>{$lineHtml}</p>\n";
}
if ($inList) {
$html .= "</ul>\n";
}
if ($inCode) {
$html .= "</code></pre>\n";
}
return $html;
}
$docHtml = billing_render_markdown($markdown);
?>
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>XML Configuration Notes</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="../css/header.css">
<style>
.doc-wrapper {
max-width: 960px;
margin: 0 auto;
padding: 30px;
background: rgba(0,0,0,0.6);
border-radius: 8px;
line-height: 1.5;
}
.doc-wrapper h2, .doc-wrapper h3, .doc-wrapper h4 {
margin-top: 24px;
color: #fff;
}
.doc-wrapper p {
color: #e3e3e3;
}
.doc-wrapper code {
background: rgba(255,255,255,0.08);
padding: 2px 4px;
border-radius: 4px;
}
pre.code-block {
background: rgba(0,0,0,0.85);
color: #8ef0ff;
padding: 12px;
overflow-x: auto;
border-radius: 6px;
}
ul {
margin-left: 20px;
color: #e3e3e3;
}
</style>
</head>
<body>
<div class="container-wide panel">
<h1>Game Config XML Reference</h1>
<p>
This page mirrors the <a href="https://github.com/OpenGamePanel/OGP-Website/wiki/XML-Notes" target="_blank" rel="noopener noreferrer">
OGP XML Notes</a> so we can edit and review configuration expectations directly inside the repo.
Update <code>modules/billing/docs/XML-Notes.md</code> whenever the upstream wiki changes.
</p>
<div class="doc-wrapper">
<?php echo $docHtml; ?>
</div>
</div>
<?php include(__DIR__ . '/../includes/footer.php'); ?>
</body>
</html>

View file

@ -1,6 +1,6 @@
<?php <?php
// Start a separate session for the website // Start a separate session for the website
session_name("gameservers_website"); session_name("opengamepanel_web");
session_start(); session_start();
// Include database configuration // Include database configuration
@ -291,3 +291,4 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['request_reset'])) {
</body> </body>
<?php include(__DIR__ . '/includes/footer.php'); ?> <?php include(__DIR__ . '/includes/footer.php'); ?>
</html> </html>

View file

@ -1,9 +1,6 @@
<?php <?php
// Admin authorization include — include early (before output) on admin pages // Admin authorization include — include early (before output) on admin pages
if (session_status() === PHP_SESSION_NONE) { require_once(__DIR__ . '/session_bridge.php');
session_name("gameservers_website");
session_start();
}
// If not logged in, redirect to login // If not logged in, redirect to login
if (empty($_SESSION['website_user_id'])) { if (empty($_SESSION['website_user_id'])) {
@ -66,3 +63,5 @@ if (strtolower($role) !== 'admin') {
// If we reach here, user is an admin // If we reach here, user is an admin
?> ?>

View file

@ -1,11 +1,9 @@
<?php <?php
if (session_status() === PHP_SESSION_NONE) { require_once(__DIR__ . '/session_bridge.php');
session_name("gameservers_website");
session_start();
}
// Debugging mode: do not enforce login redirects. Pages can load without authentication. // Debugging mode: do not enforce login redirects. Pages can load without authentication.
// If you later want to re-enable, restore the original redirect behavior. // If you later want to re-enable, restore the original redirect behavior.
// (This file intentionally left as a no-op during debugging.) // (This file intentionally left as a no-op during debugging.)
return; return;
?> ?>

View file

@ -4,11 +4,7 @@
* This file provides a consistent navigation menu across all website pages * This file provides a consistent navigation menu across all website pages
*/ */
// Start the website session to check if user is logged in (if not already started) require_once(__DIR__ . '/session_bridge.php');
if (session_status() === PHP_SESSION_NONE) {
session_name("gameservers_website");
session_start();
}
// Check login status // Check login status
// Primary check uses website_user_id, but some remote deployments may only set website_username. // Primary check uses website_user_id, but some remote deployments may only set website_username.
@ -122,3 +118,4 @@ if ($is_logged_in) {
</nav> </nav>
</div> </div>
</div> </div>

View file

@ -0,0 +1,97 @@
<?php
/**
* Panel bridge helpers for the storefront.
* Provides access to the native OGPDatabase layer, settings, and XML parsers
* without duplicating the panel bootstrap logic in each script.
*/
if (!function_exists('billing_panel_bootstrap')) {
/**
* Initialize the panel runtime and return shared context.
*
* @return array{db:OGPDatabase|null, settings:array, table_prefix:string}|null
*/
function billing_panel_bootstrap()
{
static $context = null;
if ($context !== null) {
return $context;
}
$root = realpath(__DIR__ . '/../../');
if ($root === false) {
error_log('billing_panel_bootstrap: unable to resolve project root');
return null;
}
// Define panel constants if they are not already defined (panel runtime does this for us).
if (!defined('INCLUDES')) {
define('INCLUDES', 'includes/');
}
if (!defined('MODULES')) {
define('MODULES', 'modules/');
}
// Load panel helpers that provisioning logic depends on.
require_once $root . '/includes/functions.php';
require_once $root . '/includes/helpers.php';
require_once $root . '/includes/lib_remote.php';
require_once $root . '/modules/config_games/server_config_parser.php';
// Load panel configuration (db credentials, prefix, etc.)
$configFile = $root . '/includes/config.inc.php';
if (!file_exists($configFile)) {
error_log('billing_panel_bootstrap: missing config file ' . $configFile);
return null;
}
require $configFile;
// Ensure required variables exist before attempting to connect.
if (!isset($db_type, $db_host, $db_user, $db_pass, $db_name, $table_prefix)) {
error_log('billing_panel_bootstrap: config variables not initialized');
return null;
}
$panelDb = createDatabaseConnection($db_type, $db_host, $db_user, $db_pass, $db_name, $table_prefix);
if (!($panelDb instanceof OGPDatabase)) {
error_log('billing_panel_bootstrap: failed to connect to panel database');
return null;
}
$settings = $panelDb->getSettings();
$context = [
'db' => $panelDb,
'settings' => is_array($settings) ? $settings : [],
'table_prefix' => $table_prefix,
];
return $context;
}
}
if (!function_exists('billing_get_panel_db')) {
/**
* Convenience wrapper to fetch the shared OGPDatabase handle.
*
* @return OGPDatabase|null
*/
function billing_get_panel_db()
{
$ctx = billing_panel_bootstrap();
return $ctx['db'] ?? null;
}
}
if (!function_exists('billing_get_panel_settings')) {
/**
* Convenience wrapper to fetch panel settings (time zone, steam creds, etc.).
*
* @return array
*/
function billing_get_panel_settings()
{
$ctx = billing_panel_bootstrap();
return $ctx['settings'] ?? [];
}
}

View file

@ -0,0 +1,32 @@
<?php
/**
* Session bridge to keep panel + storefront logins in sync.
* Always call this before rendering billing pages.
*/
if (session_status() === PHP_SESSION_NONE) {
session_name('opengamepanel_web');
session_start();
}
// If the panel session is populated, mirror into website-specific keys.
if (!empty($_SESSION['user_id']) && empty($_SESSION['website_user_id'])) {
$_SESSION['website_user_id'] = (int)$_SESSION['user_id'];
if (!empty($_SESSION['users_login'])) {
$_SESSION['website_username'] = $_SESSION['users_login'];
}
if (!empty($_SESSION['users_group'])) {
$_SESSION['website_user_role'] = $_SESSION['users_group'];
}
}
// If the website session is populated but the panel keys are missing, mirror back.
if (!empty($_SESSION['website_user_id']) && empty($_SESSION['user_id'])) {
$_SESSION['user_id'] = (int)$_SESSION['website_user_id'];
if (!empty($_SESSION['website_username'])) {
$_SESSION['users_login'] = $_SESSION['website_username'];
}
if (!empty($_SESSION['website_user_role'])) {
$_SESSION['users_group'] = $_SESSION['website_user_role'];
}
}

View file

@ -1,6 +1,6 @@
<?php <?php
// Start a separate session for the website (not the panel session) // Start a separate session for the website (not the panel session)
session_name("gameservers_website"); session_name("opengamepanel_web");
session_start(); session_start();
// Enable error display for debugging the white screen issue. Remove or gate in production. // Enable error display for debugging the white screen issue. Remove or gate in production.
ini_set('display_errors', 1); ini_set('display_errors', 1);
@ -73,34 +73,62 @@ $success_message = '';
// Process login form submission: simplified for debugging // Process login form submission: simplified for debugging
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['login'])) { if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['login'])) {
$username = trim($_POST['ulogin'] ?? ''); $username = trim($_POST['ulogin'] ?? '');
if ($username === '') { $password = $_POST['upassword'] ?? '';
$error_message = 'Please enter a username.'; if ($username === '' || $password === '') {
site_log_warn('login_failed_empty_username', ['ip'=>$_SERVER['REMOTE_ADDR'] ?? '', 'script'=>$_SERVER['SCRIPT_NAME'] ?? '']); $error_message = 'Please enter both a username and password.';
site_log_warn('login_failed_missing_fields', ['ip'=>$_SERVER['REMOTE_ADDR'] ?? '', 'script'=>$_SERVER['SCRIPT_NAME'] ?? '']);
} else { } else {
// Normal operation: create website session (should be set after proper auth) $safe = mysqli_real_escape_string($db, $username);
// In final mode, preserve username but do not fabricate IDs. The site should set website_user_id after proper registration/login. $sql = "SELECT user_id, users_login, users_passwd, users_pass_hash, users_role, users_lang, users_theme FROM {$table_prefix}users WHERE users_login = '$safe' LIMIT 1";
$_SESSION['website_username'] = $username; $res = mysqli_query($db, $sql);
$_SESSION['website_login_time'] = time(); if ($res && mysqli_num_rows($res) === 1) {
// Try to resolve an existing panel user_id by username so the menu and admin checks work. $row = mysqli_fetch_assoc($res);
$resolved_uid = null; $userId = intval($row['user_id']);
if ($db) { $legacyHash = $row['users_passwd'] ?? '';
$safe = mysqli_real_escape_string($db, $username); $modernHash = $row['users_pass_hash'] ?? '';
$res = @mysqli_query($db, "SELECT user_id FROM {$table_prefix}users WHERE users_login = '$safe' LIMIT 1"); $authOk = false;
if ($res && mysqli_num_rows($res) === 1) { if (!empty($modernHash) && function_exists('password_verify')) {
$r = mysqli_fetch_assoc($res); $authOk = password_verify($password, $modernHash);
$resolved_uid = intval($r['user_id'] ?? 0); }
if (!$authOk && !empty($legacyHash)) {
$authOk = (md5($password) === $legacyHash);
if ($authOk && function_exists('password_hash')) {
$newHash = password_hash($password, PASSWORD_DEFAULT);
$escapedHash = mysqli_real_escape_string($db, $newHash);
mysqli_query($db, "UPDATE {$table_prefix}users SET users_pass_hash = '$escapedHash' WHERE user_id = $userId LIMIT 1");
}
}
if ($authOk) {
session_regenerate_id(true);
$_SESSION['user_id'] = $userId;
$_SESSION['users_login'] = $row['users_login'] ?? $username;
$_SESSION['users_passwd'] = $legacyHash;
$_SESSION['users_group'] = $row['users_role'] ?? 'user';
$_SESSION['users_lang'] = $row['users_lang'] ?? '';
$_SESSION['users_theme'] = $row['users_theme'] ?? '';
$_SESSION['website_user_id'] = $userId;
$_SESSION['website_username'] = $row['users_login'] ?? $username;
$_SESSION['website_user_role'] = $row['users_role'] ?? '';
$_SESSION['website_login_time'] = time();
require_once(__DIR__ . '/includes/panel_bridge.php');
$panelCtx = billing_panel_bootstrap();
if ($panelCtx && isset($panelCtx['db']) && $panelCtx['db'] instanceof OGPDatabase) {
$_SESSION['users_api_key'] = $panelCtx['db']->getApiToken($userId);
} else {
$_SESSION['users_api_key'] = $_SESSION['users_api_key'] ?? '';
}
site_log_info('login_success', ['username'=>$username, 'ip'=>$_SERVER['REMOTE_ADDR'] ?? '']);
$returnToParam = $_POST['return_to'] ?? '';
$destination = $sanitize_return_path($returnToParam);
if ($destination === '') {
$destination = $SITE_ROOT_PATH . '/index.php';
}
header('Location: ' . $destination);
exit();
} }
} }
if (!empty($resolved_uid)) { $error_message = 'Invalid username or password.';
$_SESSION['website_user_id'] = $resolved_uid; site_log_warn('login_failed_invalid_credentials', ['username'=>$username, 'ip'=>$_SERVER['REMOTE_ADDR'] ?? '']);
} else {
// Fallback: assign a numeric session id so the menu treats the user as logged in during debugging
$_SESSION['website_user_id'] = time();
}
site_log_info('login_success', ['username'=>$username, 'ip'=>$_SERVER['REMOTE_ADDR'] ?? '']);
// Always redirect to index.php under site root
header('Location: ' . $SITE_ROOT_PATH . '/index.php');
exit();
} }
} }
@ -310,3 +338,4 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['login'])) {
</body> </body>
<?php include(__DIR__ . '/includes/footer.php'); ?> <?php include(__DIR__ . '/includes/footer.php'); ?>
</html> </html>

View file

@ -1,6 +1,6 @@
<?php <?php
// Start the website session // Start the website session
session_name("gameservers_website"); session_name("opengamepanel_web");
session_start(); session_start();
// Logger function // Logger function
@ -30,3 +30,4 @@ $siteRoot = $pos !== false ? substr($script, 0, $pos + strlen('/_website')) : rt
header('Location: ' . $siteRoot . '/index.php'); header('Location: ' . $siteRoot . '/index.php');
exit(); exit();
?> ?>

View file

@ -27,8 +27,15 @@ $module_title = "billing";
$module_version = "3.0"; $module_version = "3.0";
$db_version = 1; $db_version = 1;
$module_required = FALSE; $module_required = FALSE;
// Navigation disabled - this is now a purely external module // Module description
$module_menus = array(); $module_description = "Billing storefront / provisioning integration. Public ordering runs as a standalone site; panel pages provide provisioning and admin order management.";
// Register module menus so panel can show links (user and admin views)
$module_menus = array(
array('subpage' => 'my_orders', 'name' => 'My Orders', 'group' => 'user'),
array('subpage' => 'provision_servers', 'name' => 'Provision Servers', 'group' => 'user'),
array('subpage' => 'admin_orders', 'name' => 'Manage Orders', 'group' => 'admin')
);
$install_queries = array(); $install_queries = array();

View file

@ -9,7 +9,7 @@
<?php <?php
// Start session to check login status // Start session to check login status
if (session_status() === PHP_SESSION_NONE) { if (session_status() === PHP_SESSION_NONE) {
session_name("gameservers_website"); session_name("opengamepanel_web");
session_start(); session_start();
} }
@ -392,3 +392,4 @@ $status_config = [
</body> </body>
<?php include(__DIR__ . '/includes/footer.php'); ?> <?php include(__DIR__ . '/includes/footer.php'); ?>
</html> </html>

View file

@ -1,5 +1,5 @@
<?php <?php
session_name("gameservers_website"); session_name("opengamepanel_web");
session_start(); session_start();
require_once(__DIR__ . '/bootstrap.php'); require_once(__DIR__ . '/bootstrap.php');
@ -70,3 +70,4 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['username']) && !empt
</form> </form>
</body> </body>
</html> </html>

View file

@ -4,7 +4,7 @@
// Require login and configuration // Require login and configuration
if (session_status() === PHP_SESSION_NONE) { if (session_status() === PHP_SESSION_NONE) {
session_name("gameservers_website"); session_name("opengamepanel_web");
session_start(); session_start();
} }
require_once(__DIR__ . '/bootstrap.php'); require_once(__DIR__ . '/bootstrap.php');
@ -277,3 +277,4 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['confirm_renewal'])) {
</body> </body>
<?php include(__DIR__ . '/includes/footer.php'); ?> <?php include(__DIR__ . '/includes/footer.php'); ?>
</html> </html>

View file

@ -1,6 +1,6 @@
<?php <?php
// Start a separate session for the website // Start a separate session for the website
session_name("gameservers_website"); session_name("opengamepanel_web");
session_start(); session_start();
// Include database configuration // Include database configuration
@ -301,3 +301,4 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['reset_password']) &&
</body> </body>
<?php include(__DIR__ . '/includes/footer.php'); ?> <?php include(__DIR__ . '/includes/footer.php'); ?>
</html> </html>

View file

@ -134,7 +134,7 @@ echo "</div>";
// Test 7: Test session functionality // Test 7: Test session functionality
echo "<div class='section'>"; echo "<div class='section'>";
echo "<h2>Test 7: Session Test</h2>"; echo "<h2>Test 7: Session Test</h2>";
session_name("gameservers_website"); session_name("opengamepanel_web");
session_start(); session_start();
$_SESSION['test_key'] = 'test_value'; $_SESSION['test_key'] = 'test_value';
if (isset($_SESSION['test_key']) && $_SESSION['test_key'] === 'test_value') { if (isset($_SESSION['test_key']) && $_SESSION['test_key'] === 'test_value') {
@ -155,3 +155,4 @@ echo "</div>";
echo "</body></html>"; echo "</body></html>";
?> ?>

View file

@ -24,6 +24,205 @@
require_once("server_config_parser.php"); require_once("server_config_parser.php");
function config_games_normalize_path($path)
{
$clean = preg_replace('/[^A-Za-z0-9_\\[\\]\\/\\-]/', '', (string)$path);
return ltrim($clean, '/');
}
function config_games_print_editor_css()
{
static $printed = false;
if ($printed) {
return;
}
$printed = true;
echo <<<CSS
<style>
.xml-editor-wrapper{margin:20px 0;padding:12px;background:#111;border:1px solid #222;border-radius:8px}
.xml-node{border:1px solid #333;border-radius:6px;padding:12px;margin-bottom:10px;background:#181818}
.xml-node__header{display:flex;justify-content:space-between;align-items:center;gap:12px;border-bottom:1px solid #2a2a2a;padding-bottom:6px;margin-bottom:8px}
.xml-node__title{font-weight:600;color:#f5f5f5}
.xml-node__path{font-size:0.85rem;color:#989898}
.xml-node__body label{font-size:0.85rem;color:#bbb;display:block;margin-bottom:4px}
.xml-node__body input[type="text"], .xml-node__body textarea, .xml-node__body select{width:100%;padding:8px;border:1px solid #3a3a3a;border-radius:4px;background:#101010;color:#fff;font-family:monospace}
.xml-node__body textarea{min-height:120px}
.xml-node__attributes{margin-top:8px}
.xml-node__attributes .attr-row{display:flex;gap:8px;align-items:center;margin-bottom:6px}
.xml-node__attributes .attr-row input[type="text"]{flex:1}
.xml-children{margin-top:10px;border-left:2px solid #2a2a2a;padding-left:12px}
.xml-actions{text-align:right;margin-top:16px}
.xml-node__actions{display:flex;gap:8px;align-items:center}
.xml-hint{font-size:0.85rem;color:#999;margin-top:4px}
</style>
CSS;
}
function config_games_render_node(SimpleXMLElement $node, array $ancestors, array &$counters, int $depth = 0)
{
$name = $node->getName();
$pathKey = implode('/', $ancestors) === '' ? $name : implode('/', $ancestors) . '/' . $name;
$counters[$pathKey] = ($counters[$pathKey] ?? 0) + 1;
$index = $counters[$pathKey];
$pathParts = array_merge($ancestors, ["{$name}[{$index}]"]);
$rawPath = implode('/', $pathParts);
$path = config_games_normalize_path($rawPath);
$hasChildren = count($node->children()) > 0;
$value = (string)$node;
$safeLabel = htmlspecialchars($name, ENT_QUOTES, 'UTF-8');
$safePath = htmlspecialchars($path, ENT_QUOTES, 'UTF-8');
$displayPath = htmlspecialchars(str_replace('[', '[', $rawPath), ENT_QUOTES, 'UTF-8');
$isScript = in_array(strtolower($name), ['pre_install','post_install','precmd','postcmd','cli_template']);
$html = "<div class='xml-node depth-{$depth}'>";
$html .= "<div class='xml-node__header'><div><div class='xml-node__title'>{$safeLabel}</div><div class='xml-node__path'>{$displayPath}</div></div>";
$html .= "<div class='xml-node__actions'><label>Action</label><select name=\"nodes[{$safePath}][action]\"><option value='keep'>Keep</option><option value='remove'>Remove</option></select></div></div>";
$html .= "<div class='xml-node__body'>";
$html .= "<input type='hidden' name=\"nodes[{$safePath}][path]\" value=\"{$safePath}\">";
$html .= "<input type='hidden' name=\"nodes[{$safePath}][has_children]\" value=\"" . ($hasChildren ? '1' : '0') . "\">";
if (!$hasChildren || $isScript) {
$safeValue = htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
if ($isScript || strlen($value) > 120) {
$html .= "<label>Value</label><textarea name=\"nodes[{$safePath}][value]\">{$safeValue}</textarea>";
} else {
$html .= "<label>Value</label><input type='text' name=\"nodes[{$safePath}][value]\" value=\"{$safeValue}\">";
}
} elseif (trim($value) !== '') {
$safeValue = htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
$html .= "<label>Inner Text</label><textarea name=\"nodes[{$safePath}][value]\">{$safeValue}</textarea>";
$html .= "<p class='xml-hint'>This element contains nested tags; clearing the text does not remove children.</p>";
}
$attributes = $node->attributes();
if ($attributes && count($attributes) > 0) {
$html .= "<div class='xml-node__attributes'><strong>Attributes</strong>";
foreach ($attributes as $attrName => $attrValue) {
$attrSafe = htmlspecialchars($attrName, ENT_QUOTES, 'UTF-8');
$valSafe = htmlspecialchars((string)$attrValue, ENT_QUOTES, 'UTF-8');
$html .= "<div class='attr-row'><span>{$attrSafe}</span><input type='text' name=\"nodes[{$safePath}][attributes][{$attrSafe}]\" value=\"{$valSafe}\" placeholder='Leave blank to remove'></div>";
}
$html .= "<div class='attr-row'><input type='text' name=\"nodes[{$safePath}][new_attribute][name]\" placeholder='New attribute name'><input type='text' name=\"nodes[{$safePath}][new_attribute][value]\" placeholder='New attribute value'></div>";
$html .= "</div>";
} else {
$html .= "<div class='xml-node__attributes'><div class='attr-row'><input type='text' name=\"nodes[{$safePath}][new_attribute][name]\" placeholder='Attribute name'><input type='text' name=\"nodes[{$safePath}][new_attribute][value]\" placeholder='Attribute value'></div></div>";
}
if ($hasChildren) {
$html .= "<div class='xml-children'>";
foreach ($node->children() as $child) {
$html .= config_games_render_node($child, array_merge($ancestors, ["{$name}[{$index}]"]), $counters, $depth + 1);
}
$html .= "</div>";
}
$html .= "</div></div>";
return $html;
}
function config_games_render_editor(SimpleXMLElement $xml)
{
config_games_print_editor_css();
$rootName = $xml->getName();
$html = "<div class='xml-editor-wrapper'>";
$counters = [];
foreach ($xml->children() as $child) {
$html .= config_games_render_node($child, [$rootName], $counters);
}
$html .= "</div>";
return $html;
}
function config_games_save_xml($db, $home_cfg_id, array $nodesPayload)
{
$cfg_info = $db->getGameCfg($home_cfg_id);
if ($cfg_info === FALSE) {
return false;
}
$config_file = SERVER_CONFIG_LOCATION . $cfg_info['home_cfg_file'];
if (!file_exists($config_file) || !is_readable($config_file)) {
return false;
}
$nodes = [];
foreach ($nodesPayload as $path => $data) {
$cleanPath = config_games_normalize_path($path);
if ($cleanPath === '') {
continue;
}
$nodes[$cleanPath] = $data;
}
if (empty($nodes)) {
return false;
}
$dom = new DOMDocument();
$dom->preserveWhiteSpace = false;
$dom->formatOutput = true;
if (@$dom->load($config_file) === false) {
return false;
}
$xpath = new DOMXPath($dom);
uksort($nodes, function ($a, $b) {
return substr_count($b, '/') <=> substr_count($a, '/');
});
foreach ($nodes as $path => $nodeData) {
$query = '/' . $path;
$nodeList = @$xpath->query($query);
if (!$nodeList || $nodeList->length === 0) {
continue;
}
$domNode = $nodeList->item(0);
$action = $nodeData['action'] ?? 'keep';
if ($action === 'remove') {
if ($domNode->parentNode) {
$domNode->parentNode->removeChild($domNode);
}
continue;
}
$hasChildren = !empty($nodeData['has_children']);
if (array_key_exists('value', $nodeData)) {
while ($domNode->firstChild) {
$domNode->removeChild($domNode->firstChild);
}
if ($nodeData['value'] !== '') {
$domNode->appendChild($dom->createTextNode($nodeData['value']));
}
} elseif (!$hasChildren) {
while ($domNode->firstChild) {
$domNode->removeChild($domNode->firstChild);
}
}
if (isset($nodeData['attributes']) && is_array($nodeData['attributes'])) {
foreach ($nodeData['attributes'] as $attrName => $attrValue) {
$attrNameClean = preg_replace('/[^A-Za-z0-9_\\-:]/', '', (string)$attrName);
if ($attrNameClean === '') {
continue;
}
$attrValue = trim((string)$attrValue);
if ($attrValue === '') {
$domNode->removeAttribute($attrNameClean);
} else {
$domNode->setAttribute($attrNameClean, $attrValue);
}
}
}
if (isset($nodeData['new_attribute']['name']) && $nodeData['new_attribute']['name'] !== '') {
$newName = preg_replace('/[^A-Za-z0-9_\\-:]/', '', (string)$nodeData['new_attribute']['name']);
$newValue = (string)($nodeData['new_attribute']['value'] ?? '');
if ($newName !== '' && $newValue !== '') {
$domNode->setAttribute($newName, $newValue);
}
}
}
if ($dom->save($config_file) === false) {
return false;
}
$config = read_server_config($config_file);
if ($config !== FALSE) {
$db->addGameCfg($config);
}
return true;
}
function exec_ogp_module() { function exec_ogp_module() {
global $db,$view; global $db,$view;
@ -91,6 +290,17 @@ function exec_ogp_module() {
print_success(get_lang('configs_updated_ok')); print_success(get_lang('configs_updated_ok'));
} }
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save_xml']) && isset($_POST['home_cfg_id'])) {
$edit_id = (int)$_POST['home_cfg_id'];
$nodesPayload = isset($_POST['nodes']) && is_array($_POST['nodes']) ? $_POST['nodes'] : [];
if (config_games_save_xml($db, $edit_id, $nodesPayload)) {
print_success(get_lang('configs_updated_ok'));
} else {
print_failure('Failed to save XML configuration.');
}
$_GET['home_cfg_id'] = $edit_id;
}
$game_cfgs = $db->getGameCfgs(); $game_cfgs = $db->getGameCfgs();
echo "<table class='center'>\n echo "<table class='center'>\n
@ -165,31 +375,19 @@ function exec_ogp_module() {
{ {
echo "<a href='?m=config_games&home_cfg_id=".$home_cfg_id."&delete'>".get_lang_f('delete_game_config_for',$cfg_info['game_name']." $os $arch")."</a><br>"; echo "<a href='?m=config_games&home_cfg_id=".$home_cfg_id."&delete'>".get_lang_f('delete_game_config_for',$cfg_info['game_name']." $os $arch")."</a><br>";
$configs = read_server_config($config_file); $xml = @simplexml_load_file($config_file);
echo "<table>\n"; if ($xml === false) {
foreach( $configs as $key => $value ) print_failure(get_lang_f("error_when_handling_file",$config_file));
{ } else {
echo "<tr><td><b>$key<b></td><td colspan=25 >$value</td></tr>\n"; echo "<form action='?m=config_games&amp;home_cfg_id=".$home_cfg_id."' method='post'>";
foreach($value as $subkey => $subvalue ) echo "<input type='hidden' name='home_cfg_id' value='".(int)$home_cfg_id."'>";
{ echo "<button type='submit' name='save_xml' value='1' style='float:right;margin-bottom:10px;'>".get_lang('save')."</button>";
echo "<tr><td><b>$subkey<b></td><td>$subvalue</td>\n"; echo "<div style='clear:both'></div>";
echo config_games_render_editor($xml);
list($attributes,$attrvalue)=each($subvalue); echo "<div class='xml-actions'><button type='submit' name='save_xml' value='1'>".get_lang('save')."</button></div>";
echo "</form>";
foreach($attrvalue as $attrkey => $attrval) echo "<p class='note'>Use the action dropdown to remove entire sections. Attribute values left blank will be removed. Script sections such as post_install are fully editable.</p>";
{
echo "<td><b>$attrkey<b></td><td>$attrval</td>\n";
}
echo "</td>";
foreach($subvalue as $option => $options )
{
echo "<td><b>$option<b></td><td>$options</td>\n";
}
}
echo "</tr>\n";
} }
echo "</table>\n";
} }
} }
} }