Commit Graph
1649 Commits
Author SHA1 Message Date
rootandClaude Opus 5 0408e7d8d3 fix(security): escape provider catalogue values in the admin streams search
handleProviderStreams built the Provider Streams table (the "search
provider" modal on the stream and movie pages) by pasting values from the
remote provider's API — stream icons, names, container extensions, expiry
and connection counts — into HTML attributes and an onClick handler. Rows
are read with only < and > entity-encoded, so quotes survived: a provider
returning a stream_icon of `x' onerror='…` ran script in the admin's session
when the modal opened, and a backslash undid the \' escaping in addStream().

Each value is now decoded and encoded once for where it lands: an attribute,
a JSON string literal inside the handler, or cell text.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V2uBUbGApb4A7Rbcoi7dxA
2026-09-17 07:40:53 +00:00
rootandClaude Opus 5 fadf0aaeb4 fix(security): stop deriving the player's token key from public values
PlayerScopeBootstrap replaced the random live_streaming_pass, for every
player request, with md5(sha1(server_name . server_ip) . <constant in this
file>). A subscriber knows or can guess all three, and the subtitle proxy
(PlayerProxyController) fetches and returns whatever http URL a token under
that key names. So any signed-in line could mint tokens for internal URLs —
for instance /admin/api on loopback, which needs no password by default and
stops streams or lists users.

The player now keeps the secret setup generated. The only tokens under this
key are the subtitle links the player mints and the proxy reads, both in the
same scope, so they keep working.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V2uBUbGApb4A7Rbcoi7dxA
2026-09-17 07:39:40 +00:00
Divarion_D 13d4d6b077 Prepare release 2.5.2 2026-09-16 22:04:48 +03:00
Divarion_D 720ffe925d fix(player-v2): route unified layout to the player_v2 header/footer
renderUnifiedLayoutHeader/Footer only branched on 'player' and
'reseller'; the 'player_v2' scope fell through to the admin new-UI
shell, so Web Player V2 pages loaded admin Vuexy vendor assets
(node-waves, datatables-bs5, select2, ...) and the admin header_stats
poll — none of which exist under assets/player_v2/ (404s). Add a
player_v2 branch requiring the existing layouts/player_v2 header/footer,
whose Sneat asset set is present under assets/player_v2/.

Refs: #178
2026-09-16 22:00:13 +03:00
Divarion_D 4136283c29 feat(admin): expose Web Player V2 (access-code type 8) in code form
The backend already handles access-code type 8 (save validation,
updateCodes nginx scope/alias mapping to player_v2, getWebPlayerV2Code
helper), but the admin code form and codes table never listed it, so it
could not be selected. Add the type-8 option, its table label/badge, and
extend the code form's live URL preview to type 8.

Refs: #178
2026-09-16 22:00:04 +03:00
Divarion_D f2892400bd fix(player): pass array/int defaults to getUserStreams/getUserSeries
Both helpers declare non-nullable typed params (array $rPicking, int
$rStart/$rLimit) but several player/PlayerV2 home and live call sites
still passed null, throwing a TypeError under PHP 8 the moment those
paths rendered. Pass the intended defaults instead ([] for $rPicking,
0/0 for the "no LIMIT" live-ids fetch, which stays falsy).
2026-09-16 21:59:54 +03:00
Divarion_D 5b04b7a6e0 fix(admin-ui): restore Users nav tabs from bottom "More" catch-all
The sidebar section list in menu.php still referenced the stale top-level
key `users`, which was split long ago into `lines`, `active_codes`, `mag`,
`e2` and `reseller` in CoreNavbarProvider. Unclaimed by any section, the
whole user cluster (plus `category_templates`) fell into the trailing
"More" catch-all at the very bottom of the menu.

Reference the real keys and give the user cluster its own "Users" section
header, keeping content/vod/distribution/category_templates under Catalog.
2026-09-16 19:17:44 +03:00
DanilandGitHub 2b3ae3dc53 Merge pull request #195 from obscuremind/main
tests(e2e)
2026-09-16 19:04:38 +03:00
DanilandGitHub 5cfb7de46d Merge pull request #196 from Vateron-Media/refactor/bootstrap-psr4-kernel
refactor(bootstrap): testable BootKernel pipeline + per-install OPENSSL_EXTRA
2026-09-16 19:03:19 +03:00
Divarion_D 4ea916ab89 fix(bootstrap): address Sourcery review — WebApi profile guard, falsy http code, getBool
Three fixes from the automated PR review:

- ErrorResponder::respondError() used `$httpCode === null` where the legacy
  generateError() used `!$rCode`; a caller passing 0 now falls through to the 404
  page again instead of emitting http_response_code(0). (+ test)
- StageProfiles::for(BootContext::WebApi) now throws instead of silently building
  a wrong stage list from the common prefix/suffix — WebApi boots via
  WebApiBootstrap, never the kernel. (+ test)
- LegacyCoreStage reads enable_cache via SettingsManager::getBool() (the typed
  getter the Web API path used), rather than the raw get(). Behaviour is
  equivalent (`!` already coerces) but the intent is clearer.
2026-09-16 18:57:54 +03:00
Divarion_D a453cd9620 docs: fix stale references to the removed prelude files
The prelude shims (Paths/AppConfig/Binaries/ErrorCodes.php) were deleted and the
$rErrorCodes global is gone. Point the developer guides at the new homes:
constants → ConstantsInitializer (paths()/appConfig()/binaries() maps), error
catalogue → ErrorResponder::codes(). Also refresh the now-outdated "refactored
later" note in build/rector.php's skip list.

Only docs/en is edited (docs/ru is regenerated from it before a release);
make docs-build passes.
2026-09-16 18:52:01 +03:00
Divarion_D 323d4aa455 refactor(config): hoist frequently-edited release constants to top-of-file define()s
XC_VM_VERSION / DEV_MODE / DB_ACCESS_ENABLED / DB_ACCESS_PWD are edited on every
release (and by the release automation). Buried as array entries in appConfig()
they were awkward to find and to sed. Move them back to guarded define()s at the
top of ConstantsInitializer.php; appConfig() reads them back, and init() skips
the already-defined ones. The guard keeps a pre-definition (e.g. the PHPStan
stub) from fataling.

Update the release checklist accordingly: the sed commands target the familiar
`define('XC_VM_VERSION', '...')` form in ConstantsInitializer.php again (they were
pointing at the deleted AppConfig.php).
2026-09-16 18:51:51 +03:00
Divarion_D 7064f41fed test(bootstrap): cover stage skip-paths and side-effect stages
Add cheap unit tests for stages that need no DB or config extension:
SessionStage / FloodProtectionStage / HostVerificationStage self-skip under the
CLI SAPI, ProcessTitleStage and AdminShutdownStage run without error, and
StatusConstantsStage defines the STATUS_* codes. Unit-covered stages: 8 of 16
(the rest need a live MySQL + xcvm_core and are covered by the dev-container and
real-install smokes).
2026-09-16 18:38:31 +03:00
obscuremindandGitHub b6ba43dc8d Merge branch 'Vateron-Media:main' into main 2026-09-16 16:27:56 +01:00
Divarion_D 0ab6a8f6a9 refactor(bootstrap): collapse prelude shims into ConstantsInitializer::init()
The Paths/AppConfig/Binaries shims were byte-identical (each just called
ConstantsInitializer::init()), and ErrorCodes.php only bridged a $rErrorCodes
global that nothing reads any more. Every boot path required all five by name.

Replace those requires with a single ConstantsInitializer::init() at each of the
four call sites (ConstantsStage, WebApiBootstrap, StreamingRequestBootstrap, the
progress endpoint) and delete the four dead shim files. The generateError()/
generate404() functions are already provided globally via composer autoload.files,
so their require was redundant too — ErrorHandler.php stays (it is the
autoload.files target) but is no longer required by name.

Verified: init() defines the full constant set and the error functions autoload;
762 tests, PHPStan, phpcs and make gates green; all four boot smokes (cli/admin/
webapi/streaming) still PASS against a real MariaDB in the dev container.
2026-09-16 18:24:57 +03:00
Divarion_D c2c686b3a2 docs(bootstrap): rewrite the file header to state its purpose
XC_Bootstrap is now a thin facade, so the old header — a full description of every
context plus four usage examples — described the pre-refactor monolith. Replace it
with a short statement of what the file is (entry point: MAIN_HOME + Composer
autoloader + the BC facade) and a pointer to where the logic lives (BootKernel and
its stages; the context is a BootContext).
2026-09-16 18:24:44 +03:00
Divarion_D 2489824f16 feat(security): per-install OPENSSL_EXTRA secret
OPENSSL_EXTRA was a hardcoded literal shared by every install, i.e. public in the
source tree. Give fresh installs their own secret while leaving existing installs
untouched.

- Installer: after writing config.ini, generate a 40-char secret into
  config/openssl_extra (chmod 600), but ONLY when the file is absent. It is key
  material for Encryption (hmac_keys rows, cached image filenames, proxy URL keys,
  stream tokens), so it must be generated once and never rotated — re-running the
  installer must not overwrite it or all existing encrypted data would orphan.
- ConstantsInitializer now sources OPENSSL_EXTRA from that file directly rather
  than through the config extension. A standalone file is deliberate: it is
  independent of the config.ini -> config.enc migration and never rewritten, so
  the value is read back identically for the life of the install regardless of
  how the proprietary extension surfaces config keys. Missing/empty file falls
  back to the historical literal, so existing installs keep decrypting unchanged.

Verified: no file -> literal, file -> its value, empty file -> literal; installer
generation is idempotent (40 chars, 0600, not overwritten on re-run).
2026-09-16 17:43:53 +03:00
rootandClaude Opus 5 d0458a8eeb test(e2e): drive the admin panel the way an administrator does
The Playwright suite only checked that pages render. It now performs the
administrator's work against a live test panel and asserts the panel's own
data after each step:

- catalogue: a stream category, a bouquet and a reseller package — created,
  renamed / edited, reopened, deleted;
- subscribers: a line with a bouquet (search, edit in the modal, disable /
  enable, ban / unban, delete), a MAG and an Enigma2 device;
- bulk: two lines selected with the header checkbox, disabled and deleted;
- resellers: created with credits, topped up, edited, disabled, deleted;
- block lists: an IP (RFC 5737 address — blocking adds an iptables rule), a
  user agent and an ISP;
- streams: a live stream added with a source and a server, started, running
  with codecs and the Resources column filled in, stopped, renamed, deleted;
- sign-in: a second administrator refused with a wrong password, signing in
  and out — a separate account, because every admin login re-hashes the
  password and ends that account's other sessions.

Records are named `e2e-<run>-…`; a teardown project sweeps whatever a run
leaves behind and nothing else. tools/create-admin.php provisions the
dedicated test administrator on the panel host.

The first runs found three save paths that answered an empty page (fixed in
f7bba5cb, fd13c940, ee2f586a). Against the test panel: 82 passed, 1 skipped
(no series to select).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WDhDajBPziJwjWZcXnh6R
2026-09-16 14:39:39 +00:00
rootandClaude Opus 5 ee2f586a44 fix(streams): adding a stream without an icon failed
StreamService::process() runs the stream icon through
ImageUtils::downloadImage() when "download images" is on, and an empty icon
arrives there as null. The `string` type the cs-fix pass (758a9cab) put on
the parameter made that a TypeError, so saving a new stream with no icon
answered an empty page and created nothing. Series, movies, episodes, radios
and created channels hand it optional covers and backdrops the same way.

downloadImage() now takes null and hands back whatever it cannot download
unchanged, null included. Found by the new E2E stream test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WDhDajBPziJwjWZcXnh6R
2026-09-16 14:38:17 +00:00
Divarion_D e3068e0839 refactor(bootstrap): drop redundant require_once on the streaming path
AsyncFileOperations, DatabaseHandler and Logger are PSR-4 classes resolved by the
Composer autoloader, so the explicit require_once of each (in StreamingBootstrap
and StreamingRequestBootstrap) is dead weight on the hottest path. Remove them;
behaviour is unchanged.

StreamingRequestBootstrap otherwise keeps its own fail-closed guard flow (settings
from the file cache, PHP_ERRORS from cache, the 'exit' gate) inline: unlike the
web-API path it has no DB step in common with the kernel — the connection is made
inside LegacyInitializer::initStreaming via DatabaseFactory::connect — so there is
nothing to fold into the shared stages.

Verified in a php+MariaDB dev container: the three classes autoload without the
requires and StreamingRequestBootstrap::init('status') boots and reaches the DB.
2026-09-16 17:35:07 +03:00
Divarion_D 6ab7e63ca1 refactor(bootstrap): route WebApiBootstrap DB init through the shared stages
WebApiBootstrap now reuses DatabaseStage + LegacyCoreStage for the "connect, wire
the domain services, run initCore, reconnect if the settings cache is incomplete"
step, so that logic has a single source of truth shared with the main kernel.

The web-API-specific parts stay inline because they are order-sensitive and not
container-based: the prelude split around the ini_set defaults, RequestGuard
(flood/host/PHP_ERRORS/Logger from the file cache), and the $gitRelease global.
The redundant explicit require_once of LegacyInitializer/DatabaseHandler/
GitHubReleases is dropped — those autoload. The endpoint cache list is now a
class constant.

Adds BootContext::WebApi (with a BootKernel::defaults arm for match
exhaustiveness) as the state's context; WebApi builds its own two-stage pipeline
rather than going through BootKernel, so no container context/options are set —
preserving the previous behaviour exactly.

Verified in a php+MariaDB dev container: WebApiBootstrap::init('api') populates
$db/$gitRelease/$rSettings, defines PHP_ERRORS/SERVER_ID, and the booted handle
queries the DB.
2026-09-16 17:23:33 +03:00
rootandClaude Opus 5 fd13c94032 fix(devices): deleting an unpaired MAG or Enigma2 device failed
MagService::getById() and EnigmaService::getById() look up the paired line
with UserRepository::getLineById($rRow['user']['pair_id']), and pair_id is
NULL for a device without a pair. The `int` type the cs-fix pass (758a9cab)
put on getLineById() made that a TypeError, so loading such a device —
deleting it, among others — answered an empty page and changed nothing.
Found by the new E2E device test.

getLineById() is also fed activation codes' nullable subscriber_id and raw
request values, so the guard lives there: anything that is not a positive id
finds nothing, as the untyped version did.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WDhDajBPziJwjWZcXnh6R
2026-09-16 14:18:09 +00:00
rootandClaude Opus 5 f7bba5cbcd fix(admin): saving an edit from the new UI answered an empty page
post.php hands its optional `referer` parameter to
AdminHelpers::getPageFromURL() on every edit (streams, movies, created
channels, episodes, lines, MAG, Enigma2, radios, series, resellers). The
new-UI forms post without one, and the `string` type the cs-fix pass
(758a9cab) put on the parameter turned that null into a TypeError: the save
answered an empty 200 and the form showed its error toast. Found by the new
E2E line-edit test.

The function already treated an empty URL as "no page"; it now takes null the
same way, and a URL without a path no longer reads an undefined `path` key.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WDhDajBPziJwjWZcXnh6R
2026-09-16 14:18:09 +00:00
DanilandGitHub 3512f0e816 Merge pull request #194 from obscuremind/fix/streams-resources-column
fix(admin): restore the streams page Resources column data
2026-09-16 16:59:08 +03:00
Divarion_D e1bb80672e refactor(bootstrap): split XC_Bootstrap into an injectable BootKernel pipeline
Replace the fully-static XC_Bootstrap god-class with a stage pipeline so the boot
logic becomes unit-testable and the per-context sequences are explicit.

- BootState replaces the 8 static readiness flags with a value object threaded
  through the pipeline; stages read/write it instead of static state.
- BootStageInterface + BootPipeline run an ordered stage list and abort loudly
  on a throwing stage.
- 16 stages under Core/Bootstrap/Stage/ hold one subsystem each, extracted
  verbatim from the old private methods (constants, config, flood, host, session,
  database, legacy core, redis, process title, admin API, translator, admin
  shutdown, status constants, admin globals, container populate, health check).
- StageProfiles builds the ordered list per context, mirroring the exact previous
  sequence; BootKernel resolves options, sets up the container and runs it.
- XC_Bootstrap is now a thin BC facade delegating to BootKernel; its getters read
  the returned BootState. reset() also clears EventDispatcher and the new
  DatabaseFactory::reset() (a side-effect-free registry clear for test isolation).

The DB-touching contexts (Cli/Stream/Admin) still require a live MySQL and the
xcvm_core extension, so they are verified on a canary rather than in CI; the
Minimal context and the pipeline/profile composition are covered by new tests.
2026-09-16 16:58:49 +03:00
rootandClaude Opus 5 92f8d7eaf1 fix(admin): restore the streams page Resources column data
The Smart Activation Codes commit (b8325fc1) rewrote TableController from a
base older than bb949d18 and dropped the `usage` key from each stream row
along with its $rOrder slot. The view still renders the column and
cron:streams still samples cpu/mem/producer into progress_info, so every row
showed a dash.

Restored, and limited to running streams: stopping a stream does not clear
progress_info, so a stopped or idle on-demand stream would keep showing the
last reading of a producer that no longer exists.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WDhDajBPziJwjWZcXnh6R
2026-09-16 13:38:24 +00:00
Divarion_D 45b82938a0 refactor(bootstrap): source STATUS_* from ConstantsInitializer::initStatus()
Replace the ~49 inline define() calls in XC_Bootstrap::defineStatusConstants()
with a single delegation to ConstantsInitializer::initStatus(). New code and
tests can now read the values via StatusRegistry without the one-shot define()
blocking per-test variation; the legacy STATUS_* reads are unchanged.
2026-09-16 16:34:24 +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 6a39902fbf refactor(bootstrap): add ConstantsInitializer + ErrorResponder PSR-4 classes
Introduce the source-of-truth classes for the bootstrap testability refactor.
Purely additive — nothing is wired to them yet.

- ConstantsInitializer: pure value maps (paths/appConfig/binaries/statuses)
  plus the single define() site (init/initStatus). The maps evaluate with
  different MAIN_HOME/BIN_PATH in one process, which the one-shot define()
  constants they feed cannot — this is what makes them testable.
- ErrorResponder: the generateError()/generate404() logic extracted into pure
  codes()/renderDebug()/render404()/respond*() plus a single side-effecting
  emit(). A test-mode toggle throws ErrorResponseException instead of exit().
- ErrorResponseException: value carrier for a resolved error response.

OPENSSL_EXTRA is now sourced per-install via ConfigReader with a mandatory
fallback to the historical literal, so existing installs (whose persisted data
derives from it) keep decrypting; generation-at-install is left to the installer.

Verified byte-for-byte against the legacy prelude before wiring: 53/53 constants,
debug/404 HTML frozen as sha256 goldens, 65 error codes.
2026-09-16 16:34:02 +03:00
DanilandGitHub abfc723743 Merge pull request #190 from Vateron-Media/dependabot/composer/src/composer-eb2ce679fb
deps: bump phpstan/phpstan from 2.2.12 to 2.2.13 in /src in the composer group across 1 directory
2026-09-16 15:42:02 +03:00
DanilandGitHub a4ff831e28 Merge pull request #193 from Vateron-Media/integrate/pr189-staged
feat: Web Player V2, Active Codes & Category Templates (hardened replay of #189)
2026-09-16 15:37:38 +03:00
Divarion_DandAbdoAhmedElbanaa 4ca21e3041 fix(ci): resolve phpcs + phpstan failures; parameterize favorites IN queries
- ResellerApiDispatcher: import the correct XcVm\Core\Config\DomainResolver; the
  bare reference resolved to a non-existent XcVm\Infrastructure\DomainResolver
  and would fatal when building the reseller active-code portal URL.
- CategoryTemplateService: fix cloneTemplate @param docblock (user/isAdmin, not
  the removed newOwnerId); replace array_filter(..., 'strlen') with a bool
  callback; drop a no-op array_values on an already-list.
- ExternalXtreamService / ImageResizeService: drop no-op array_values; drop a
  redundant is_array() check.
- PlayerCategoryHelper: cuddle elseif (Slevomat control-structure rule).
- BasePlayerV2Controller: permit extract() via the repo's phpcs:ignore idiom and
  add EXTR_SKIP.
- player_utility_functions: drop a no-op ?? on a non-nullable parameter.
- FavoritesController: bind favorite id lists as placeholders (already
  int-sanitized and ACL-intersected; clears the SAST finding, best practice).

make cs / make phpstan / gates / PHPUnit (726) all green.

Co-Authored-By: AbdoAhmedElbanaa <115952879+AbdoAhmedElbanaa@users.noreply.github.com>
2026-09-16 15:13:34 +03:00
Divarion_DandAbdoAhmedElbanaa 053420808e feat(player-v2): frontend assets (SPA scripts, styles, vendored libs)
Web Player V2 client assets: per-page SPA scripts (home/live/movies/series/
radio/search/favorites/profile/login, cinema player, spa/sync/theme), styles,
images, and vendored libraries (video.js, hls.js, jquery, bootstrap, pickr,
iconify). No symlinks, no source maps, no secrets.

Co-Authored-By: AbdoAhmedElbanaa <115952879+AbdoAhmedElbanaa@users.noreply.github.com>
2026-09-15 20:52:15 +03:00
Divarion_DandAbdoAhmedElbanaa 3f6a5f6dbf feat(reseller/admin): active-codes & category-template integration in line/mag flows
Reseller/admin line and MAG controllers and views, the reseller REST API
controller, and admin assets updated for the Active Codes and Category
Templates features. No SQL injection or XSS introduced; id lists are
int-sanitized and no server value is emitted unescaped in the views.

Co-Authored-By: AbdoAhmedElbanaa <115952879+AbdoAhmedElbanaa@users.noreply.github.com>
2026-09-15 20:47:46 +03:00
Divarion_DandAbdoAhmedElbanaa 75c92b194c refactor(core/auth): player_v2 access code; deny-empty outputs; sub-user cycle guard
- AuthRepository/AuthService: recognise access-code type 8 (player_v2),
  mirroring the existing type 6/7 (player/portal) handling.
- PageAuthorization: allow a full admin (trusted is_admin flag) to reach the
  category-templates page without the granular adv/categories permission.
- UserRepository: restore deny-by-default in resolveOutputFormats (an empty
  allowed_outputs list yields no formats, not all); keep the new getSubUsers
  cycle detection against a corrupted owner_id chain.
- ResellerAPI, BaseApiController: integration updates.

Co-Authored-By: AbdoAhmedElbanaa <115952879+AbdoAhmedElbanaa@users.noreply.github.com>
2026-09-15 20:47:46 +03:00
Divarion_DandAbdoAhmedElbanaa 71602c7cc5 i18n: restore ac_* strings and add the used feature keys
The integration had dropped all 208 ac_* (Active Codes) keys from every
language file while adding new feature keys, so the Active Codes UI rendered
raw key names. Restore the language files to their committed state (ac_*
intact) and append only the 57 new keys that are actually referenced in the
panel (59 unused keys from the re-send are skipped). Additive-only; the files
parse under the app's INI_SCANNER_RAW.

Co-Authored-By: AbdoAhmedElbanaa <115952879+AbdoAhmedElbanaa@users.noreply.github.com>
2026-09-15 20:47:45 +03:00
Divarion_DandAbdoAhmedElbanaa a3757c4fc5 fix(player): restore resize auth gate; make listings channel ACL fail-closed
- PlayerResizeController: restore the `if (!rUserInfo['id']) exit();` guard that
  the integration dropped, so the image-fetch endpoint again requires an
  authenticated player session.
- ListingsController: remove the `$rFlip === []` branch that made the channel
  check fail OPEN when the viewer's channel ACL was empty (after the bouquet
  fallback), letting them read EPG for channels they are not entitled to. An
  empty ACL now matches nothing (fail-closed).

Co-Authored-By: AbdoAhmedElbanaa <115952879+AbdoAhmedElbanaa@users.noreply.github.com>
2026-09-15 20:38:18 +03:00
Divarion_DandAbdoAhmedElbanaa 3de38160e2 refactor(player): legacy player controller updates from the integration
Working-tree changes to the legacy Player controllers, deferred out of the
player-v2 commit so player_v2 first shipped against their prior (main)
versions:
- ListingsController: also accept channel/stream-id lists from GET/REQUEST,
  each int-sanitized (array_map('intval', explode(...))) before use.
- PlayerMovieController: similar/recommended-movie queries; every id list is
  interpolated only after implode(',', array_map('intval', ...)).
- HomeController, LiveController, PlayerResizeController, PortalController:
  assorted updates aligned with the integration (PortalController returns
  JSON with scheme-aware URL building).

No new external-class dependencies; all id lists are int-sanitized.

Co-Authored-By: AbdoAhmedElbanaa <115952879+AbdoAhmedElbanaa@users.noreply.github.com>
2026-09-15 20:29:48 +03:00
Divarion_DandAbdoAhmedElbanaa c268d4378c fix(player-v2): escape catalog poster URLs and category emoji in views
- index.php: the hero backdrop and the first movie/series shelf used raw
  cover/backdrop URLs from the catalog in src=/background contexts; run them
  through ImageUtils::validateURL() like the other shelves so a crafted cover
  URL cannot break out of the attribute.
- live/radio/movies/series.php: htmlspecialchars() the category emoji on
  output (it is regex-extracted to emoji ranges today, but escaping keeps it
  safe if that ever changes).

Co-Authored-By: AbdoAhmedElbanaa <115952879+AbdoAhmedElbanaa@users.noreply.github.com>
2026-09-15 20:28:39 +03:00
Divarion_DandAbdoAhmedElbanaa 1a33cd68fe fix(reseller-api): restore auth-guard parentheses (operator precedence)
Five guards had lost the parentheses around the assignment, so e.g.
`if (!$rUser = self::getUser($rID) || !isset($rUser['data']))` parsed as
`!($rUser = (self::getUser($rID) || !isset(...)))`: the || short-circuited
before the check and getUser() always returns a truthy array, so
disableUser/enableUser/editUser executed for ANY id and getLine/getUser
skipped Authorization::check entirely — a reseller could disable/enable/edit
any user and read any line by id. Restore `!($var = ...)` in getLine,
getUser, editUser, disableUser and enableUser.

This commit also carries the reseller-facing Active Codes API surface added by
the integration (delegates to the committed ActiveCodeService).

Co-Authored-By: AbdoAhmedElbanaa <115952879+AbdoAhmedElbanaa@users.noreply.github.com>
2026-09-15 20:23:11 +03:00
Divarion_DandAbdoAhmedElbanaa c86278b0d6 feat(player-v2): web player app + scope wiring; auth/IDOR/XSS hardening
Add the Web Player V2 application and wire it into the front controller: 16
PlayerV2 controllers, views + layouts, routes/player_v2.php, the player_v2
scope in index.php, the scope bootstraps (ScopeBootstrapFactory,
PlayerScopeBootstrap, StreamingRequestBootstrap, player_utility_functions),
nginx config, and the ExternalXtreamService client (already SSRF / DNS-
rebinding / TLS hardened).

Security hardening applied on top of the re-send:
- 'resize' removed from noBootstrapPages so the image endpoint stays behind an
  authenticated player session (was reachable unauthenticated).
- FavoritesController intersects the requested ids with the line's allowed
  live/vod/series/radio ids (was IDOR metadata disclosure).
- Escape _TITLE in the <title> tag; emit server-rendered values inside
  <script> via json_encode with JSON_HEX flags instead of addslashes/raw
  (player.php and the series/movies/live/radio config blocks) to close
  JS-context XSS.

Working-tree changes to the legacy Player/* controllers are not included here;
player_v2 routes to their current (main) versions meanwhile.

Co-Authored-By: AbdoAhmedElbanaa <115952879+AbdoAhmedElbanaa@users.noreply.github.com>
2026-09-15 20:20:36 +03:00
Divarion_DandAbdoAhmedElbanaa 59496efa59 fix(image-resize): pin vetted IP (DNS-rebinding) + TLS-off only for trusted servers
Switch the outbound image fetch from file_get_contents to cURL:
- Raw user URLs: resolvePublicIps() validates the host and the vetted IPs are
  pinned via CURLOPT_RESOLVE, so libcurl cannot re-resolve to an internal
  address between the SSRF check and the fetch (DNS rebinding / TOCTOU); TLS is
  verified and redirects refused.
- Trusted server-list URLs (s:<id>:...): keep TLS verification off, since
  internal LB nodes commonly present self-signed certs (the previous blanket
  verify_peer=true could have broken image loading from them).

Co-Authored-By: AbdoAhmedElbanaa <115952879+AbdoAhmedElbanaa@users.noreply.github.com>
2026-09-15 20:07:38 +03:00
Divarion_DandAbdoAhmedElbanaa 7f648a9b21 fix(player-api): drop credentialed reflected-Origin CORS
The player API echoed the request Origin back with Access-Control-Allow-
Credentials: true, letting any website make credentialed cross-origin calls
and read authenticated responses. The API authenticates by credentials/token
in the request (not cookies), so credentials mode is unnecessary: emit a
plain wildcard Access-Control-Allow-Origin and drop the credentials header at
both response sites.

Co-Authored-By: AbdoAhmedElbanaa <115952879+AbdoAhmedElbanaa@users.noreply.github.com>
2026-09-15 20:00:51 +03:00
Divarion_DandAbdoAhmedElbanaa 01c9e96af8 fix(image-resize): SSRF guard + TLS verify on outbound image fetch
The resize endpoint fetches an attacker-influenceable ?url= server-side and
can echo the bytes back (WebP passthrough). Harden it:
- Reject URLs whose host resolves to a loopback/private/reserved/CGNAT range
  (or does not resolve), and restrict to http(s); admin-configured server-list
  URLs (s:<id>:...) stay exempt as trusted.
- Re-enable TLS verification and stop following redirects (a 30x could point
  back at an internal address past the pre-flight check).

Co-Authored-By: AbdoAhmedElbanaa <115952879+AbdoAhmedElbanaa@users.noreply.github.com>
2026-09-15 19:59:44 +03:00
Divarion_DandAbdoAhmedElbanaa 4029277781 fix(player-api): mint stream/subtitle tokens via mintToken; activation-code login
- Stream-link and subtitle-proxy tokens are now minted with
  Encryption::mintToken(..., !empty(secure_stream_tokens)) instead of the
  legacy encrypt(), matching the readToken() consumers in stream/*.php and
  PlayerProxyController and the reference mints in PlaylistGenerator and
  player/movie.php. With secure_stream_tokens on, encrypt()-minted tokens
  were unreadable, breaking playback/subtitles.
- PlayerApiController: support activation-code login (delegates to the
  committed ActiveCodeService::activateCode), parse a JSON POST body, and
  return structured auth errors.

Co-Authored-By: AbdoAhmedElbanaa <115952879+AbdoAhmedElbanaa@users.noreply.github.com>
2026-09-15 19:52:48 +03:00
Divarion_DandAbdoAhmedElbanaa efabd7e822 fix(active-codes): stop disclosing the bound MAC of a locked code
An attacker who knows a code could read its bound MAC and spoof it to defeat
the hardware lock. Drop the MAC from the DEVICE_MISMATCH message, and remove
locked_mac from checkCode()'s response (checkCode is reachable unauthenticated
via ActiveCodeApiController). locked_mac had no consumers; the is_device_locked
boolean is retained.

Co-Authored-By: AbdoAhmedElbanaa <115952879+AbdoAhmedElbanaa@users.noreply.github.com>
2026-09-15 19:44:15 +03:00
Divarion_DandAbdoAhmedElbanaa 780badaf06 feat(category-templates): admin/reseller UI + custom_data application
Add the Category Templates management UI on top of the already-committed
CategoryTemplateService:
- Admin CategoryTemplate(s)Controller + Ajax controller (create/save/delete/
  clone/toggle-system/apply-all/get); reseller CategoryTemplate(s)Controller
  and views.
- Apply a line's custom_data template layout when building output:
  CategoryService (per-category sync), LineService, MagService,
  PlaylistGenerator, and CacheEngineCronJob (custom_data in the line SELECT).
- Register admin + reseller routes and navbar entries; these shared files
  also carry the Active Codes routes/navbar wiring.

Admin Category Templates views are not part of the re-sent changeset, so the
admin CT pages render empty until those views are added; reseller CT is
complete.

Co-Authored-By: AbdoAhmedElbanaa <115952879+AbdoAhmedElbanaa@users.noreply.github.com>
2026-09-15 19:44:01 +03:00
Divarion_DandAbdoAhmedElbanaa 0846413e8e fix(category-templates): gate applyToAll by template visibility
applyToAll built and applied a template's layout to lines without checking
the caller could see the template, so any reseller could apply another
owner's private template by id (IDOR). Add the same canAccessTemplate() gate
used by save/delete/clone/get. Non-admin callers already target only their
own lines (targetResellerId is honoured for admins only), so no additional
target-scope check is required.

Co-Authored-By: AbdoAhmedElbanaa <115952879+AbdoAhmedElbanaa@users.noreply.github.com>
2026-09-15 18:50:31 +03:00
Divarion_DandAbdoAhmedElbanaa fabce4bfb2 feat(active-codes): full code lifecycle + category-template custom_data
Adopt the author's re-sent, expanded Active Codes and wire it up:

- ActiveCodeService gains listCodes/getCodeDetails/updateCode/deleteCode/
  resetDevice/checkCode/exportBatchJson; codes can be generated with a custom
  streaming username/password and a category_template_id whose layout is
  materialised into lines.custom_data via CategoryTemplateService.
- Bundle CategoryTemplateService (its buildCustomData/getTemplatesForUser are
  the dependency the new service and reseller controller call) plus the
  category_templates schema (021) and lines.custom_data column (024).
- Wire the admin/reseller API surface: ActiveCodeApiController, AdminApi
  controller/wrapper, ResellerApiDispatcher, ResellerActiveCodeController.

Security fixes applied on top of the re-send:
- activateCode: a device-locked code no longer hands out credentials to a
  request that omits the MAC/device id, and the first-activation claim is
  atomic again (WHERE ... AND status/activated_at guard restored).
- CategoryTemplateService: getSubscriberCount/syncTemplateToLines match the
  bound template by JSON_EXTRACT only -- the substring LIKE matched id 1 to
  10/11/100..., overwriting unrelated subscribers' custom_data; cloneTemplate
  and the Ajax get action now enforce template visibility (was IDOR).

ac_* translations are left at the branch's existing version and handled
separately.

Co-Authored-By: AbdoAhmedElbanaa <115952879+AbdoAhmedElbanaa@users.noreply.github.com>
2026-09-15 18:47:50 +03:00
Divarion_DandAbdoAhmedElbanaa aa6e328877 feat(vod): dispatch MediaAnalyzedEvent after successful analysis
VodCronJob emits a typed MediaAnalyzedEvent(streamId, type) on the VALID
branch once a movie/episode finishes analysis, so modules (e.g. Telegram
notifications) can react via #[ListensTo] without any core-to-module
coupling.

Co-Authored-By: AbdoAhmedElbanaa <115952879+AbdoAhmedElbanaa@users.noreply.github.com>
2026-09-15 18:28:54 +03:00