Major client-area overhaul, WHMCS 9 + VirtFusion v7 compatibility, and a
hardening pass on every destructive client.php endpoint.
Tested against WHMCS 9.0.3 + VirtFusion v7.0.0 Build 9.
Features
- "On This Page" jump-link group injected into the WHMCS Actions sidebar
via ClientAreaPrimarySidebar; auto-hides links for hidden panels.
- Monthly traffic chart (last 12 months) with rx/tx bars and centered
legend; replaces the dead canvas that read non-existent JSON paths.
- Live Stats panel: CPU, memory, disk I/O from remoteState; 30s refresh
while the panel is visible AND the page has focus.
- Filesystem usage rows in the Resources panel from qemu-guest-agent
fsinfo; pseudo-FS filtered out.
- Server Overview meta chips: data-center location with country flag,
OS template/agent name with kernel on hover, "Created N days ago".
- Hypervisor maintenance banner at the top of the page.
- Mask Sensitive screenshot mode: IPv4 keeps first two octets, IPv6
keeps first two hextets, hostnames keep first char per dot-label.
Inputs masked via text-security: disc; covers Server Name + Hostname
+ IP cells + rDNS panel rows.
- Per-IP copy buttons folded into the Server Overview cells (replaces
the deleted standalone Network panel).
- VNC viewer popup served from a same-origin authenticated route
(client.php?action=vncViewer) — POST + requireSameOrigin, rotates
the wss token on every open, X-Frame-Options DENY, strict CSP.
Bug Fixes
- UsageUpdate cron silently no-op'd: read server.usage.traffic.used
which doesn't exist. Bandwidth now from /servers/{id}/traffic;
disk usage from remoteState.agent.fsinfo.
- WHMCS 9 multi-service order short-circuit: AfterModuleCreate's
AcceptOrder fired after the first service and terminated the batch
loop, orphaning siblings. Defer until every VF service in the order
has a server_id.
- Orphaned services produced six generic 500s; new
requireProvisionedService() helper emits one clean 409 with an
actionable message. Wired into all 17 client.php cases.
- Server Overview Traffic showed "- / Unlimited"; now renders real
bytes and "Unmetered" (limit=0 is per-period uncapped, not feature-off).
- Rename endpoint moved to PUT /servers/{id}/modify/name in VF v7
(was 404'ing); response is HTTP 201 not 200/204.
- Rename was force-lowercasing the input; relaxed validation to
preserve case + freeze the input row mid-flight to prevent
double-submits.
- "Other" OS category icon override removed; uses VirtFusion's icon
instead of a hardcoded SVG.
- Save button squish on the rename row fixed via flex-wrap layout.
Security
- CSRF protection (requirePost + requireSameOrigin) added to every
destructive POST: rebuild, resetPassword, resetServerPassword,
powerAction, rename, selfServiceAddCredit, toggleVnc, vncViewer.
Previously only rdnsUpdate had it.
- Open-redirect defence in Module::fetchLoginTokens — refuses to
return a redirect URL whose host doesn't match the configured VF
panel hostname.
- Per-action rate limiting via new Module::requireRateLimit helper
(Cache-backed): rebuild 60s, resetPassword/resetServerPassword 30s,
powerAction 10s, vncViewer/toggleVnc/selfServiceAddCredit 5s.
- vncViewer route delivers strict Content-Security-Policy
(default-src none, script-src self + VF panel, connect-src wss VF
panel, frame-ancestors none).
- IPv6 examples in placeholder/comments switched to the IANA
documentation prefix 2001:db8::/32 (RFC 3849).
Removed
- Network panel (duplicated Server Overview IP rows).
- VNC enable/disable toggle (VF firewall flag is non-functional;
toggle was misleading).
- Network Speed row in Resources panel (always 0 from VF API).
Internal
- Module::fetchServerData now passes ?remoteState=true.
- ServerResource::process exposes osName/osPretty/osKernel/osDistro/
osIcon/location/locationIcon/hypervisorMaintenance/createdAt/
builtAt/live.* fields.
- Module::toggleVnc corrected to send {vnc:bool} (the actual API
param) instead of {enabled:bool} (silent no-op).
- Module::getVncConsole + toggleVnc return baseUrl alongside the
envelope so the viewer route can build the wss URL.
- Panel margins tightened mb-3 → mb-2 across all 11 panels.
This commit is contained in:
@@ -358,12 +358,20 @@ function VirtFusionDirect_UsageUpdate(array $params)
|
||||
foreach ($services as $service) {
|
||||
try {
|
||||
$systemService = Database::getSystemService($service->id);
|
||||
if (! $systemService) {
|
||||
if (! $systemService || empty($systemService->server_id)) {
|
||||
// No VirtFusion server linked to this WHMCS service yet —
|
||||
// either provisioning hasn't happened or it failed mid-create.
|
||||
// Skipping is correct: there is nothing to read usage from.
|
||||
continue;
|
||||
}
|
||||
|
||||
// Fetch server settings (limits + storage profile) with remoteState=true
|
||||
// so the qemu-agent fsinfo block is included for disk usage. The agent
|
||||
// is best-effort — guests without qemu-agent installed will have no
|
||||
// fsinfo, in which case we simply skip the diskused write rather than
|
||||
// zeroing it.
|
||||
$request = $module->initCurl($cp['token']);
|
||||
$data = $request->get($cp['url'] . '/servers/' . (int) $systemService->server_id);
|
||||
$data = $request->get($cp['url'] . '/servers/' . (int) $systemService->server_id . '?remoteState=true');
|
||||
|
||||
if ($request->getRequestInfo('http_code') != 200) {
|
||||
continue;
|
||||
@@ -377,19 +385,43 @@ function VirtFusionDirect_UsageUpdate(array $params)
|
||||
$server = $serverData['data'];
|
||||
$update = [];
|
||||
|
||||
// Disk usage (WHMCS expects MB)
|
||||
if (isset($server['usage']['storage']['used'])) {
|
||||
$update['diskused'] = round($server['usage']['storage']['used'] / 1048576);
|
||||
// Disk usage (WHMCS expects MB) — derived from qemu-agent fsinfo when
|
||||
// available. Sum across all reported filesystems (root + any extra
|
||||
// mounts) and convert bytes -> MB. If the agent isn't running we get
|
||||
// no fsinfo entries and leave diskused untouched.
|
||||
$fsinfo = $server['remoteState']['agent']['fsinfo'] ?? null;
|
||||
if (is_array($fsinfo) && $fsinfo !== []) {
|
||||
$diskUsedBytes = 0;
|
||||
foreach ($fsinfo as $fs) {
|
||||
if (isset($fs['used-bytes']) && is_numeric($fs['used-bytes'])) {
|
||||
$diskUsedBytes += (int) $fs['used-bytes'];
|
||||
}
|
||||
}
|
||||
if ($diskUsedBytes > 0) {
|
||||
$update['diskused'] = (int) round($diskUsedBytes / 1048576);
|
||||
}
|
||||
}
|
||||
if (isset($server['settings']['resources']['storage'])) {
|
||||
// settings.resources.storage is in GB; WHMCS disklimit is MB.
|
||||
$update['disklimit'] = (int) $server['settings']['resources']['storage'] * 1024;
|
||||
}
|
||||
|
||||
// Bandwidth usage (WHMCS expects MB)
|
||||
if (isset($server['usage']['traffic']['used'])) {
|
||||
$update['bwused'] = round($server['usage']['traffic']['used'] / 1048576);
|
||||
// Bandwidth usage (WHMCS expects MB) — fetched from the dedicated
|
||||
// /servers/{id}/traffic endpoint, which is the canonical source for
|
||||
// billing-period totals. The /servers/{id} response only exposes the
|
||||
// current period's window (start/end/limit), not the byte counter.
|
||||
$trafficRequest = $module->initCurl($cp['token']);
|
||||
$trafficData = $trafficRequest->get($cp['url'] . '/servers/' . (int) $systemService->server_id . '/traffic');
|
||||
if ($trafficRequest->getRequestInfo('http_code') == 200) {
|
||||
$trafficJson = json_decode($trafficData, true);
|
||||
$currentPeriod = $trafficJson['data']['monthly'][0] ?? null;
|
||||
if (is_array($currentPeriod) && isset($currentPeriod['total']) && is_numeric($currentPeriod['total'])) {
|
||||
$update['bwused'] = (int) round($currentPeriod['total'] / 1048576);
|
||||
}
|
||||
}
|
||||
if (isset($server['settings']['resources']['traffic'])) {
|
||||
// settings.resources.traffic is in GB; 0 means unlimited, which
|
||||
// WHMCS represents the same way (0 bwlimit = no cap).
|
||||
$trafficGB = (int) $server['settings']['resources']['traffic'];
|
||||
$update['bwlimit'] = $trafficGB > 0 ? $trafficGB * 1024 : 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user