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>`.
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.
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.
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.
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.
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.
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).
- 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().