feat(reseller): trial deep-links, voucher balance card, Host header guard

Safe subset of Rosmi720/XC_VM@8fa1e67f. The dashboard revamp and the sidebar
restructure from that commit are deliberately left out — see below.

- The sidebar's existing `?trial=1` links on Lines / MAG / Enigma now actually
  land on the Trial filter: each page picks the trial value from its own
  $rStatusFilters map (5 for lines, 4 for MAG and Enigma) when no explicit
  ?filter is given.
- Restyle the voucher credit-accounting card in active_code.php onto standard
  Vuexy surface classes instead of a hand-tinted primary panel. Element ids and
  the calculation are untouched, so the existing calculator JS still drives it.
- RequestGuard: fall back to '' for HTTP_HOST. A request can legitimately arrive
  without a Host header, and explode(':', null) is deprecated on PHP 8.
- Add the live_calculator string the new card asks for. Without it Translator
  returns the key itself AND appends it to the ini, so the card would render the
  literal "live_calculator" (which is how the placeholder appeared upstream).
  The Arabic wording is a first pass and welcomes a correction from its author.

Left out of this commit:
- ResellerDashboardController / dashboard.php: three fallbacks drop tenant
  scoping when the scoped query comes back empty — top countries re-queries
  lines_activity server-wide, latest movies and live streams re-run without the
  stream_ids restriction, and series and episodes are never scoped at all. It
  also pins unknown connections to a hardcoded 'EG' country, and moves ~10 raw
  SQL queries into a controller.
- The sidebar restructure: it replaces the LineService::canGenerateTrials()
  check with a plain create_line permission, which would show trial actions to
  resellers that may not generate trials.
- The Redis mGet guard for ResellerTableRenderer::handleLiveConnections, which
  Admin\TableController already carries. The method sits at cc=53 with no
  coverage, so the CRAP ratchet rightly refuses the hardening until it is
  tested — worth doing as its own change.

Verified: 857 tests, make gates, CRAP gate.
This commit is contained in:
AbdoAhmedElbanaa
2026-09-22 17:00:30 +03:00
committed by Divarion_D
parent 5c6f9f920c
commit 78d7b23212
7 changed files with 62 additions and 29 deletions
+2 -2
View File
@@ -31,9 +31,9 @@ use XcVm\Core\Logging\Logger;
$rShowErrors = false;
if (!isset($_SERVER['argc'])) {
$rIP = $_SERVER['REMOTE_ADDR'];
$rIP = $_SERVER['REMOTE_ADDR'] ?? '';
if (empty($rIP) || !file_exists(FLOOD_TMP_PATH . 'block_' . $rIP)) {
define('HOST', trim(explode(':', $_SERVER['HTTP_HOST'])[0]));
define('HOST', trim(explode(':', $_SERVER['HTTP_HOST'] ?? '')[0]));
if (file_exists(CACHE_TMP_PATH . 'settings')) {
$rData = file_get_contents(CACHE_TMP_PATH . 'settings');
+1
View File
@@ -2324,5 +2324,6 @@ filter_by_owner = "تصفية حسب المالك"
set_as_system_template = "تعيين كقالب للنظام"
sub_resellers = "الموزعين الفرعيين"
dashboard_nav_top = "يرجى تحديد خيار من شريط التنقل أعلاه للمتابعة."
live_calculator = "حساب لحظي"
close_ticket = "هل أنت متأكد من رغبتك في إغلاق هذه التذكرة؟"
reopen_ticket = "هل أنت متأكد من رغبتك في إعادة فتح هذه التذكرة؟"
+1
View File
@@ -2353,5 +2353,6 @@ copied_success = "Copied!"
custom_format_link = "Custom link"
download = "Download"
select_an_option = "Please select an option first"
live_calculator = "Real-Time Calculation"
close_ticket = "Are you sure you want to close this ticket?"
reopen_ticket = "Are you sure you want to reopen this ticket?"
+55 -24
View File
@@ -337,41 +337,72 @@ $dnsList = array_filter(array_map('trim', explode(',', (string)($rUserInfo['rese
<!-- Right Column: Live Credit Calculator & Guidelines -->
<div class="col-12 col-xl-4">
<!-- Live Credit Calculator Card -->
<div class="card shadow-sm border-0 mb-4 bg-primary text-white">
<div class="card shadow-sm border-0 mb-4">
<div class="card-header d-flex justify-content-between align-items-center pb-3 border-bottom">
<div class="d-flex align-items-center gap-2">
<div class="avatar avatar-sm bg-label-primary rounded p-2">
<i class="ti tabler-wallet icon-20px text-primary"></i>
</div>
<div>
<h6 class="card-title mb-0 fw-bold"><?= $language::get('ac_credit_accounting') ?></h6>
<small class="text-muted"><?= $language::get('live_calculator') ?: 'Real-Time Calculation'; ?></small>
</div>
</div>
<span class="badge bg-label-primary rounded-pill px-3 py-1">
<i class="ti tabler-sparkles me-1"></i><?= $language::get('live') ?: 'Live'; ?>
</span>
</div>
<div class="card-body p-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<span class="text-white-50 text-uppercase fw-semibold small"><?= $language::get('ac_credit_accounting') ?></span>
<i class="ti tabler-wallet fs-3 text-white"></i>
<!-- Current Balance Display -->
<div class="d-flex align-items-center justify-content-between p-3 rounded-3 bg-label-primary mb-4">
<div>
<small class="text-muted text-uppercase fw-semibold d-block mb-1"><?= $language::get('ac_your_current_balance') ?></small>
<h3 class="text-primary fw-bolder mb-0" id="card-current-balance"><?= number_format($userCredits, 2); ?> <span class="fs-6 fw-normal text-muted"><?= $language::get('ac_credits') ?></span></h3>
</div>
<div class="avatar avatar-md bg-primary text-white rounded-circle shadow-sm d-flex align-items-center justify-content-center">
<i class="ti tabler-coins fs-3"></i>
</div>
</div>
<div class="mb-4">
<small class="text-white-50 d-block"><?= $language::get('ac_your_current_balance') ?></small>
<h2 class="text-white fw-bold mb-0" id="card-current-balance"><?= number_format($userCredits, 2); ?> <span class="fs-6 fw-normal"><?= $language::get('ac_credits') ?></span></h2>
</div>
<div class="p-3 bg-white bg-opacity-10 rounded-3 mb-3">
<div class="d-flex justify-content-between mb-2">
<span class="small text-white-50"><?= $language::get('ac_cost_per_voucher') ?>:</span>
<span class="fw-semibold" id="card-cost-per-code">0.00 <?= $language::get('ac_credits') ?></span>
<!-- Calculation Breakdown -->
<div class="card bg-label-secondary border-0 rounded-3 p-3 mb-3">
<div class="d-flex justify-content-between align-items-center mb-2 pb-2 border-bottom">
<span class="text-muted small d-flex align-items-center gap-2">
<i class="ti tabler-ticket fs-5 text-secondary"></i>
<?= $language::get('ac_cost_per_voucher') ?>:
</span>
<span class="fw-semibold text-heading" id="card-cost-per-code">0.00 <?= $language::get('ac_credits') ?></span>
</div>
<div class="d-flex justify-content-between mb-2">
<span class="small text-white-50"><?= $language::get('ac_quantity') ?>:</span>
<span class="fw-semibold" id="card-qty">1</span>
<div class="d-flex justify-content-between align-items-center mb-2 pb-2 border-bottom">
<span class="text-muted small d-flex align-items-center gap-2">
<i class="ti tabler-calculator fs-5 text-secondary"></i>
<?= $language::get('ac_quantity') ?>:
</span>
<span class="badge bg-label-primary fw-bold fs-7 px-2.5 py-1" id="card-qty">1</span>
</div>
<hr class="border-white opacity-25 my-2">
<div class="d-flex justify-content-between align-items-center">
<span class="fw-bold"><?= $language::get('ac_total_cost') ?>:</span>
<div class="d-flex justify-content-between align-items-center pt-1">
<span class="fw-bold text-heading d-flex align-items-center gap-2">
<i class="ti tabler-receipt-2 fs-5 text-warning"></i>
<?= $language::get('ac_total_cost') ?>:
</span>
<span class="fs-5 fw-bold text-warning" id="card-total-cost">0.00 <?= $language::get('ac_credits') ?></span>
</div>
</div>
<div class="d-flex justify-content-between align-items-center p-2 rounded-3 bg-black bg-opacity-25" id="balance-after-box">
<span class="small text-white-50"><?= $language::get('ac_balance_after_generation') ?>:</span>
<span class="fw-bold" id="card-balance-after"><?= number_format($userCredits, 2); ?> <?= $language::get('ac_credits') ?></span>
<!-- Balance After Generation Box -->
<div class="d-flex justify-content-between align-items-center p-3 rounded-3 bg-body border" id="balance-after-box">
<span class="small text-muted fw-semibold d-flex align-items-center gap-2">
<i class="ti tabler-scale fs-5 text-info"></i>
<?= $language::get('ac_balance_after_generation') ?>:
</span>
<span class="fw-bold text-heading fs-6" id="card-balance-after"><?= number_format($userCredits, 2); ?> <?= $language::get('ac_credits') ?></span>
</div>
<div id="insufficient-balance-alert" class="alert alert-danger bg-danger text-white border-0 mt-3 d-none mb-0">
<i class="ti tabler-alert-circle me-1"></i> <?= $language::get('ac_insufficient_balance') ?>
<!-- Insufficient Balance Alert -->
<div id="insufficient-balance-alert" class="alert alert-danger d-flex align-items-center mt-3 d-none mb-0" role="alert">
<i class="ti tabler-alert-circle fs-4 me-2 flex-shrink-0"></i>
<div><?= $language::get('ac_insufficient_balance') ?></div>
</div>
</div>
</div>
+1 -1
View File
@@ -36,7 +36,7 @@ $rCanKill = !empty($rPermissions['reseller_client_connection_logs']);
// Deep-link owner filter (?owner=ID) — pre-selects the matching static option.
$rSelectedOwner = RequestManager::has('owner') ? (string) RequestManager::get('owner') : '';
$rSelectedFilter = RequestManager::has('filter') ? (int) RequestManager::get('filter') : 0;
$rSelectedFilter = RequestManager::has('filter') ? (int) RequestManager::get('filter') : (RequestManager::has('trial') && RequestManager::get('trial') == 1 ? 4 : 0);
// Reseller Enigma2 status filter values handled by ResellerTableRenderer::handleEnigmas.
$rStatusFilters = [1 => $language::get('active'), 2 => $language::get('disabled'), 3 => $language::get('expired'), 4 => $language::get('trial')];
+1 -1
View File
@@ -42,7 +42,7 @@ if (empty($rSiteUrl)) {
// Deep-link owner filter (?owner=ID) — pre-selects the matching static option.
$rSelectedOwner = RequestManager::has('owner') ? (string) RequestManager::get('owner') : '';
$rSelectedFilter = RequestManager::has('filter') ? (int) RequestManager::get('filter') : 0;
$rSelectedFilter = RequestManager::has('filter') ? (int) RequestManager::get('filter') : (RequestManager::has('trial') && RequestManager::get('trial') == 1 ? 5 : 0);
// Reseller status filter values handled by ResellerTableRenderer::handleLines.
$rStatusFilters = [1 => 'Active', 2 => 'Disabled', 3 => 'Banned', 4 => 'Expired', 5 => 'Trial'];
+1 -1
View File
@@ -36,7 +36,7 @@ $rCanKill = !empty($rPermissions['reseller_client_connection_logs']);
// Deep-link owner filter (?owner=ID) — pre-selects the matching static option.
$rSelectedOwner = RequestManager::has('owner') ? (string) RequestManager::get('owner') : '';
$rSelectedFilter = RequestManager::has('filter') ? (int) RequestManager::get('filter') : 0;
$rSelectedFilter = RequestManager::has('filter') ? (int) RequestManager::get('filter') : (RequestManager::has('trial') && RequestManager::get('trial') == 1 ? 4 : 0);
// Reseller MAG status filter values handled by ResellerTableRenderer::handleMags.
$rStatusFilters = [1 => $language::get('active'), 2 => $language::get('disabled'), 3 => $language::get('expired'), 4 => $language::get('trial')];