* @copyright 2025-2026 Vateron Media * @link https://github.com/Vateron-Media/XC_VM * @license AGPL-3.0 https://www.gnu.org/licenses/agpl-3.0.html */ class ConstantsInitializer { /** * Define the path, app-config and binary constants exactly once. * * Ordering (MAIN_HOME -> paths -> binaries) is enforced here in code rather * than by require order: BIN_PATH is established by the path map before the * binary map derives from it. * * @param string|null $mainHome Deploy root with trailing slash. Falls back to * the MAIN_HOME constant when omitted. */ public static function init(?string $mainHome = null): void { $mainHome ??= defined('MAIN_HOME') ? MAIN_HOME : null; if ($mainHome === null) { throw new RuntimeException( 'ConstantsInitializer::init() requires MAIN_HOME to be defined or passed explicitly.' ); } self::defineAll(self::paths($mainHome)); $appConfig = self::appConfig(); // OPENSSL_EXTRA is per-install key material: prefer the secret the installer // wrote, falling back to the historical literal. Resolved only on the // genuine first define (tests / re-init keep the already-set constant). if (!defined('OPENSSL_EXTRA')) { $appConfig['OPENSSL_EXTRA'] = self::resolveOpensslExtra($mainHome . 'config/openssl_extra', $appConfig['OPENSSL_EXTRA']); } self::defineAll($appConfig); // Derive binaries from the now-defined BIN_PATH (an explicit test override // of BIN_PATH wins over the computed value). $binPath = defined('BIN_PATH') ? BIN_PATH : $mainHome . 'bin/'; self::defineAll(self::binaries($binPath)); } /** * Define the STATUS_* constants exactly once. * * Kept separate from init() to preserve the current lazy semantics: status * codes are only needed by the admin/reseller API handlers. */ public static function initStatus(): void { if (defined('STATUS_FAILURE')) { return; } self::defineAll(self::statuses()); } /** * Filesystem path constants, derived from the deploy root. * * @param string $mainHome Deploy root with trailing slash. * @return array Constant name => value. */ public static function paths(string $mainHome): array { $content = $mainHome . 'content/'; $tmp = $mainHome . 'tmp/'; $bin = $mainHome . 'bin/'; $storage = $mainHome . 'storage/'; $fanoutRun = $bin . 'xc_fanout/sockets/'; $images = $storage . 'images/'; return [ 'CONTENT_PATH' => $content, 'TMP_PATH' => $tmp, 'CONFIG_PATH' => $mainHome . 'config/', 'BIN_PATH' => $bin, 'STORAGE_PATH' => $storage, 'SIGNALS_PATH' => $mainHome . 'signals/', 'FANOUT_RUN_PATH' => $fanoutRun, 'FANOUT_CTL_SOCK' => $fanoutRun . 'control.sock', 'FANOUT_HTTP_SOCK' => $fanoutRun . 'http.sock', 'STREAMS_PATH' => $content . 'streams/', 'EPG_PATH' => $content . 'epg/', 'VOD_PATH' => $content . 'vod/', 'ARCHIVE_PATH' => $content . 'archive/', 'CREATED_PATH' => $content . 'created/', 'DELAY_PATH' => $content . 'delayed/', 'VIDEO_PATH' => $content . 'video/', 'PLAYLIST_PATH' => $content . 'playlists/', 'CONS_TMP_PATH' => $tmp . 'opened_cons/', 'CRONS_TMP_PATH' => $tmp . 'crons/', 'CIDR_TMP_PATH' => $tmp . 'cidr/', 'CACHE_TMP_PATH' => $tmp . 'cache/', 'STREAMS_TMP_PATH' => $tmp . 'cache/streams/', 'SERIES_TMP_PATH' => $tmp . 'cache/series/', 'LINES_TMP_PATH' => $tmp . 'cache/lines/', 'DIVERGENCE_TMP_PATH' => $tmp . 'divergence/', 'FLOOD_TMP_PATH' => $tmp . 'flood/', 'PLAYER_TMP_PATH' => $tmp . 'player/', 'MINISTRA_TMP_PATH' => $tmp . 'ministra/', 'SIGNALS_TMP_PATH' => $tmp . 'signals/', 'LOGS_TMP_PATH' => $tmp . 'logs/', 'WATCH_TMP_PATH' => $tmp . 'watch/', 'IMAGES_PATH' => $images, 'E2_IMAGES_PATH' => $images . 'enigma2/', ]; } /** * Application-level constants: versioning, Git repos and feature flags. * * @return array Constant name => value. */ public static function appConfig(): array { return [ 'DB_ACCESS_ENABLED' => DB_ACCESS_ENABLED, 'DB_ACCESS_PWD' => DB_ACCESS_PWD, 'DEV_MODE' => DEV_MODE, 'XC_VM_VERSION' => XC_VM_VERSION, 'GIT_OWNER' => 'Vateron-Media', 'GIT_REPO_MAIN' => 'XC_VM', 'GIT_REPO_UPDATE' => 'XC_VM_Update', 'GIT_REPO_BIN' => 'XC_VM_Binaries', 'GIT_REPO_FANOUT' => 'XC_VM_Fanout', 'GIT_REPO_PROXY' => 'XC_VM_Proxy', 'MONITOR_CALLS' => 3, 'OPENSSL_EXTRA' => 'fNiu3XD448xTDa27xoY4', ]; } /** * Binary-path constants, derived from the bin directory. * * Only the FFmpeg 4.0 anchor is pinned here; other versions are resolved * dynamically by FfmpegPaths/FfmpegBinaries. * * @param string $binPath Bin directory with trailing slash (BIN_PATH). * @return array Constant name => value. */ public static function binaries(string $binPath): array { return [ 'PHP_BIN' => $binPath . 'php/bin/php', 'YOUTUBE_BIN' => $binPath . 'yt-dlp', 'FFMPEG_FONT' => $binPath . 'free-sans.ttf', 'GEOLITE2_BIN' => $binPath . 'maxmind/GeoLite2-Country.mmdb', 'GEOLITE2C_BIN' => $binPath . 'maxmind/GeoLite2-City.mmdb', 'GEOISP_BIN' => $binPath . 'maxmind/GeoIP2-ISP.mmdb', 'FFMPEG_BIN_40' => $binPath . 'ffmpeg_bin/4.0/ffmpeg', 'FFPROBE_BIN_40' => $binPath . 'ffmpeg_bin/4.0/ffprobe', ]; } /** * STATUS_* result codes used across admin/reseller API handlers. * * @return array Constant name => value. */ public static function statuses(): array { return [ 'STATUS_FAILURE' => 0, 'STATUS_SUCCESS' => 1, 'STATUS_SUCCESS_MULTI' => 2, 'STATUS_CODE_LENGTH' => 3, 'STATUS_NO_SOURCES' => 4, 'STATUS_DISABLED' => 5, 'STATUS_NOT_ADMIN' => 6, 'STATUS_INVALID_EMAIL' => 7, 'STATUS_INVALID_PASSWORD' => 8, 'STATUS_INVALID_IP' => 9, 'STATUS_INVALID_PLAYLIST' => 10, 'STATUS_INVALID_NAME' => 11, 'STATUS_INVALID_CAPTCHA' => 12, 'STATUS_INVALID_CODE' => 13, 'STATUS_INVALID_DATE' => 14, 'STATUS_INVALID_FILE' => 15, 'STATUS_INVALID_GROUP' => 16, 'STATUS_INVALID_DATA' => 17, 'STATUS_INVALID_DIR' => 18, 'STATUS_INVALID_MAC' => 19, 'STATUS_EXISTS_CODE' => 20, 'STATUS_EXISTS_NAME' => 21, 'STATUS_EXISTS_USERNAME' => 22, 'STATUS_EXISTS_MAC' => 23, 'STATUS_EXISTS_SOURCE' => 24, 'STATUS_EXISTS_IP' => 25, 'STATUS_EXISTS_DIR' => 26, 'STATUS_SUCCESS_REPLACE' => 27, 'STATUS_FLUSH' => 28, 'STATUS_TOO_MANY_RESULTS' => 29, 'STATUS_SPACE_ISSUE' => 30, 'STATUS_INVALID_USER' => 31, 'STATUS_CERTBOT' => 32, 'STATUS_CERTBOT_INVALID' => 33, 'STATUS_INVALID_INPUT' => 34, 'STATUS_NOT_RESELLER' => 35, 'STATUS_NO_TRIALS' => 36, 'STATUS_INSUFFICIENT_CREDITS' => 37, 'STATUS_INVALID_PACKAGE' => 38, 'STATUS_INVALID_TYPE' => 39, 'STATUS_INVALID_USERNAME' => 40, 'STATUS_INVALID_SUBRESELLER' => 41, 'STATUS_NO_DESCRIPTION' => 42, 'STATUS_NO_KEY' => 43, 'STATUS_EXISTS_HMAC' => 44, 'STATUS_CERTBOT_RUNNING' => 45, 'STATUS_RESERVED_CODE' => 46, 'STATUS_NO_TITLE' => 47, 'STATUS_NO_SOURCE' => 48, ]; } /** * Resolve the per-install OPENSSL_EXTRA secret from a dedicated file the * installer writes, falling back to the historical literal when absent. * * A standalone file (not config.ini) is used deliberately: it is independent * of the config.ini -> config.enc migration and is never rewritten, so the * value a fresh install generates is read back identically for the life of * the install — OPENSSL_EXTRA must never change, since it feeds key/HMAC * derivation for persisted data (hmac_keys rows, cached image filenames, * proxy URL keys, stream tokens). The literal fallback keeps existing installs * — whose data derives from it — decrypting unchanged. * * @param string $secretFile Absolute path to the per-install secret file. * @param string $fallback Historical literal default. */ private static function resolveOpensslExtra(string $secretFile, string $fallback): string { if (is_file($secretFile)) { $value = trim((string) @file_get_contents($secretFile)); if ($value !== '') { return $value; } } return $fallback; } /** * define() every entry that is not already defined. * * @param array $constants Constant name => value. */ private static function defineAll(array $constants): void { foreach ($constants as $name => $value) { if (!defined($name)) { define($name, $value); } } } }