Commit Graph
10 Commits
Author SHA1 Message Date
obscuremind 5a8b4bf8a5 feat(auth): two-factor sign-in for admins and resellers
A TOTP code from an authenticator app (RFC 6238), or one of ten one-time
recovery codes, after the password. Each user turns it on from the profile
page (a QR code drawn in the browser, never by a QR service); a group can
require it, and a member without it sets it up at the next sign-in.

A sign-in that needs a code is held: the session keys the password wrote
are moved aside until a good code, so no page sees a signed-in session
before it. A code is good once (users_2fa.last_step), recovery codes are
stored as hashes and spent on use, and ten wrong codes in 15 minutes lock
the account's second step; wrong codes count toward the login flood limit.
An admin turns it off for a user who lost the app, or root runs
`console.php tools twofactor <username>`.
2026-10-06 21:41:49 +00:00
obscuremind be0d92778a fix(server): a server's save refuses a new port another program already holds
On the test MAIN, moving the HTTP port to 8081 (held by another service) was
saved and announced to the nodes, while nginx's reload failed to bind it and
kept running on 8080. The save of this machine's own row now checks each port
it does not use yet (ServerService::portsTaken: a connect to 127.0.0.1) and
answers STATUS_PORT_IN_USE (49) with a message on the server form. Other
servers' rows are not checked: their ports are on another machine.
2026-10-01 13:56:03 +00:00
obscuremind 489ebee80b feat(ffmpeg): every node fetches its distribution's builds from XC_VM_FFMPEG
The bundled ffmpeg 8.0 needs glibc 2.35 and does not start on Ubuntu 20.04 or
Debian 11 (plan: per-distro ffmpeg, Stages 2-4). XC_VM_FFMPEG releases one
build per label and distribution, each built in that distribution's container.

- `console.php ffmpeg` (FfmpegBuildsCommand, as xc_vm) fetches 4.0, 7.1 and 8.1 for
  this distribution (`ffmpeg_<label>_<distro>.tar.gz`), checks each against the
  release's hashes.md5, extracts it aside, and swaps it into
  bin/ffmpeg_bin/<label>/ only once ffmpeg and ffprobe start here; the
  installed build stays otherwise. ffmpeg_version.json keeps each label's
  release, so an unchanged one is not fetched again. cron:root_signals runs it
  daily on every node, the first pass at once, and hourly while a launch
  has not finished (a service restart in the same cron pass killed one on
  the test LB).
- XUI's 4.0 is kept where a node has it: only it takes -nofix_dts, which the
  legacy DTS path passes, until the rebuilt 4.0's DTS handling is validated.
  A rebuilt 4.0 (BUILD_INFO beside it) is never passed -nofix_dts
  (FfmpegPaths::fixDts).
- The 8.x label is 8.1: migration 057 moves the ffmpeg_cpu/ffmpeg_gpu settings
  from 8.0, and a version a node lacks takes the newest build of its major it
  has (8.0 until it fetched 8.1).
- GIT_REPO_FFMPEG, on the BIN update channel.
2026-10-01 11:09:48 +00:00
Divarion_D e865611205 feat(updates): add a dev update channel for nightly panel builds
The panel update channel gains 'dev' (BIN and FANOUT stay stable/beta).
On 'dev' the MAIN release client also reads the releases-only XC_VM_Dev
repository, merges both release lists by version_compare(), and fetches
each asset from the repository that published its tag (X.Y.Z-dev.N tags
live in XC_VM_Dev). An unreachable XC_VM_Dev leaves the channel on the
regular releases. Nightly changelogs are read from a release asset,
since XC_VM_Dev has no source tree.

- UpdateChannels::mainReleases() builds the MAIN client; the update
  command, update cron, web API bootstrap, rollback picker and server
  install use it instead of constructing their own.
- The releases API is requested with per_page=100 so prereleases cannot
  push stable releases off the default 30-item page.
- Nightly builds are excluded from the rollback picker (rollback accepts
  X.Y.Z only).
- Settings: 'Dev (nightly)' option for the panel channel plus a warning.
2026-09-25 22:40:51 +03:00
Divarion_D 320edb819c refactor(bootstrap): convert procedural prelude to BC shims
Point the five procedural prelude files at the new source-of-truth classes,
keeping them on disk as thin shims because the boot paths require_once them by
name and legacy code reads their globals/constants directly.

- Paths.php / AppConfig.php / Binaries.php -> ConstantsInitializer::init()
- ErrorCodes.php -> bridges ErrorResponder::codes() into $GLOBALS['rErrorCodes']
- ErrorHandler.php -> generateError()/generate404() shims (function_exists
  guarded) delegating to ErrorResponder; production still exit()s, so the ~139
  call sites are untouched.

Register ErrorHandler.php in composer autoload.files so the functions exist even
on paths that do not require the prelude; regenerate the production-only vendor
autoload accordingly.

The path/app-config constants are no longer literal define()s PHPStan can scan,
so add the four it previously learned from them but that were missing from the
stub (GIT_REPO_FANOUT, FANOUT_{RUN_PATH,CTL_SOCK,HTTP_SOCK}).

Debug/404 output stays byte-identical to the legacy functions (golden tests);
full suite, PHPStan, phpcs and make gates all green.
2026-09-16 16:34:16 +03:00
Divarion_D 72eb0fbc58 refactor(bootstrap): replace per-scope include pairs with ScopeBootstrap classes
The front controller selected a pair of procedural files per scope
(<scope>_session.php + <scope>_functions.php) from a hardcoded map and
require'd them at global scope. Consolidate into a typed class hierarchy:

- ScopeBootstrap (interface) + ScopeBootstrapFactory::create($scope)
- Admin/Reseller/PlayerScopeBootstrap, each porting its former session +
  functions logic 1:1 into boot()/bootSession()/bootFunctions().

index.php now calls ScopeBootstrapFactory::create($scope)->boot() instead
of building file paths and require'ing. Unknown scopes still fall back to
admin.

Behaviour is unchanged: the view-facing globals ($rUserInfo, $rPermissions,
$_STATUS, $customScript, $_PAGE, server/health flags) are still injected via
explicit `global` declarations in the boot methods, so the ~140 procedural
view templates read them from scope exactly as before. player_utility_-
functions.php is unchanged and still loaded by PlayerScopeBootstrap.

Also fix the PHPStan stub: SERVER_ID is an int (getMainID(): ?int, a
server_id PK), not a string. Moving its define() into a class method meant
PHPStan no longer inferred the type from a top-level define and fell back to
the stub, which wrongly typed it `(string) mt_rand()` — surfacing 20
false argument.type errors at int-param call sites. Stub now uses mt_rand().

Removes: admin_session_fc.php, admin_functions_fc.php, reseller_session.php,
reseller_functions.php, player_session.php, player_functions.php.

make phpstan / cs / gates all green.
2026-08-27 15:57:35 +03:00
Divarion_D cb056cdcf5 fix(phpstan): wire constants-stub regen into make, refresh drifted stub
gen-constants-stub.php had no Makefile/CI target, so tools/phpstan/
constants.stub.php (a bootstrapFile in phpstan.dist.neon) silently drifted:
regenerating it added DB_ACCESS_PWD and GIT_REPO_PROXY (real runtime define()s
in src/Core/Config/AppConfig.php that PHPStan could not see). Added `make
phpstan-stub` so the stub is regenerable/discoverable, and refreshed it
(129 -> 131 constants).
2026-08-07 21:03:42 +03:00
Divarion-D 66ecbc8cbc fix(phpstan): fix real bugs (return contracts, arg/byref, stub types) 599→585
- constants stub: use mt_rand()-based exprs so PHPStan infers GENERAL types,
  not literal 0/'' — fixes false division-by-zero (PACKET_SIZE) and
  foreach-over-false (str_split with len 0). Load stub via bootstrapFiles so
  result-cache invalidates on change.
- return contracts: explicit returns where a path fell through to null and
  violated the declared type:
  - StreamRepository::getById/getWatchFolder, GroupService::getById,
    getStream() → return false (declared array|false).
  - ServerRepository::getPublicURL → return '' when server missing (array→string).
  - MagService::resetSTB → return query() result (declared bool).
  - StreamUtils::getPlaylistSegments → explicit return null.
  - NetworkUtils::stopDownload → @return null corrected to @return void.
- PlexController: getPlexToken() called with 5 args but accepts 4 — dropped
  the dead 5th argument.
- DropboxClient::getMetaFromHeaders: array_shift() on an array_filter()
  expression (not a variable, by-ref error) — assign to a var first.
- WatchdogCommand: wrap numeric-string subtractions (nginx/proc-stat values)
  in floatval()/intval().
2026-06-23 20:02:46 +03:00
Divarion-D b9e911e7e8 fix(phpstan): resolve false positives and real bugs (988 → 622)
Systemic fixes (cascade across modules):
- Type `$db` params as \DatabaseHandler in 38 files (was `object`), so
  PHPStan resolves get_row()/query() and stops inferring closed array
  shapes — clears offsetAccess.notFound clusters.
- Database/DatabaseHandler: correct get_row()/clean_row() PHPDoc to
  array<string,mixed>, and query() $buffered param to mixed (it is the
  first positional bind value via func_get_args, not a bool flag).
- constants stub generator: explicit type overrides (HOST/PAGE_NAME →
  string, PHP_ERRORS → bool) to avoid null/mixed false positives.
- phpstan.dist.neon: scanFiles autoload.php/bootstrap.php; ignore
  unactionable FPs (dynamic require paths, proprietary XC_VM ioncube
  class, MaxMind vendor lib, ext-inotify stub gap).

Real bugs fixed:
- streaming/ConnectionLimiter: appended the whole accumulated UUID array
  instead of a single uuid, breaking per-stream temp-file cleanup.
- streaming/TimeshiftClient: dead `|| $x == 3` branch (always redundant).
- streaming/AsyncFileOperations: redundant is_array after === false.
- core/Storage/DropboxClient: @param callback → callable|null.

streaming module is clean (0 errors); infrastructure 39 → 27.
2026-06-23 19:46:33 +03:00
Divarion-D 69669d8e1c feat(phpstan): add PHPStan static analysis setup and constants stub generation 2026-06-23 19:28:45 +03:00