Commit Graph
19 Commits
Author SHA1 Message Date
Divarion_D b8325fc19e feat(active-codes): Smart Activation Codes system
Pre-generated stock voucher codes that pair with an auto-created line and
count down only on the client's first activation. Full admin + reseller
management — list, generate wizard, batch manager, mass edit — plus an
activation portal and a public activation API.

- schema: activation_codes table (migration 019 + database.sql)
- domain: ActiveCodeService (generate / activate / mass actions / export /
  batch summary; view queries live here, not in the templates)
- admin + reseller controllers and views; PortalController + activation API
- nginx routes for /active_code.php and the active_code API endpoint
- wired into auth, users, reseller dispatcher, table/player APIs and navbar

Passes phpstan, phpcs, make gates, and the unit suite (524 tests).
2026-09-11 19:08:20 +03:00
Divarion_D 512a7a2985 fix: resolve all PHPStan errors and remove the frozen baseline
Drop build/phpstan-baseline.neon (122 frozen pre-existing errors) and fix
the underlying issues in source instead of suppressing them. A fresh
level-5 run without the baseline surfaced 158 errors across 56 files; all
are resolved. `make phpstan`, `make cs`, and `make gates` are green.

Fix categories:
- variable.undefined — initialize vars to a correct default before the
  branch/loop that conditionally set them, so every path defines them.
- return.type — align declared return types with reality (widen to
  array|false / ?array / \Movie|null etc. where callers handle the
  sentinel, or return the declared type consistently).
- argument.type — cast at call sites (curl_setopt/stream_set_blocking bool
  flags, str_replace/mktime/uniqid operands, \CurlHandle phpdoc).
- redundant/dead conditions — simplify always-true guards and drop
  unreachable else branches, preserving behavior.

Real latent bugs caught along the way:
- Core/Http/CurlClient — retry loop never incremented on failure and never
  broke on success (could spin); now retries as documented.
- Core/Util/StreamUtils — explode('=', $x, 1) meant path/domain query
  params were never parsed (limit 1 -> 2).
- Cli/CronJobs/RootSignalsCronJob — set_governor emitted an undefined PHP
  $i into the cpufreq-set bash command (now a literal shell $i).

Root-cause analyzer fixes (not suppressions):
- Database::ping()/num_rows() marked @phpstan-impure so repeated calls are
  not treated as constant (clears the ResellerApiDispatcher false cluster).
- StreamService::getArchive() $rReturn given an explicit @var for the
  loop-accumulated shape PHPStan cannot infer from array().

Two residual entries are documented false positives (analyzer limitations,
not bugs) as path-scoped ignoreErrors in phpstan.dist.neon:
- CacheHandlerCommand:66 — settings force-reloaded from DB mid-run.
- EpgCronJob:314 — reconnect-verify ping() after db_connect().

Also drops an unused `use ...Epg\EPG;` import in admin/api.php.
2026-08-27 15:24:41 +03:00
Divarion_D 80c54a0940 fix: harden null/missing-key access across stream + provider paths
Fixes warnings/exceptions from production panel logs (v2.3.9), mostly
unguarded access to keys of external provider payloads and to files that
can vanish under races:

- ProvidersCronJob: suppress file_get_contents noise on unreachable
  providers (readURL), and normalise each stream row with defaults
  (`+= [...]`) so missing stream_id/category_id/name/stream_icon/
  epg_channel_id/container_extension no longer warn; skip rows with no
  stream_id. Guard category_id/category_name in the category feeds.
- StreamRedirector: guard the cached stream_ file read (missing cache
  file no longer warns or produces a half-built $rStream), and default
  info.direct_source/direct_proxy.
- stream/probe.php: guard $rChannelInfo (redirectStream may return false)
  and the decoded stream_info before reading codecs/container/bitrate.
- admin/live.php: @filesize on a segment .ts that may be rotated away
  mid-read.
- PlayerApiController: default parse_url()['path'/'host'] and
  $_SERVER['HTTP_USER_AGENT'].
- UserRepository::applyIspInfo: initialise isp_asn default so it is never
  read undefined when the GeoIP lookup returns no ISP.
- admin/providers.php: default max_connections/active_connections for
  providers whose last fetch failed.
2026-08-16 00:14:42 +03:00
Divarion_D 67df99efa2 refactor(user): drop the unused $rGetBouquetInfo param from getE2Info
getE2Info() never referenced its third parameter; the only caller
(XPluginApiController) passes just $rDevice and relies on defaults, so removing
the dead middle argument shifts nothing.

Verified: php -l, phpunit (435 tests), make gates.
2026-08-09 13:42:21 +03:00
Divarion_D 222e914453 refactor(user): move getStreamingUserInfo to the GeoIP stack; getUserInfo -> wrapper
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.
2026-08-09 13:34:32 +03:00
Divarion_D 2f8b36d1f3 refactor(signal): unify deferred-work signals behind SignalQueue
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.
2026-08-09 13:01:57 +03:00
Divarion_D 8f72218ea6 refactor(user): consolidate getStreamingUserInfo/getUserInfo shared blocks
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.
2026-08-09 01:19:04 +03:00
Divarion_D d9138f228a refactor(user): simplify getE2Info/getUserInfo, fix the isp_asn miss bug
- 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.
2026-08-09 01:03:16 +03:00
Divarion_D 7c66939d49 fix(stream): guard the ISP-update block when no ISP was detected
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.
2026-08-09 00:54:21 +03:00
Divarion-D 57e4805769 refactor: remove unused Database imports across multiple services 2026-06-28 20:45:17 +03:00
Divarion-D a9cf7a5266 refactor(database): centralize domain DB access in DatabaseAware trait
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.
2026-06-28 13:48:35 +03:00
Divarion-D f829bd5672 fix(nginx): add LB loopback admin routes and correct Public docroot case
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.
2026-06-26 20:02:28 +03:00
Divarion-D 9eec63937e style: import/namespace hygiene across src (PHP-CS-Fixer)
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.
2026-06-25 20:24:36 +03:00
Divarion-D aaa8e2a23a chore(psr4): phase 3 — namespace Infrastructure layer
Namespace the Infrastructure classes and PascalCase its subdirectories for PSR-4
consistency with Core/Domain:
- git mv bootstrap/→Bootstrap/, cache/→Cache/, database/→Database/, redis/→Redis/;
  update the require paths to the procedural Bootstrap/*.php glue files.
- Namespace the 7 classes: StreamingRequestBootstrap, WebApiBootstrap →
  XcVm\Infrastructure\Bootstrap; CacheReader → \Cache; DatabaseFactory →
  \Database; RedisManager → \Redis; ResellerApiDispatcher, ResellerTableRenderer
  → XcVm\Infrastructure. The procedural Bootstrap glue files stay global.
- Qualify built-ins/ioncube (\Redis, \RedisException, \DateTime, \Exception,
  \XC_VM) and still-global \StreamingBootstrap; add use to referrers; convert
  the leading-backslash \CacheReader/\DatabaseFactory/\RedisManager refs from
  earlier commits to FQCNs.
- deleted_files.txt: old lowercase subdir paths for client cleanup.
- phpstan-baseline.neon regenerated.

Verified: php -l clean; PHPStan no errors; PHPUnit 295/295; re-sweep clean.
2026-06-25 18:45:19 +03:00
Divarion-D 42d9ca8ea0 chore(psr4): phase 3 — namespace Domain layer
Namespace all of Domain into XcVm\Domain\<Subdir> (33 classes across Bouquet,
Device, Epg, Line, Security, Server, Stream, User, Vod).

- Add namespace to every Domain class; add use to referrers across src/ and
  tests/; convert the leading-backslash refs qualified in earlier Core commits
  (\UserRepository, \ServerRepository, \BouquetService, \CategoryService,
  \ConnectionTracker, \BlocklistService, ...) to their FQCNs.
- Qualify still-global / built-in deps inside Domain with leading backslash
  (\RedisManager, \TMDB, \WatchService, \FFprobeRunner, \ProcessChecker,
  \Exception, \DateTime, \PDO, ...) — Infrastructure/Streaming/module classes
  migrate later.
- BruteforceGuard: string guards class_exists('ServerRepository'/'BlocklistService')
  → ::class FQCN; drop the now-always-true method_exists check.
- phpstan-baseline.neon regenerated (292→291).

Verified: php -l clean; PHPStan no errors; PHPUnit 295/295; leading-backslash
re-sweep across Domain classes is clean.
2026-06-25 18:39:28 +03:00
Divarion-D da291e20f8 chore(psr4): phase 3 (Core) — namespace remaining Core subdirs
Namespace the rest of Core, completing the Core layer:
- Backup, Boundary, Cache (CacheInterface/FileCache/RedisCache), Diagnostics,
  GeoIP (GeoIPService/MaxMindUpdater), Init (LegacyInitializer), Localization
  (Translator), Process (Thread/Multithread/ProcessManager), Updates
  (GitHubReleases), Util (NetworkUtils, AdminHelpers, Encryption, GeoIP,
  ImageUtils/ImageResizeService, Mobile_Detect, StreamUtils, SystemInfo,
  TimeUtils), Validation (InputValidator) → XcVm\Core\<Subdir>.
- Rename GithubReleases.php → GitHubReleases.php so the file matches its class
  name (PSR-4 is case-sensitive); fix the WebApiBootstrap require accordingly.
- Qualify built-ins (\Exception, \DateTime, \DateTimeZone, \Redis,
  \RuntimeException, \BadMethodCallException, ...) and still-global deps
  (\ServerRepository, \CacheReader, \DatabaseFactory, \BouquetService,
  \CategoryService, \FfmpegPaths, \StreamSorter, \XC_VM). LegacyInitializer's
  Domain calls stay \-qualified (the known Core→Domain exception).
- Add use to referrers; fix leading-backslash refs from earlier commits; fix
  string class refs class_exists('Translator'/'NetworkUtils'/...) → ::class.
  Remove the duplicate global 'use BoundaryInterface;' in MinistraModule.
- phpstan-baseline.neon regenerated.

Core is now fully namespaced (procedural constant/error/RequestGuard files stay
global by design; M3uParser/PhpM3u8 remain vendored). Verified: php -l clean;
PHPStan no errors; PHPUnit 295/295; leading-backslash re-sweep clean.
2026-06-24 22:35:33 +03:00
Divarion-D 160adc1439 chore(psr4): phase 3 (Core hubs) — namespace Core/Auth
Move Core/Auth into XcVm\Core\Auth (Authenticator, Authorization, AuthRepository,
AuthService, BruteforceGuard, PageAuthorization, SessionManager).

- Namespace the 7 classes; same-namespace siblings unqualified; migrated deps
  (Logger, ApiClient, QueryHelper, SettingsManager, RequestManager) keep their
  existing use imports. Qualify still-global deps with leading backslash
  (\UserRepository, \NetworkUtils, \ServerRepository, \BlocklistService,
  \DatabaseFactory, \RedisManager, \AdminHelpers, \Encryption, \CodeService,
  \CodeRepository) — these migrate later with Domain/Util.
- Add 'use XcVm\Core\Auth\...;' to referencing files (Authorization 69,
  AuthRepository 24, BruteforceGuard 12, SessionManager/PageAuthorization 9,
  Authenticator 5, AuthService 3) across src/ and tests/.
- Fix pre-existing leading-backslash refs (e.g. Router's \Authorization, now
  \XcVm\Core\Auth\Authorization). Preserve class_exists('NetworkUtils',false)
  defensive string guards as-is (still-global classes).
- phpstan-baseline.neon regenerated (292→292; AuthRepository::getHMACById message
  re-anchored with namespace, no new/unknown-class errors).

Verified: php -l clean; PHPStan no errors; PHPUnit 295/295; Auth classes resolve
via Composer; leading-backslash re-sweep clean.
2026-06-24 21:42:30 +03:00
Divarion-D 42db4be509 chore(psr4): phase 3 (Core hubs) — namespace Core/Database
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.
2026-06-24 21:15:06 +03:00
Divarion-D 1a296d0985 chore(psr4): phase 1 — rename 7 top-level dirs to PascalCase
Atomic case-rename of the seven class-holding source directories to match the
PSR-4 namespace casing, plus every consequent path reference. No code logic
changes — pure structural rename. (config/content/resources/signals/migrations/
ministra/storage/tmp/vendor/www/bin stay lowercase.)

- git mv: core→Core, domain→Domain, infrastructure→Infrastructure,
  streaming→Streaming, modules→Modules, cli→Cli, public→Public (module subdirs
  like Modules/plex stay lowercase; loaded by ModuleLoader, not Composer).
- PHP filesystem paths updated across src/ + tests/: require/include, glob/scandir
  bases, view includes, asset/nginx-alias paths, autoload.php registerDirectories(),
  ModuleLoader/ModuleManager module roots, console.php discovery dirs.
- src/composer.json PSR-4 vendored-lib paths → Core/Parsing/...; vendor/ regenerated.
- nginx.conf: docroot + SCRIPT_FILENAME → Public/. SCRIPT_NAME and the public-facing
  /streaming/*.php compat routes are URLs, left unchanged.
- Build/CI: Makefile LB_DIRS / LB_DIRS_TO_REMOVE / LB_FILES_TO_REMOVE PascalCased
  (closes the LB-archive privileged-leak blocker); phpstan.dist.neon paths/
  scanDirectories/excludePaths; phpunit.xml.dist coverage; phpstan-baseline.neon
  regenerated (294→294 errors, no new resolution failures).
- migrations/deleted_files.txt: old lowercase trees listed for client cleanup
  (assumes case-sensitive FS — the supported Linux target).

Verified: grep-gate 0 live lowercase-dir require/include in src+tests; php -l clean;
PHPUnit 292/292; PHPStan no errors; Composer first / XC_Autoloader last in SPL stack;
global classes resolve from the renamed dirs.
2026-06-24 20:10:34 +03:00