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