Overhaul the Docsify documentation (English + Russian) so it matches the current codebase and follows one consistent pattern. Content accuracy (post-migration): - Rewrite development/autoloader.md to PSR-4 / Composer (the old XC_Autoloader scanner, igbinary tmp/cache/autoload_map and registerDirectories are gone). - PascalCase every source path (src/core -> src/Core, domain/Stream, cli/Commands, public/Controllers, Infrastructure/Redis, ...) across all docs. - Replace the removed autoload.php references with vendor/autoload.php (build_system, bootstrap-contexts, error-handling, modules). - ssl-generation: note that the installer now auto-generates a unique self-signed certificate before Nginx starts. Common pattern (Clean & uniform): - Strip emoji from headings; remove the in-page Navigation blocks (the Docsify sidebar already provides navigation). - One H1 + intro per doc; uniform "Related files" / "Связанные файлы" section, added to the code-centric docs that lacked it. Structure: - Remove the empty stray docs/api/; move updates_checklist.md into builds/; link the previously-orphaned ucs-integration.md. - Regroup the sidebars (split the oversized guides group into Developer Guides / Security & Access / Integrations; fold builds into Build & Release). Augment: - dev-workflow: Local Setup (make dev-tools) + Quality Checks (phpstan, cs, gates). - build_system: Composer Dependencies section (committed prod-only vendor, committed lock, dev tools via composer install, no build-time vendor step). en/ru parity: - Apply the same structure, fixes and pattern to docs/ru/ (translated), including a new Russian ucs-integration.md. The en and ru file sets are now identical.
17 KiB
HTTP Request Handling
This document describes how HTTP requests are processed in XC_VM, covering the full lifecycle from initial entry through routing and dispatch. There are multiple runtime paths depending on the request type.
Overview
The HTTP layer is built from these core components:
| Component | File | Role |
|---|---|---|
RequestGuard |
src/Core/Http/RequestGuard.php |
Pre-routing safety: flood protection, host verification, logger init |
InputValidator |
src/Core/Validation/InputValidator.php |
Input sanitization (cleanGlobals, parseIncomingRecursively) |
RequestManager |
src/Core/Http/RequestManager.php |
Static facade storing merged GET+POST request data |
Request |
src/Core/Http/Request.php |
Object-oriented request wrapper (exists but not used in the primary production flow) |
Router |
src/Core/Http/Router.php |
Page and API route registration and dispatch |
Response |
src/Core/Http/Response.php |
Static response helpers (JSON, redirect, CORS, etc.) |
LegacyInitializer |
src/Core/Init/LegacyInitializer.php |
Legacy bootstrap that wires sanitization into RequestManager |
StreamingRequestBootstrap |
src/Infrastructure/Bootstrap/StreamingRequestBootstrap.php |
Lightweight bootstrap for streaming endpoints |
Request Flow: Admin/Panel Pages
Entry point: src/Public/index.php
nginx -> Public/index.php
-> URL parsing (scope + pageName)
-> XC_Bootstrap::boot(CONTEXT_ADMIN)
-> floodProtection() (block banned IPs)
-> hostVerification() (check allowed domains)
-> initSession()
-> initDatabase()
-> initLegacyCore()
-> LegacyInitializer::initCore()
-> InputValidator::cleanGlobals($_GET, $_POST, $_SESSION, $_COOKIE)
-> InputValidator::parseIncomingRecursively($_GET) -> $rInput
-> InputValidator::parseIncomingRecursively($_POST, $rInput) -> RequestManager::set()
-> initRedis()
-> initAdminAPI()
-> initTranslator()
-> Load routes from src/Public/routes/{scope}.php
-> Load routes from src/Public/routes/api.php
-> ModuleLoader::bootAll() (admin/reseller scope, with collision detection)
-> Router::dispatchApi($action) (checked first for "api" page)
-> Router::dispatch($pageName, $method)
-> Controller handler
Key detail: input sanitization
The production admin flow does NOT use Request::capture(). Instead, LegacyInitializer::initCore() drives input processing:
InputValidator::cleanGlobals()is called on$_GET,$_POST,$_SESSION, and$_COOKIEin place, stripping NULL bytes, path traversal sequences (../), and RTL override characters.InputValidator::parseIncomingRecursively()sanitizes keys and values (HTML entities, script tags, comment delimiters, line endings) and returns a clean array.- The result (GET merged with POST, POST taking priority) is stored via
RequestManager::set().
Throughout the codebase, request data is accessed via RequestManager::get($key) and RequestManager::getAll(), not through the Request object.
Request Flow: REST API
Entry point: src/Public/index.php (short-circuits before Router)
When XC_SCOPE is includes/api/admin or includes/api/reseller:
nginx -> Public/index.php
-> XC_Bootstrap::boot(CONTEXT_ADMIN)
-> new AdminApiController() or new ResellerRestApiController()
-> $controller->index()
-> exit
This path bypasses the Router entirely.
Request Flow: Streaming
Entry point: www/stream/*.php endpoints, or Public/index.php for player_api
nginx -> StreamingRequestBootstrap::init($filename)
-> Load error codes, paths, config, binaries
-> Flood protection (check block_{IP} file)
-> Load settings from file cache
-> Host verification
-> Logger init
-> StreamingBootstrap::bootstrap($filename, $settings)
-> LegacyInitializer::initStreaming()
-> Request::cleanGlobals() on superglobals
-> Request::parseIncomingRecursively() -> $GLOBALS['rRequest']
-> RequestManager::set($GLOBALS['rRequest'])
The streaming path is deliberately lightweight. It does not load the Router, EventDispatcher, Translator, or full ServiceContainer. There is no route dispatch; each streaming endpoint has a dedicated entry point.
RequestGuard
File: src/Core/Http/RequestGuard.php
A procedural guard script included early in the legacy bootstrap. Runs only for HTTP requests (skipped when $_SERVER['argc'] is set, indicating CLI).
Responsibilities
- Flood protection -- If the file
FLOOD_TMP_PATH/block_{IP}exists, the request is rejected with HTTP 403. - Settings cache load -- Reads
$rSettingsfrom the igbinary-serialized file cache atCACHE_TMP_PATH/settings. - Host verification -- When
$rSettings['verify_host']is true, checks thatHOSTappears in the cachedallowed_domainslist. Exceptions: the hostnamexc_vmand any valid IP address are always allowed. - Error display flag -- Sets the
PHP_ERRORSconstant from$rSettings['debug_show_errors']. - Logger initialization -- Calls
Logger::init(PHP_ERRORS, LOGS_TMP_PATH . 'error_log.log').
Note: In the modern bootstrap (XC_Bootstrap::boot()), these responsibilities are handled by floodProtection() and hostVerification() methods directly, not by including RequestGuard.php.
InputValidator
File: src/Core/Validation/InputValidator.php
Provides static methods for input sanitization and action-level validation.
Sanitization methods
| Method | Description |
|---|---|
cleanGlobals(&$data, $iteration) |
In-place removal of NULL bytes, path traversal (../), and RTL override chars. Max 10 levels of recursion. |
parseIncomingRecursively(&$data, $input, $iteration) |
Returns a new sanitized array. Cleans both keys and values. Max 20 levels of recursion. |
parseCleanKey($key) |
Strips double dots, __wrapped__ patterns, applies htmlspecialchars(urldecode()). |
parseCleanValue($value) |
Strips <script tags, HTML comments, normalizes line breaks, handles entity encoding. |
Action validation
| Method | Description |
|---|---|
validate($action, $data) |
Returns true/false for whether $data meets minimum requirements for the given API action. |
validateOrFail($action, $data) |
Returns null if valid, or ['status' => STATUS_INVALID_INPUT, 'data' => $data] if invalid. |
confirmIDs($ids) |
Filters an array to only positive integer IDs. |
RequestManager
File: src/Core/Http/RequestManager.php
Static facade that stores the merged GET+POST request data. This is the primary request data access pattern used throughout the codebase.
How data gets in
LegacyInitializer::initCore() calls:
$rInput = InputValidator::parseIncomingRecursively($_GET, array());
RequestManager::set(InputValidator::parseIncomingRecursively($_POST, $rInput));
POST parameters override GET parameters with the same key (POST is merged on top of GET).
API
| Method | Description |
|---|---|
set(array $request) |
Store the entire request data array. |
getAll() |
Retrieve all stored request data. |
get(string $key, $default = null) |
Retrieve a single value by key. |
update(string $key, $value) |
Update a single key in the stored data. |
Usage
// Read a request parameter
$streamId = RequestManager::get('stream_id');
// Read all parameters
$allParams = RequestManager::getAll();
// Update a value (rare, used by some legacy handlers)
RequestManager::update('status', 'active');
Request
File: src/Core/Http/Request.php
Object-oriented request wrapper. Contains a static capture() factory and instance methods for accessing sanitized input. While the class exists and is fully functional, the primary production flow uses InputValidator + RequestManager instead. The Request class's static sanitization methods (cleanGlobals, parseIncomingRecursively) are used by LegacyInitializer::initStreaming() for backward compatibility.
Construction
// Static factory (singleton, not used in production admin flow)
$request = Request::capture();
// Direct construction
$request = new Request($_GET, $_POST, $_SERVER, $_COOKIE);
Instance methods
| Method | Signature | Description |
|---|---|---|
input |
input($key, $default = null) |
Get from merged input (POST priority over GET) |
get |
get($key = null, $default = null) |
Get from query string ($_GET). null key returns all. |
post |
post($key = null, $default = null) |
Get from POST data. null key returns all. |
all |
all() |
All merged input data |
has |
has($key) |
Check if key exists in merged input |
getInt |
getInt($key, $default = 0) |
Get value as integer |
getBool |
getBool($key, $default = false) |
Get value as boolean (via filter_var) |
server |
server($key, $default = null) |
Get $_SERVER value |
cookie |
cookie($key, $default = null) |
Get cookie value |
method |
method() |
HTTP method string (GET, POST, etc.) |
isPost |
isPost() |
Check if method is POST |
isAjax |
isAjax() |
Check X-Requested-With: XMLHttpRequest |
ip |
ip() |
Client IP (checks X-Forwarded-For, X-Real-IP, REMOTE_ADDR) |
uri |
uri() |
Request URI |
userAgent |
userAgent() |
User-Agent header |
host |
host() |
Host header (falls back to SERVER_NAME) |
rawBody |
rawBody() |
Raw POST body from php://input |
json |
json($assoc = true) |
JSON-decoded POST body |
Static sanitization methods (backward compatibility)
These mirror InputValidator and are used by the streaming init path:
| Method | Description |
|---|---|
cleanGlobals(&$data, $iteration) |
Same as InputValidator::cleanGlobals() |
parseIncomingRecursively(&$data, $input, $iteration) |
Same as InputValidator::parseIncomingRecursively() |
parseCleanKey($key) |
Same as InputValidator::parseCleanKey() |
parseCleanValue($value) |
Same as InputValidator::parseCleanValue() |
Router
File: src/Core/Http/Router.php
Singleton router for page and API dispatch. Replaces the legacy switch($rAction) pattern.
Route registration
| Method | Signature | Description |
|---|---|---|
get |
get($route, $handler, $options = []) |
Register a GET page route |
post |
post($route, $handler, $options = []) |
Register a POST form route |
any |
any($route, $handler, $options = []) |
Register both GET and POST for the same route |
api |
api($action, $handler, $options = []) |
Register an API route (JSON, dispatched by action name) |
group |
group($prefix, $callback, $options = []) |
Group routes under a common prefix with shared middleware/permissions |
The $handler parameter accepts:
[ClassName::class, 'method']-- instantiated via ServiceContainer (with fallback tonew)- A closure or callable
[object, 'method']
The $options array supports:
'permission' => ['type', 'key']-- checked viaAuthorization::check()before the handler runs'middleware' => [callable, ...]-- array of callables executed after permission check, before handler
Route examples
$router = Router::getInstance();
// Simple page routes
$router->get('streams', [StreamController::class, 'index']);
$router->post('stream/save', [StreamController::class, 'save']);
// API route (JSON)
$router->api('deleteStream', [StreamController::class, 'apiDelete']);
// Grouped routes with middleware and permissions
$router->group('watch', function (Router $r) {
$r->get('', [WatchController::class, 'index']);
$r->get('add', [WatchController::class, 'add']);
$r->post('settings', [WatchController::class, 'saveSettings']);
$r->api('enable', [WatchController::class, 'apiEnable']);
}, [
'permission' => ['admin', 'watch'],
'middleware' => [$authCheck],
]);
Route normalization
The router normalizes legacy page names by converting underscores to slashes:
| Input | Normalized |
|---|---|
watch |
watch |
watch_add |
watch/add |
settings_watch |
settings/watch |
plex_add.php |
plex/add |
This normalization is applied both at registration time (buildRoute) and at dispatch time (normalizePage), so routes registered as watch/add match page names like watch_add.
Dispatch
// Page dispatch (called from Public/index.php)
$router->dispatch($pageName, $method); // returns true if matched
// API dispatch (called for action= parameter)
$router->dispatchApi($action); // returns true if matched
dispatch($page, $method) execution order
- Normalize
$page(underscores to slashes, strip.php). - Look up in POST routes (if method is POST) or GET routes. If POST route not found, falls back to GET routes.
- Permission check via
checkPermission(). If denied, callsdenyAccess()(redirect or 403). - Middleware execution. Each callable in the
middlewarearray is called. If any returnsfalse, execution stops. - Handler invocation via
callHandler().
dispatchApi($action) execution order
- Look up in API routes by action name.
- Permission check. If denied, outputs
{"result": false}and exits. - Handler invocation. No middleware is executed.
Important: dispatchApi() does NOT run middleware. This is a deliberate difference from page dispatch.
Module route registration
Modules register routes via ModuleInterface::registerRoutes(). The router supports a safe registration mode to prevent modules from overwriting core routes:
$router->beginModuleRegistration();
// Module routes registered here -- duplicates are silently skipped
$moduleLoader->bootAll($container, $router);
$router->endModuleRegistration();
// Check for collisions (logged in development mode)
$collisions = $router->drainRouteCollisions();
During module registration mode (preserveExistingRoutes = true), if a module tries to register a route that already exists, the existing route is preserved and the collision is recorded. drainRouteCollisions() returns and clears the collected collisions as an array of ['type' => 'get'|'post'|'api', 'key' => 'route/path'].
Introspection
| Method | Description |
|---|---|
hasRoute($page) |
Check if a page route exists (GET or POST) |
hasApiRoute($action) |
Check if an API route exists |
getRoutes() |
Return all registered route keys as ['get' => [...], 'post' => [...], 'api' => [...]] |
Response
File: src/Core/Http/Response.php
Static helper for sending HTTP responses. Replaces scattered header() + echo + exit() patterns.
| Method | Signature | Description |
|---|---|---|
json |
json($data, $statusCode = 200, $options = 0) |
Send JSON response and exit |
jsonError |
jsonError($message, $statusCode = 400, $extra = []) |
Send JSON error and exit |
redirect |
redirect($url, $statusCode = 302) |
Send redirect and exit |
notFound |
notFound($message = 'Not Found') |
Send 404 and exit |
header |
header($name, $value) |
Set a single response header |
cors |
cors() |
Set CORS headers (Access-Control-Allow-Origin: *) |
noCache |
noCache() |
Set no-cache headers (used for HLS playlists) |
raw |
raw($content, $contentType, $statusCode) |
Send raw content with content type and exit |
empty |
empty($statusCode = 204) |
Send empty response and exit |
Bootstrap Contexts
XC_Bootstrap::boot($context) provides context-dependent initialization. Each context builds on the previous:
| Context | What it initializes |
|---|---|
CONTEXT_MINIMAL |
Autoload + constants + config + Logger. No DB connection. |
CONTEXT_CLI |
+ Database + LegacyInitializer::initCore() (input sanitization, settings, FFmpeg paths). Optional Redis. |
CONTEXT_STREAM |
+ Database only (lightweight, no LegacyInitializer). Streaming endpoints use StreamingRequestBootstrap instead. |
CONTEXT_ADMIN |
+ Session + Database + LegacyInitializer::initCore() + Redis + Admin API + Translator + admin globals. Full initialization. |
All HTTP contexts (not CLI) also run flood protection and host verification before context-specific initialization.
Related files
| File | Purpose |
|---|---|
src/Core/Http/RequestGuard.php |
Pre-routing safety and logger init (legacy include) |
src/Core/Http/Request.php |
OOP request wrapper with sanitization methods |
src/Core/Http/Router.php |
Route registration and dispatch |
src/Core/Http/RequestManager.php |
Static request data facade (primary access pattern) |
src/Core/Http/Response.php |
Response output helpers |
src/Core/Validation/InputValidator.php |
Input sanitization and action validation |
src/Core/Init/LegacyInitializer.php |
Legacy core initialization (wires sanitization into RequestManager) |
src/Infrastructure/Bootstrap/StreamingRequestBootstrap.php |
Lightweight streaming endpoint bootstrap |
src/Streaming/StreamingBootstrap.php |
Streaming DB connection and legacy init |
src/bootstrap.php |
Unified bootstrap (XC_Bootstrap class) |
src/Public/index.php |
Front controller for admin/reseller/player/API |
src/Public/routes/admin.php |
Admin page route definitions |
src/Public/routes/reseller.php |
Reseller page route definitions |
src/Public/routes/player.php |
Player page route definitions |