getStreamingUserInfo used the older GeoIP stack (GeoIPService + the boot-loaded
$rBlockedISP/$rBlockedServers globals + BlocklistService::checkISP/checkServer)
while getUserInfo used the newer one (GeoIP + BlocklistService::getBlocked*).
The two are functionally equivalent - same MaxMind DBs, same on-disk caches,
identical block-check logic, same blocked_isp/blocked_servers data - so
standardise getStreamingUserInfo on the newer stack, gaining GeoIP's
defined()-guard + try/catch and BlocklistService's DB fallback / 20s refresh.
With the ISP and forced_country blocks now identical across both methods, hoist
them into applyIspInfo() / applyForcedCountry(). getStreamingUserInfo becomes a
~15-line orchestration of shared helpers and getUserInfo collapses to the thin
wrapper its docblock always claimed to be. Drops the last global-state
dependency and the now-unused GeoIPService import.
Behaviour preserved. Validated live: player_api/testxc auth=1, Active,
allowed_output_formats intact (the pre-existing GENERATE_PLAYLIST_FAILED on this
test box reproduces identically on the prior commit - unrelated).
Verified: php -l, phpunit (435 tests, 973 assertions), make gates.
Signals — the file-backed queue of deferred DB writes drained by the
cache-handler daemon — were produced two different ways: five call sites went
through RedisManager::setSignal() while getStreamingUserInfo inlined the same
file_put_contents(). Worse, the writer lived on RedisManager yet wrote a *file*,
not Redis — a misleading home that made the two paths look like different
transports (they were byte-identical).
Introduce XcVm\Infrastructure\Signal\SignalQueue as the single owner of the
on-disk contract (cache_<md5(key)> holding [key, data]):
- push() - the one producer API; all 7 call sites use it now
(UserRepository x4, auth.php x2, BruteforceGuard x1)
- pending() - the drain API; CacheHandlerCommand reads through it instead of its
own glob + json_decode
- pathFor() / PREFIX - path helpers
RedisManager::setSignal() stays as a thin @deprecated alias delegating to
SignalQueue::push() for any out-of-tree callers.
Behaviour unchanged (same files, same format, same consumer switch). Adds
SignalQueueTest (format / idempotency / drain / malformed). Validated live:
player_api/testxc still authenticates (auth=1) with SignalQueue autoloaded.
Verified: php -l, phpunit (435 tests, 973 assertions), make gates.
The two methods were ~90% duplicate, differing only in how they obtain
settings/cache/bouquets and in the divergent GeoIP + signal backends
(GeoIPService/file-signals vs GeoIP/Redis) that stay inline. Extract the
identical blocks into private helpers, called by both:
loadUserRow - credential resolution (token/cache file or DB),
resolving $rUserID by reference so the cached
re-verification keeps its exact behaviour
verifyCachedCredentials - cached access-token / username+password re-check
decodeUserFields - JSON line fields -> arrays
resolveOutputFormats - allowed output-format keys
aggregateBouquetIds - bouquet -> channel/series/vod/live/radio id lists
resolveCategoryIds - bouquet -> category ids
Also de-obfuscate the remaining `if (cond) {} else { body }` blocks in both
methods. Net -74 lines despite adding the shared helpers (~240 lines of
duplication removed). Behaviour preserved; the file-vs-Redis signal divergence
is intentionally left as-is (separate decision).
Tests: UserRepositoryTest covers the pure helpers (aggregate/category/decode/
verify), 12 tests total. Validated live: player_api/testxc returns auth=1,
Active, allowed_output_formats=[m3u8,ts,rtmp]; on-demand /live start still works.
Verified: php -l, phpunit (430 tests, 962 assertions), make gates.
- getE2Info: collapse the obfuscated `if (cond) {} else { body }` chain into
straight positive-form ifs; drop the redundant re-init of pair_line_info.
- getUserInfo: same de-obfuscation of the ISP block, and fix the same
"Undefined array key isp_asn" bug already fixed in getStreamingUserInfo — the
ISP-persist step ran even on a GeoIP miss (con_isp_name null), reading the
undefined isp_asn key and writing a null isp_desc/as_number to the line.
- Extract the persist predicate both methods shared into a pure, tested
UserRepository::ispChanged() helper (+ tests/Unit/UserRepositoryTest.php).
Verified: php -l, phpunit (423 tests, 944 assertions), make gates.
getStreamingUserInfo() sets isp_asn / con_isp_name only when
GeoIPService::getISP() actually returns an ISP. The follow-up block that
persists a changed ISP still ran when none was detected (con_isp_name stays
null): it read the undefined isp_asn key — the "Undefined array key isp_asn"
warning from UserRepository.php:348 — and wrote a null isp_desc/as_number to the
line. Gate it on a non-empty con_isp_name, matching the isp_violate check above.
Replace the fragile per-class setDb()/db() pattern (which threw when a
bootstrap path forgot to wire $db) with a shared trait that lazily resolves
from DatabaseFactory. Fixes the streaming UserRepository fatal and the same
latent issue in module crons. 42 classes converted.
LoadBalancer nginx had no ^/admin/ location, so when a server higher in
the tree pulled a stream whose source is an LB, the LB returned 404 for
/admin/{live,timeshift,thumb,vod} before the request ever reached PHP.
The MAIN→LB direction worked (MAIN has the route); LB→MAIN was silently
broken — stream shown down, nothing in the panel logs.
Add a reduced admin location exposing ONLY the loopback handlers
(live|timeshift|thumb|vod) — not index|api|proxy_api — so the admin
panel itself is never served from a LoadBalancer. Mirrors the MAIN
admin gateway block.
Also fix a PSR-4 rename miss: the directory rename public/ → Public/
(1a296d09) updated src/bin/nginx/conf/nginx.conf but left
lb_configs/nginx.conf pointing at /home/xc_vm/public/ everywhere
(root, stream, progress, api). On a case-sensitive fresh LB install the
on-disk path is Public/, so every SCRIPT_FILENAME 404'd — all LB
streaming broke, not just loopback. Repoint the 8 filesystem paths to
Public/; SCRIPT_NAME stays /public/ (logical CGI path, matches MAIN).
Note: bin/nginx is update-excluded, so existing LBs keep their old
config and must be patched manually or reinstalled; only fresh installs
pick up the corrected lb_configs/nginx.conf.
Apply 'make cs-fix' — 493 files. Mechanical, import-block only:
- sort use statements alphabetically (class/function/const grouped);
- drop imports left unused by the PSR-4 migration (e.g. classes referenced by
leading-backslash FQCN whose redundant 'use' the automated insertion had added);
- one blank line after namespace and after the import block; collapse stray
blank lines around use.
No logic changes. Verified: php -l clean; PHPStan no errors; PHPUnit 295/295; and a
temporary PHPStan pass over src/Public/Controllers confirms no still-referenced
import was removed (0 unresolved classes). 'use' after inline HTML in view
templates is valid and aliases correctly (verified) — those imports are sorted too.
Move Core/Database into XcVm\Core\Database (DatabaseHandler, Database,
MigrationRunner, QueryHelper). First of the Core hub sub-layers.
- Namespace the 4 classes; DatabaseHandler extends Database (same namespace);
built-ins/ioncube qualified (\PDO, \PDOException, \Exception, \Throwable,
\XC_VM); Database keeps its 'use XcVm\Core\Logging\FileLogger;'.
- Add 'use XcVm\Core\Database\...;' to referencing files (DatabaseHandler 51,
Database 64, QueryHelper 29, MigrationRunner 3) + 2 test files (PHPStan does not
analyse tests/, so PHPUnit is the gate there).
- Rewrite pre-existing leading-backslash global refs (\DatabaseHandler etc., e.g.
in @param docblocks of ResellerApiDispatcher/ResellerTableRenderer) to the full
FQCN \XcVm\Core\Database\... — a 'use' import does not cover a leading-\
reference. Done with a lookbehind so FQCN continuations and use-lines are intact.
- phpstan-baseline.neon regenerated (292→292; pre-existing Database/migration_logic
findings re-anchored after class names in messages gained the namespace).
Verified: php -l clean; PHPStan no errors; PHPUnit 295/295.