Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 146 additions & 21 deletions AGENTS.md

Large diffs are not rendered by default.

37 changes: 28 additions & 9 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,13 +115,16 @@ web/
├── configs/permissions/ web.json + sourcemod.json — bitmask flag definitions
├── tests/ PHPUnit (api/ for handlers, integration/ for flows)
├── bin/ CLI tools (currently just generate-api-contract.php)
├── init-recovery.php Panel-runtime guard helpers + friendly error pages (#1335 C1 + M1)
├── install/ Install wizard self-hosters run on a fresh setup (#1332)
│ ├── index.php Entry point — paths-init → vendor/-check (recovery short-circuit) → bootstrap → dispatch
│ ├── index.php Entry point — paths-init → already-installed gate (#1335 C2) → vendor/-check (recovery short-circuit) → bootstrap → dispatch
│ ├── init.php Paths-only bootstrap (NEVER touches vendor/)
│ ├── bootstrap.php Composer + Smarty bootstrap (loaded only when vendor/ is present)
│ ├── recovery.php Self-contained "vendor/ missing" surface (pure inline HTML + CSS)
│ ├── pages/page.<N>.php Per-step page handlers (1=licence, …, 6=optional AMXBans import)
│ ├── already-installed.php Self-contained "panel already installed" guard (#1335 C2 — pure inline HTML + CSS)
│ ├── pages/page.<N>.php Per-step page handlers (1=license, …, 6=optional AMXBans import)
│ ├── includes/routing.php Step → page-handler dispatch
│ ├── includes/helpers.php Shared step-handler helpers (prefix validation, raw-PDO probe, KV escape, PDO error translation)
│ └── includes/sql/ struc.sql + data.sql — the schema source of truth
├── updater/ Per-version migrations existing installs run after upgrade
├── phpstan.neon PHPStan level 5 + custom rules + dba bootstrap
Expand All @@ -148,8 +151,15 @@ Both scripts include `init.php` first, which performs identical bootstrap.

1. Defines path constants (`ROOT`, `INCLUDES_PATH`, `TEMPLATES_PATH`, …)
and the `IN_SB` sentinel that page files check.
2. Bails if `config.php` is missing or if the `install/` or `updater/`
directories are present and the host isn't `localhost`.
2. Redirects to `/install/` if `config.php` is missing (the
wizard is the canonical fresh-install path; #1335 M1 replaced
the bare-text `die()`). If `install/` or `updater/` are still
on disk after a successful install/upgrade, refuses to boot via
`web/init-recovery.php`'s `sbpp_check_install_guard()` —
unconditional in production, with a single explicit
`SBPP_DEV_KEEP_INSTALL` opt-in for the docker dev stack
(#1335 C1; the loopback / `HTTP_HOST` exemption was a
panel-takeover path and is gone).
3. Loads Composer autoload (`includes/vendor/autoload.php`).
4. Manually requires the auth + security + Database modules and
initialises them. The classes themselves ARE PSR-4 namespaced
Expand Down Expand Up @@ -914,17 +924,23 @@ model:
- **`docker/php/web-entrypoint.sh`** waits for MariaDB, renders
`web/config.php` from env vars (only if absent), runs `composer install`
if `vendor/` is empty, then `exec`s Apache.
- **`docker/php/dev-prepend.php`** rewrites `HTTP_HOST` to `localhost`
for any loopback request so `init.php`'s install-folder guard accepts
the forwarded `:8080` port.
- **`docker/php/dev-prepend.php`** defines `SBPP_DEV_KEEP_INSTALL`
on every request so `web/init-recovery.php`'s
`sbpp_check_install_guard()` skips the install/ + updater/-presence
refusal — the dev stack ships those directories on the bind mount
by design (the wizard isn't exercised locally; `docker/db-init/`
seeds the schema out of band). Pre-#1335 this file rewrote
`HTTP_HOST` to `localhost` to ride a `init.php` exemption that
was a panel-takeover path in production; the explicit
loud-named-define escape hatch replaced it.
- **`docker/db-init/00-render-schema.sh`** runs once on first DB boot:
substitutes `{prefix}` / `{charset}` in `struc.sql` + `data.sql`,
loads them, and seeds an `admin` row with bcrypt of `admin`.
- **`sbpp.sh`** is a thin wrapper around `docker compose` plus the
quality gates and DB tasks. Run `./sbpp.sh -h` for the full menu.

The seeded admin password and the `HTTP_HOST` shim are dev-only and
documented as such in `docker-compose.yml`.
The seeded admin password and the `SBPP_DEV_KEEP_INSTALL` constant are
dev-only and documented as such in `docker-compose.yml`.

## Quality gates

Expand Down Expand Up @@ -1074,3 +1090,6 @@ but don't bulk-rewrite legacy code without justification.
| `htmlspecialchars_decode` on JSON params | Store raw UTF-8; Smarty auto-escape handles display (#1108) |
| `DB_CHARSET = 'utf8'` (3-byte alias) | `utf8mb4` end-to-end (panel PDO + plugin `SET NAMES`) (#1108)|
| TinyMCE WYSIWYG for `dash.intro.text` | Plain `<textarea>` + `Sbpp\Markup\IntroRenderer` (CommonMark, escape unsafe HTML) (#1113) |
| `init.php`'s `HTTP_HOST != 'localhost'` exemption on the install/ + updater/-presence guard | Unconditional guard via `web/init-recovery.php`'s `sbpp_check_install_guard()`; docker dev rides explicit `SBPP_DEV_KEEP_INSTALL` constant (#1335 C1) |
| Bare-text `die()` in `init.php` for missing-config / install-still-present / autoload-missing paths | Self-contained chrome via `web/init-recovery.php`'s `sbpp_render_install_blocked_page()` (mirror of `recovery.php`'s pure-inline-HTML contract) (#1335 M1) |
| `/install/` walkable on a panel where `config.php` already exists | Wizard refuses to start via `web/install/already-installed.php`'s 409 surface (#1335 C2) |
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ MariaDB, including shared hosting (cPanel, DirectAdmin, Plesk).
to your web root (`public_html/`, `htdocs/`, `www/`, …) via your
host's File Manager or any FTP/SFTP client. The plugin tarball
goes onto your game server under `addons/sourcemod/`.

If the wizard's environment check (next step) flags any folder
as "Not writable", set permissions to `0775` (or `0777` on shared
hosts where you don't control the PHP user) on `web/demos/`,
`web/cache/`, `web/images/games/`, and `web/images/maps/`. Most
hosts let you do this through File Manager → Properties; over
SSH it's `chmod -R 0775 web/{demos,cache,images}`.
4. **Run the installer.** Visit `https://your-panel-url/install/` in
a browser and follow the wizard — license, database details,
environment check, schema install, admin account, done.
Expand Down
7 changes: 4 additions & 3 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
# ./sbpp.sh logs # tail web/db logs
# open http://localhost:8080 # panel — login admin / admin
#
# This file is for development only. Do NOT use it in production: it disables
# the install-folder safety check, ships a known admin password, and exposes
# the database port to the host.
# This file is for development only. Do NOT use it in production: it ships a
# known admin password, exposes the database port to the host, and the dev
# image defines `SBPP_DEV_KEEP_INSTALL` (issue #1335 C1) so init.php's
# install/ + updater/ presence guard skips during dev.

services:
web:
Expand Down
6 changes: 3 additions & 3 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ COPY --from=composer:2 /usr/bin/composer /usr/bin/composer

# Sensible PHP defaults for development: errors on, generous limits, opcache
# revalidates on every request so file edits are picked up immediately.
# auto_prepend_file normalizes HTTP_HOST so init.php's "delete install/"
# check (which only matches a bare "localhost") doesn't trip when serving on
# localhost:8080.
# auto_prepend_file defines `SBPP_DEV_KEEP_INSTALL` (issue #1335 C1) so
# init.php's install/ + updater/ presence guard skips during dev — the
# bind-mounted worktree carries both directories from git.
COPY docker/php/dev-prepend.php /usr/local/etc/php/dev-prepend.php
RUN { \
echo 'memory_limit=256M'; \
Expand Down
36 changes: 26 additions & 10 deletions docker/php/dev-prepend.php
Original file line number Diff line number Diff line change
@@ -1,14 +1,30 @@
<?php
// Auto-prepended on every request inside the dev container.
//
// init.php has a guard:
// if ($_SERVER['HTTP_HOST'] != "localhost" && !defined("IS_UPDATE")) {
// if (file_exists(ROOT."/install")) { die('Please delete the install directory'); }
// }
// That bare-string match doesn't accept "localhost:8080" or "127.0.0.1", so
// the panel refuses to load. Strip the port for any loopback host so the
// guard sees the value it expects without weakening it for real deployments.
if (isset($_SERVER['HTTP_HOST'])
&& preg_match('/^(localhost|127\.0\.0\.1|\[::1\])(:\d+)?$/i', $_SERVER['HTTP_HOST'])) {
$_SERVER['HTTP_HOST'] = 'localhost';
// Issue #1335 C1: pre-#1335 init.php exempted `HTTP_HOST == "localhost"`
// from the install/ + updater/ presence guard, and this file's job was
// to rewrite `HTTP_HOST` to drop the port (`localhost:8080` -> `localhost`)
// so the bare-string match would accept dev requests. That entire
// shape was a panel-takeover path — anyone reaching a production
// panel with a `localhost` Host header (port-forward, SSH tunnel,
// ngrok, Cloudflare Tunnel) bypassed the guard.
//
// The post-#1335 contract: init.php's guard is unconditional, with a
// single explicit dev-only escape hatch. Defining `SBPP_DEV_KEEP_INSTALL`
// here tells `sbpp_check_install_guard()` to skip the install/ +
// updater/ presence check. The constant is loud-named so a
// production-side define is visibly wrong; the panel's release
// tarball has no path to set it; only the dev container's
// `auto_prepend_file` ini directive (configured in
// `docker/Dockerfile`) actually defines it.
//
// The dev container needs the escape hatch because the worktree is
// bind-mounted into the panel's web root and includes both
// `install/` and `updater/` from git. Deleting either is not an
// option in dev — the docker-compose dev stack seeds the DB out of
// band (the wizard isn't exercised), but `install/` itself stays in
// place so wizard development happens against the same files that
// ship to production.
if (!defined('SBPP_DEV_KEEP_INSTALL')) {
define('SBPP_DEV_KEEP_INSTALL', true);
}
Loading
Loading