forked from sbpp/sourcebans-pp
-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathinit.php
More file actions
355 lines (313 loc) · 15.6 KB
/
init.php
File metadata and controls
355 lines (313 loc) · 15.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
<?php
// SourceBans++ (c) 2014-2026 SourceBans++ Dev Team
// Licensed under the Elastic License 2.0.
// See LICENSE.txt for the full license text and THIRD-PARTY-NOTICES.txt for attributions.
use Smarty\Smarty;
// ---------------------------------------------------
// Directories
// ---------------------------------------------------
define('ROOT', dirname(__FILE__) . "/");
define('SCRIPT_PATH', ROOT . 'scripts');
define('TEMPLATES_PATH', ROOT . 'pages');
define('INCLUDES_PATH', ROOT . 'includes');
define('SB_MAP_LOCATION', 'images/maps');
define('SB_DEMO_LOCATION', 'demos');
define('SB_ICON_LOCATION', 'images/games');
define('SB_MAPS', ROOT . SB_MAP_LOCATION);
define('SB_DEMOS', ROOT . SB_DEMO_LOCATION);
define('SB_ICONS', ROOT . SB_ICON_LOCATION);
define('SB_THEMES', ROOT . 'themes/');
define('SB_CACHE', ROOT . 'cache/');
define("MMDB_PATH", ROOT . 'data/GeoLite2-Country.mmdb');
define('IN_SB', true);
// ---------------------------------------------------
// Are we installed? (Issue #1335 M1 + C1)
// ---------------------------------------------------
// Pre-#1335 these guards were three bare `die('text')` calls. The
// CTA on the wizard's done page sends the operator straight here
// before they delete `install/` or paste in the manual config
// snippet, so the bare-text 200 response read like a server crash.
// The recovery surface is loaded eagerly so each branch can call
// `sbpp_render_install_blocked_page()` (a `: never` HTML render)
// without each guard re-implementing inline HTML.
//
// `web/init-recovery.php` lives outside `web/install/` because the
// `'install'` scenario fires precisely when `install/` exists and
// the operator hasn't deleted it yet — we don't want that surface
// to depend on files inside the directory it's nudging the operator
// to remove. The file has zero `Sbpp\…` / Composer / Smarty
// dependencies (mirror of `web/install/recovery.php`'s contract);
// the panel runtime hits it BEFORE the `vendor/autoload.php`
// require below.
require_once(ROOT.'/init-recovery.php');
#DB Config
//
// `SBPP_CONFIG_PATH` env var (#1381 deliverable 4d): the production
// Docker image lets operators mount config.php from a Docker secret
// path outside the read-only image layer. Falls back to the legacy
// `<panel-root>/config.php` for tarball / wizard installs that don't
// set the var. See sbpp_resolve_config_path() in init-recovery.php
// for the contract.
$sbppConfigPath = sbpp_resolve_config_path(ROOT . 'config.php');
if (!file_exists($sbppConfigPath)) {
// M1 bonus: redirect to /install/ instead of a bare-text die.
// The wizard is the actionable next step for a panel without
// config.php, so dropping the operator there is strictly more
// helpful than a stark `die()`. `install/` may have been
// deleted post-install (the desired state once config.php
// exists), so this redirect only fires on a genuine "no
// panel here yet" first hit.
header('Location: install/');
exit;
}
require_once($sbppConfigPath);
// SBPP_TRUSTED_PROXIES (#1381 CRIT-4): operator-defined list of
// CIDR ranges whose `X-Forwarded-Proto` / `X-Forwarded-For` the
// panel will trust. Format: whitespace-separated list of IP
// literals or CIDR ranges (IPv4 + IPv6), e.g.
// `'10.0.0.0/8 192.168.0.0/16 ::1'`. Empty / undefined disables
// XFP / XFF consultation entirely.
//
// Resolution order:
// 1. `config.php` `define('SBPP_TRUSTED_PROXIES', ...)` — wins.
// 2. `SBPP_TRUSTED_PROXIES` env var (the Docker prod image
// exports this in `configure_apache`).
// 3. Empty string — the secure default; `Host::isSecure()`
// ignores `X-Forwarded-Proto` and trusts only the
// authoritative `$_SERVER['HTTPS']` (which `mod_remoteip`
// + `SetEnvIfExpr` populate server-side after the proxy
// hop is validated by Apache).
if (!defined('SBPP_TRUSTED_PROXIES')) {
$envProxies = getenv('SBPP_TRUSTED_PROXIES');
define('SBPP_TRUSTED_PROXIES', is_string($envProxies) ? $envProxies : '');
}
// Issue #1335 C1: pre-fix this guard exempted `HTTP_HOST ==
// "localhost"`, which was a panel-takeover path on any panel
// reachable via a `localhost` Host header (port-forward, SSH
// tunnel, ngrok, Cloudflare Tunnel) and on any local-development
// workflow where the operator never saw the warning they'd need to
// act on once they deployed. The exemption is gone — the install /
// updater directories are post-install / post-upgrade artefacts
// that MUST be deleted on production panels.
//
// `SBPP_DEV_KEEP_INSTALL` is the explicit dev-only escape hatch:
// the project's `docker/php/dev-prepend.php` defines it on every
// request inside the dev container so the bind-mounted worktree
// (which carries `install/` + `updater/` from git) doesn't fail
// the guard. Production panels MUST NOT define this constant —
// see `sbpp_check_install_guard()`'s docblock for the contract.
// PHPStan sees `dev-prepend.php`'s `define(SBPP_DEV_KEEP_INSTALL,
// true)` and would otherwise flag any `=== true` check here as
// `identical.alwaysTrue`. The function takes a bool either way, so
// the `defined()` check IS the gate — there's no path that defines
// the constant to anything other than the literal `true`, and the
// loud name makes accidental production-side defines visibly wrong.
$blocked = sbpp_check_install_guard(
ROOT,
defined('IS_UPDATE'),
defined('SBPP_DEV_KEEP_INSTALL'),
);
if ($blocked !== null) {
sbpp_render_install_blocked_page($blocked);
}
#Composer autoload
if (!file_exists(INCLUDES_PATH.'/vendor/autoload.php')) {
sbpp_render_install_blocked_page('autoload');
}
require_once(INCLUDES_PATH.'/vendor/autoload.php');
// ---------------------------------------------------
// Initial setup
// ---------------------------------------------------
// All classes below now live under Sbpp\… namespaces (issue #1290 phase B)
// and are PSR-4 autoloaded from web/includes/. The require_once chain is
// retained so each file's class_alias() shim runs eagerly — the legacy
// global names (`Database`, `CUserManager`, `Auth`, `Log`, `CSRF`, …) need
// to be registered before procedural code references them, and the
// autoloader can't trigger those aliases on a global-name lookup. New
// code can reference the namespaced symbols (e.g. `Sbpp\Db\Database`)
// directly without any explicit require.
require_once(INCLUDES_PATH.'/Security/Crypto.php');
require_once(INCLUDES_PATH.'/Security/CSRF.php');
require_once(INCLUDES_PATH.'/Auth/JWT.php');
require_once(INCLUDES_PATH.'/Auth/Handler/NormalAuthHandler.php');
require_once(INCLUDES_PATH.'/Auth/Handler/SteamAuthHandler.php');
require_once(INCLUDES_PATH.'/Auth/Auth.php');
require_once(INCLUDES_PATH.'/Auth/Host.php');
require_once(INCLUDES_PATH.'/Auth/UserManager.php');
require_once(INCLUDES_PATH.'/View/AdminTabs.php');
// Three-tier version resolution (#1207 CC-5): tarball JSON, git describe,
// then the literal 'dev' sentinel. See \Sbpp\Version::resolve() for the
// full rationale; this block just unpacks the result into the constants
// the chrome and views consume. \Sbpp\Version is PSR-4 autoloaded via
// composer (Sbpp\\ -> includes/), so no explicit require is needed.
$version = \Sbpp\Version::resolve(ROOT . 'configs/version.json');
define('SB_VERSION', $version['version']);
define('SB_GITREV', $version['git']);
// ---------------------------------------------------
// Setup our DB
// ---------------------------------------------------
// utf8mb4 is the project-wide default so multi-byte player names (CJK,
// Cyrillic, emoji) survive inserts. The narrower `utf8` alias is the 3-byte
// subset MariaDB kept for back-compat. The updater wizard at
// `web/updater/data/600.php` already converts every table with
// `ALTER TABLE … CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci`
// and rewrites `config.php` to define `DB_CHARSET = 'utf8mb4'`, so any
// operator who has run the updater past version 600 is already on
// utf8mb4. This define is the safety net for the (unlikely) case of a
// `config.php` written without the constant — it does NOT override an
// operator who explicitly set `'utf8'` in their config.
if (!defined('DB_CHARSET')) {
define('DB_CHARSET', 'utf8mb4');
}
if (!defined('SB_EMAIL')) {
define('SB_EMAIL', '');
}
require_once(INCLUDES_PATH.'/Db/Database.php');
$GLOBALS['PDO'] = new Database(DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASS, DB_PREFIX, DB_CHARSET);
require_once(INCLUDES_PATH.'/SteamID/bootstrap.php');
require_once(INCLUDES_PATH.'/Config.php');
Config::init($GLOBALS['PDO']);
define("DEBUG_MODE", Config::getBool('config.debug'));
if (DEBUG_MODE) {
ini_set('display_errors', 1);
error_reporting(E_ALL ^ E_NOTICE);
}
Auth::init($GLOBALS['PDO']);
// ---------------------------------------------------
// Setup our user manager
// ---------------------------------------------------
$userbank = new CUserManager(Auth::verify());
// ---------------------------------------------------
// Bind a CSRF token to the session (must run before any form is
// rendered so the token is available, and before any state-changing
// API request reaches the dispatcher).
// ---------------------------------------------------
CSRF::init();
require_once(INCLUDES_PATH.'/LogType.php');
require_once(INCLUDES_PATH.'/LogSearchType.php');
require_once(INCLUDES_PATH.'/BanType.php');
require_once(INCLUDES_PATH.'/BanRemoval.php');
require_once(INCLUDES_PATH.'/WebPermission.php');
require_once(INCLUDES_PATH.'/Log.php');
Log::init($GLOBALS['PDO'], $userbank);
// Api / ApiError are loaded here (rather than at the top of the require
// chain) because Sbpp\Api\Api `use Sbpp\Log;` and the legacy alias for
// Log only exists once Log.php has been required above. ApiError must
// come first: Sbpp\Api\Api references it internally (Api::error() etc.).
// Without these requires the legacy global aliases (`Api`, `ApiError`)
// would only resolve when web/api.php's own require_once fires — page
// handlers that call `Api::redirect()` outside the JSON dispatcher
// would die at runtime even though phpstan-bootstrap.php loads them
// eagerly and the analyser would be happy.
require_once(INCLUDES_PATH.'/Api/ApiError.php');
require_once(INCLUDES_PATH.'/Api/Api.php');
// ---------------------------------------------------
// Setup our custom error handler
// ---------------------------------------------------
set_error_handler('sbError');
function sbError($errno, $errstr, $errfile, $errline)
{
// Map E_USER_* into a (log-level, log-title, error-word) triplet so the
// dispatch is one table read; a `default => null` arm preserves the
// legacy switch's "unknown errno → return false" fall-through. `match`
// arms can't host `Log::add(...) + return true` directly because the
// expression must yield a single value — the logging side effect runs
// outside the match below the lookup.
$entry = match ($errno) {
E_USER_ERROR => [LogType::Error, 'PHP Error', 'Fatal Error'],
E_USER_WARNING => [LogType::Warning, 'PHP Warning', 'Error'],
E_USER_NOTICE => [LogType::Message, 'PHP Notice', 'Notice'],
default => null,
};
if ($entry === null) {
return false;
}
[$logLevel, $logTitle, $errorWord] = $entry;
Log::add($logLevel, $logTitle, "[$errno] $errstr\n$errorWord on line $errline in file $errfile");
return true;
}
$webflags = json_decode(file_get_contents(ROOT.'/configs/permissions/web.json'), true);
foreach ($webflags as $flag => $perm) {
define($flag, $perm['value']);
}
$smflags = json_decode(file_get_contents(ROOT.'/configs/permissions/sourcemod.json'), true);
foreach ($smflags as $flag => $perm) {
define($flag, $perm['value']);
}
define('SB_BANS_PER_PAGE', Config::get('banlist.bansperpage'));
define('MIN_PASS_LENGTH', Config::get('config.password.minlength'));
// ---------------------------------------------------
// Setup our templater
// ---------------------------------------------------
global $theme, $userbank;
$theme_name = (Config::getBool('config.theme')) ? Config::get('config.theme') : 'default';
if (defined("IS_UPDATE")) {
$theme_name = "default";
}
define('SB_THEME', $theme_name);
if (!@file_exists(SB_THEMES . $theme_name . "/theme.conf.php")) {
die("Theme Error: <b>".$theme_name."</b> is not a valid theme. Must have a valid <b>theme.conf.php</b> file.");
}
if (!@is_writable(SB_CACHE)) {
die("Theme Error: <b>".SB_CACHE."</b> MUST be writable.");
}
require_once(INCLUDES_PATH.'/SmartyCustomFunctions.php');
$theme = new Smarty();
$theme->setErrorReporting(E_ALL);
$theme->setUseSubDirs(false);
$theme->setCompileId($theme_name);
$theme->setCaching(Smarty::CACHING_OFF);
$theme->setTemplateDir(SB_THEMES . $theme_name);
$theme->setCacheDir(SB_CACHE);
$theme->setEscapeHtml(true);
$theme->registerPlugin(Smarty::PLUGIN_FUNCTION, 'load_template', 'smarty_function_load_template');
$theme->registerPlugin(Smarty::PLUGIN_FUNCTION, 'csrf_field', 'smarty_function_csrf_field');
$theme->registerPlugin(Smarty::PLUGIN_BLOCK, 'has_access', 'smarty_block_has_access');
$theme->registerPlugin('modifier', 'smarty_stripslashes', 'smarty_stripslashes');
$theme->registerPlugin('modifier', 'smarty_htmlspecialchars', 'smarty_htmlspecialchars');
$theme->assign('csrf_token', CSRF::token());
$theme->assign('csrf_field_name', CSRF::FIELD_NAME);
// Public web path to the active theme directory (e.g. "themes/default").
// Templates use it to reference theme-local CSS / JS / fonts / images
// without hardcoding the theme name. SB_THEMES is an absolute filesystem
// path; the public-facing equivalent is just "themes/<theme>" because
// web/index.php is the document root.
$theme->assign('theme_url', 'themes/' . $theme_name);
if ((isset($_GET['debug']) && $_GET['debug'] == 1) || DEBUG_MODE) {
$theme->setForceCompile(true);
}
// Anonymous opt-out daily telemetry (#1126). Registered last so the
// settings cache, version constants, theme name, and DB are all warm
// — Telemetry::tickIfDue() reads each of them. Both index.php and
// api.php require_once init.php, so registering once here covers
// the panel + JSON API surfaces. tickIfDue() wraps its own body in
// try/catch(\Throwable), so a misbehaving collector or a flapping
// endpoint never leaks an exception out of the shutdown function.
// On FPM, Telemetry calls fastcgi_finish_request() before the cURL
// POST so the user's TCP socket closes first; non-FPM SAPIs fall
// back to ob_end_flush + flush.
register_shutdown_function([\Sbpp\Telemetry\Telemetry::class, 'tickIfDue']);
// Daily project-announcements feed (admins-only banner on the home
// dashboard). Source of truth: `docs/public/announcements.json` in
// this repo, deployed to https://sbpp.github.io/announcements.json
// on every push to main.
//
// `SB_ANNOUNCEMENTS_URL` is the operator-overridable destination —
// the `if (!defined(...))` gate lets `config.php` win, including
// the "" empty-string air-gap escape hatch documented in
// `docs/src/content/docs/configuring/announcements.mdx`. Empty
// short-circuits every fetch before flushing the response, so
// disabling the feed costs nothing.
//
// Same shutdown-hook contract as Telemetry above: 24h TTL gate
// inside tickIfDue() means at most one outbound fetch per install
// per day regardless of request volume; outer try/catch(\Throwable)
// guarantees a page render or JSON API call NEVER fails because
// the upstream is flapping; fastcgi_finish_request() flushes the
// response before the network round-trip on FPM. See
// AnnouncementFetcher.php for the full lifecycle. Page renders
// read the cache only — never block on the network.
if (!defined('SB_ANNOUNCEMENTS_URL')) {
define('SB_ANNOUNCEMENTS_URL', 'https://sbpp.github.io/announcements.json');
}
register_shutdown_function([\Sbpp\Announce\AnnouncementFetcher::class, 'tickIfDue']);