/64; strip the prefix length.
$primaryIp = explode('/', $create['server']['public_net']['ipv6']['ip'])[0];
}
$hostingUpdate = array('username' => 'root');
if ($primaryIp !== '') {
$hostingUpdate['dedicatedip'] = $primaryIp;
}
Capsule::table('tblhosting')->where('id', $params['serviceid'])->update($hostingUpdate);
// Store the root password if provided
if (isset($create['root_password'])) {
Capsule::table('tblhosting')->where('id', $params['serviceid'])->update([
'password' => encrypt($create['root_password'])
]);
// Save the timestamp when password was set for expiration tracking (72 hours)
$params['model']->serviceProperties->save([
'ArkHostHetznerVPS|Password Set Time' => time()
]);
}
// Handle floating IP creation if requested via Configurable Options or Module Settings
$floatingIpType = ArkHostHetznerVPS_GetConfigurableOption($params, 'floating');
$createFloatingIP = ArkHostHetznerVPS_GetOption($params, 'create_floating_ip');
if (($floatingIpType && $floatingIpType !== '' && strtolower($floatingIpType) !== 'none') ||
($createFloatingIP && $createFloatingIP === 'on')) {
// Determine floating IP type - default to ipv4 if module setting is used
$ipType = $floatingIpType && $floatingIpType !== '' ? $floatingIpType : 'ipv4';
try {
$floatingIpParams = $params;
$floatingIpParams['action'] = 'Create Floating IP';
$floatingIpParams['ip_type'] = $ipType;
$floatingIpParams['location'] = ArkHostHetznerVPS_NormalizeLocation(
ArkHostHetznerVPS_GetOption($params, 'floating_ip_location')
);
$floatingIpParams['description'] = 'Server ID: ' . $create['server']['id'];
$floatingIpParams['assign_to_server'] = true;
$floatingIp = ArkHostHetznerVPS_API($floatingIpParams);
if (isset($floatingIp['floating_ip']['id'])) {
// Store floating IP information
$params['model']->serviceProperties->save([
'ArkHostHetznerVPS|Floating IP ID' => $floatingIp['floating_ip']['id'],
'ArkHostHetznerVPS|Floating IP' => $floatingIp['floating_ip']['ip'],
]);
}
} catch (Exception $floatingIpErr) {
// You might want to send an admin notification here
}
}
} catch (Exception $err) {
ArkHostHetznerVPS_Error(__FUNCTION__, $params, $err);
return 'Received the error: ' . $err->getMessage() . ' Check module debug log for more detailed error.';
}
return 'success';
}
function ArkHostHetznerVPS_SuspendAccount(array $params) {
try {
$params['action'] = 'Disable';
ArkHostHetznerVPS_API($params);
} catch (Exception $err) {
ArkHostHetznerVPS_Error(__FUNCTION__, $params, $err);
return 'Received the error: ' . $err->getMessage() . ' Check module debug log for more detailed error.';
}
return 'success';
}
function ArkHostHetznerVPS_UnsuspendAccount(array $params) {
try {
$params['action'] = 'Enable';
ArkHostHetznerVPS_API($params);
} catch (Exception $err) {
ArkHostHetznerVPS_Error(__FUNCTION__, $params, $err);
return 'Received the error: ' . $err->getMessage() . ' Check module debug log for more detailed error.';
}
return 'success';
}
function ArkHostHetznerVPS_TerminateAccount(array $params) {
try {
// First, check if there's a floating IP to delete
$floatingIpId = $params['model']->serviceProperties->get('ArkHostHetznerVPS|Floating IP ID');
if ($floatingIpId) {
try {
$floatingIpParams = $params;
$floatingIpParams['action'] = 'Delete Floating IP';
$floatingIpParams['floating_ip_id'] = $floatingIpId;
ArkHostHetznerVPS_API($floatingIpParams);
} catch (Exception $floatingIpErr) {
}
}
// Then terminate the server
$params['action'] = 'Cancel';
$params['when'] = 'now';
ArkHostHetznerVPS_API($params);
Capsule::table('tblhosting')->where('id', $params['serviceid'])->update(array(
'username' => '',
'password' => '',
));
} catch (Exception $err) {
ArkHostHetznerVPS_Error(__FUNCTION__, $params, $err);
return 'Received the error: ' . $err->getMessage() . ' Check module debug log for more detailed error.';
}
return 'success';
}
function ArkHostHetznerVPS_ChangePackage(array $params) {
try {
$params['action'] = 'Upgrade';
ArkHostHetznerVPS_API($params);
} catch (Exception $err) {
ArkHostHetznerVPS_Error(__FUNCTION__, $params, $err);
return 'Received the error: ' . $err->getMessage() . ' Check module debug log for more detailed error.';
}
return 'success';
}
function ArkHostHetznerVPS_Start(array $params) {
try {
$params['action'] = 'Start';
ArkHostHetznerVPS_API($params);
} catch (Exception $err) {
ArkHostHetznerVPS_Error(__FUNCTION__, $params, $err);
return 'Received the error: ' . $err->getMessage() . ' Check module debug log for more detailed error.';
}
return 'success';
}
function ArkHostHetznerVPS_Reboot(array $params) {
try {
$params['action'] = 'Reboot';
ArkHostHetznerVPS_API($params);
} catch (Exception $err) {
ArkHostHetznerVPS_Error(__FUNCTION__, $params, $err);
return 'Received the error: ' . $err->getMessage() . ' Check module debug log for more detailed error.';
}
return 'success';
}
function ArkHostHetznerVPS_Stop(array $params) {
try {
$params['action'] = 'Stop';
ArkHostHetznerVPS_API($params);
} catch (Exception $err) {
ArkHostHetznerVPS_Error(__FUNCTION__, $params, $err);
return 'Received the error: ' . $err->getMessage() . ' Check module debug log for more detailed error.';
}
return 'success';
}
function ArkHostHetznerVPS_VNC(array $params) {
try {
$params['action'] = 'VNC Console';
$vnc = ArkHostHetznerVPS_API($params);
// Hetzner returns wss_url and password
if (isset($vnc['wss_url']) && isset($vnc['password'])) {
$consoleUrl = $vnc['wss_url'];
$vncPassword = $vnc['password'];
// Generate a standalone HTML file that can be opened separately
$html = '
VNC Console - ' . htmlspecialchars($params['domain']) . '
VNC Console - ' . htmlspecialchars($params['domain']) . '
Password: ' . htmlspecialchars($vncPassword) . '
Initializing VNC Client...
';
// Set proper headers to prevent session issues
header('Content-Type: text/html; charset=utf-8');
header('Cache-Control: no-cache, no-store, must-revalidate');
header('Pragma: no-cache');
header('Expires: 0');
// Output the HTML and exit immediately
echo $html;
die();
} else {
throw new Exception('Console URL not found in response');
}
} catch (Exception $err) {
ArkHostHetznerVPS_Error(__FUNCTION__, $params, $err);
header('Content-Type: text/html; charset=utf-8');
echo '
VNC Console Error
VNC Console Error
' . htmlspecialchars($err->getMessage()) . '
Unable to open VNC console. Please try again or contact support if the problem persists.
Close Window
';
die();
}
}
function ArkHostHetznerVPS_AdminCustomButtonArray() {
return array(
'Start' => 'Start',
'Stop'=> 'Stop',
'Reboot' => 'Reboot',
'Shutdown' => 'Shutdown',
'VNC Console'=> 'VNC',
'Enable Rescue' => 'EnableRescue',
'Reset Root Password' => 'ResetRoot',
'Create Snapshot' => 'CreateSnapshot',
'Enable Backups' => 'EnableBackups',
'Disable Backups' => 'DisableBackups',
);
}
function ArkHostHetznerVPS_AdminLink(array $params) {
try {
// Check if we have a VPS ID first
$vpsId = ArkHostHetznerVPS_GetVPSID($params);
if (!$vpsId) {
// This might be called from server configuration page where there's no service
return ' Hetzner Cloud Server';
}
// Get server info to display
$params['action'] = 'Server Info';
$serverInfo = ArkHostHetznerVPS_API($params);
if (isset($serverInfo['server'])) {
$server = $serverInfo['server'];
// Self-heal: backfill IP/username for services created before this was
// stored at provisioning time (and keep the IP current after rebuilds).
$primaryIp = '';
if (!empty($server['public_net']['ipv4']['ip'])) {
$primaryIp = $server['public_net']['ipv4']['ip'];
} elseif (!empty($server['public_net']['ipv6']['ip'])) {
$primaryIp = explode('/', $server['public_net']['ipv6']['ip'])[0];
}
if (!empty($params['serviceid'])) {
$hostingUpdate = array();
if ($primaryIp !== '' && ($params['dedicatedip'] ?? '') !== $primaryIp) {
$hostingUpdate['dedicatedip'] = $primaryIp;
}
if (($params['username'] ?? '') !== 'root') {
$hostingUpdate['username'] = 'root';
}
if (!empty($hostingUpdate)) {
Capsule::table('tblhosting')->where('id', $params['serviceid'])->update($hostingUpdate);
}
}
return ' Status: ' . $server['status'] . '
IP: ' . ($primaryIp !== '' ? $primaryIp : 'N/A');
}
return 'Server ID: ' . $vpsId;
} catch (Exception $err) {
ArkHostHetznerVPS_Error(__FUNCTION__, $params, $err);
return 'Unable to retrieve server info';
}
}
function ArkHostHetznerVPS_ClientAreaAPI(array $params) {
try {
$action = App::getFromRequest('api');
$actions = array('Server Info', 'Graphs', 'Reinstall', 'Reboot', 'Stop', 'Shutdown', 'Start', 'IPv4 Addresses', 'Hostname rDNS', 'Create backup', 'Delete backup', 'List backups', 'Restore backup', 'Get Firewall rules', 'Add Firewall rules', 'Delete Firewall rule', 'Commit Firewall rules', 'ISO Images', 'Load ISO', 'Eject ISO', 'Reset root', 'Create Snapshot', 'List Snapshots', 'Server Metrics', 'Rescue Mode', 'Disable Rescue Mode', 'GetFloatingIPStatus', 'AssignFloatingIP', 'UnassignFloatingIP', 'SetFloatingIPReverseDNS');
$results = array('result' => 'success');
if (in_array($action, $actions)) {
foreach ($_POST as $key => $value) {
$params[$key] = $value;
}
// Check backup permissions
$backupActions = array('Create backup', 'Delete backup', 'Restore backup');
if (in_array($action, $backupActions)) {
// Check if backups are enabled in module settings
$backupsEnabled = (ArkHostHetznerVPS_GetOption($params, 'backups') === 'on');
if (!$backupsEnabled) {
return array('jsonResponse' => array(
'result' => 'error',
'message' => 'Backups are not enabled for this service. Please upgrade your plan to enable backups.'
));
}
}
// Check floating IP permissions
$floatingIPActions = array('GetFloatingIPStatus', 'AssignFloatingIP', 'UnassignFloatingIP', 'SetFloatingIPReverseDNS');
if (in_array($action, $floatingIPActions)) {
// Check if customer has floating IP via configurable options, module settings, or existing service
$floatingIPOption = ArkHostHetznerVPS_GetConfigurableOption($params, 'Floating IP');
$moduleFloatingIP = ArkHostHetznerVPS_GetOption($params, 'create_floating_ip');
$hasFloatingIPService = $params['model']->serviceProperties->get('ArkHostHetznerVPS|Floating IP ID');
if (!$floatingIPOption && !($moduleFloatingIP && $moduleFloatingIP === 'on') && !$hasFloatingIPService) {
return array('jsonResponse' => array(
'result' => 'error',
'message' => 'Floating IP not available for this service. Please upgrade your plan to add floating IP.'
));
}
// Handle GetFloatingIPStatus specially (not a real Hetzner API call)
if ($action === 'GetFloatingIPStatus') {
try {
// Get current server ID safely
$currentServerId = null;
try {
$currentServerId = ArkHostHetznerVPS_GetVPSID($params);
} catch (Exception $vpsIdErr) {
// VPS ID retrieval failed - continue without it
}
// First, check if this service has a specific floating IP stored
$serviceFloatingIPId = $params['model']->serviceProperties->get('ArkHostHetznerVPS|Floating IP ID');
$customerFloatingIP = null;
if ($serviceFloatingIPId) {
// Get the specific floating IP for this service
try {
$floatingIPResult = ArkHostHetznerVPS_API(array_merge($params, array(
'action' => 'Get Floating IP',
'floating_ip_id' => $serviceFloatingIPId
)));
if (isset($floatingIPResult['floating_ip'])) {
$customerFloatingIP = $floatingIPResult['floating_ip'];
}
} catch (Exception $e) {
// Floating IP might have been deleted outside WHMCS
// Remove invalid ID from service properties
$params['model']->serviceProperties->save([
'ArkHostHetznerVPS|Floating IP ID' => '',
'ArkHostHetznerVPS|Floating IP' => ''
]);
}
}
// If no floating IP exists but customer has floating IP access, create one
if (!$customerFloatingIP && ($floatingIPOption || ($moduleFloatingIP && $moduleFloatingIP === 'on'))) {
try {
$createParams = $params;
$createParams['action'] = 'Create Floating IP';
$createParams['ip_type'] = 'ipv4';
$createParams['location'] = ArkHostHetznerVPS_NormalizeLocation(
ArkHostHetznerVPS_GetOption($params, 'floating_ip_location')
) ?: ArkHostHetznerVPS_GetLocationOption($params);
$createParams['description'] = 'WHMCS Service ID: ' . $params['serviceid'];
$createParams['assign_to_server'] = true;
$createResult = ArkHostHetznerVPS_API($createParams);
if (isset($createResult['floating_ip']['id'])) {
// Store floating IP information in service properties
$params['model']->serviceProperties->save([
'ArkHostHetznerVPS|Floating IP ID' => $createResult['floating_ip']['id'],
'ArkHostHetznerVPS|Floating IP' => $createResult['floating_ip']['ip'],
]);
$customerFloatingIP = $createResult['floating_ip'];
}
} catch (Exception $createErr) {
// Failed to create floating IP - log error but continue
logModuleCall(
'ArkHostHetznerVPS',
'CreateFloatingIPOnDemand',
$params,
'Failed to create floating IP: ' . $createErr->getMessage(),
'',
array()
);
}
}
if ($customerFloatingIP) {
$floatingIPData = array(
'hasFloatingIP' => true,
'floatingIP' => $customerFloatingIP,
'assigned' => $customerFloatingIP['server'] !== null,
'server_id' => $customerFloatingIP['server'] ? $customerFloatingIP['server']['id'] : null,
'current_server_id' => $currentServerId
);
} else {
// Customer has floating IP access but no floating IP found - this is valid
$floatingIPData = array(
'hasFloatingIP' => true,
'floatingIP' => null,
'assigned' => false,
'server_id' => null,
'current_server_id' => $currentServerId
);
}
return array('jsonResponse' => array(
'result' => 'success',
'data' => $floatingIPData
));
} catch (Exception $statusErr) {
return array('jsonResponse' => array(
'result' => 'error',
'message' => 'Failed to load floating IP status: ' . $statusErr->getMessage()
));
}
}
}
$params['action'] = $action;
$result = ArkHostHetznerVPS_API($params);
// Special handling for specific responses
if ($action === 'Graphs') {
// Handle Hetzner metrics response
if (isset($result['metrics'])) {
// Process time series data
$processedMetrics = array();
// Determine date format based on time period
$dateFormat = 'H:i'; // Default for hour/day
if (isset($params['time'])) {
switch ($params['time']) {
case 'hour':
$dateFormat = 'H:i';
break;
case 'day':
$dateFormat = 'H:i';
break;
case 'week':
$dateFormat = 'M d';
break;
case 'month':
$dateFormat = 'M d';
break;
case 'year':
$dateFormat = 'M Y';
break;
}
}
// Extract CPU usage
if (isset($result['metrics']['time_series']['cpu'])) {
$cpuData = $result['metrics']['time_series']['cpu']['values'];
$processedMetrics['cpu'] = array(
'labels' => array(),
'data' => array()
);
foreach ($cpuData as $point) {
$processedMetrics['cpu']['labels'][] = date($dateFormat, $point[0]);
$processedMetrics['cpu']['data'][] = round($point[1], 2);
}
}
// Extract disk I/O
if (isset($result['metrics']['time_series']['disk.0.iops.read'])) {
$diskReadData = $result['metrics']['time_series']['disk.0.iops.read']['values'];
$diskWriteData = $result['metrics']['time_series']['disk.0.iops.write']['values'];
$processedMetrics['disk'] = array(
'labels' => array(),
'read' => array(),
'write' => array()
);
foreach ($diskReadData as $i => $point) {
$processedMetrics['disk']['labels'][] = date($dateFormat, $point[0]);
$processedMetrics['disk']['read'][] = round($point[1], 2);
$processedMetrics['disk']['write'][] = isset($diskWriteData[$i]) ? round($diskWriteData[$i][1], 2) : 0;
}
}
// Extract network traffic
if (isset($result['metrics']['time_series']['network.0.bandwidth.in'])) {
$netInData = $result['metrics']['time_series']['network.0.bandwidth.in']['values'];
$netOutData = $result['metrics']['time_series']['network.0.bandwidth.out']['values'];
$processedMetrics['network'] = array(
'labels' => array(),
'in' => array(),
'out' => array()
);
foreach ($netInData as $i => $point) {
$processedMetrics['network']['labels'][] = date($dateFormat, $point[0]);
// Convert bytes/s to Mbps
$processedMetrics['network']['in'][] = round(($point[1] * 8) / 1000000, 2);
$processedMetrics['network']['out'][] = isset($netOutData[$i]) ? round(($netOutData[$i][1] * 8) / 1000000, 2) : 0;
}
}
$results['graphs'] = array(
'type' => 'metrics',
'data' => $processedMetrics
);
} else {
// No metrics available
$results['graphs'] = array(
'type' => 'none',
'message' => 'Metrics not available for this server'
);
}
} else if ($action === 'List backups' || $action === 'List Snapshots') {
// Handle image list response for Hetzner
if (isset($result['images'])) {
// Log the number of images found
logModuleCall(
'ArkHostHetznerVPS',
'ListBackups',
array('count' => count($result['images'])),
'Found ' . count($result['images']) . ' backup images',
'',
array()
);
// Return backups with numeric keys for JavaScript compatibility
$backupIndex = 0;
foreach ($result['images'] as $image) {
$results[$backupIndex] = array(
'id' => $image['id'],
'name' => $image['description'] ?? $image['name'],
'created' => $image['created'],
'size' => isset($image['image_size']) ? round($image['image_size'], 2) : 0, // Already in GB from API
'type' => $image['type'],
'status' => $image['status'] ?? 'available' // Include status from Hetzner API
);
$backupIndex++;
}
} else {
// No backups found
$results['message'] = 'No backups found';
logModuleCall(
'ArkHostHetznerVPS',
'ListBackups',
array('result' => $result),
'No images key in API response',
'',
array()
);
}
} else if ($action === 'Server Info') {
// Handle server info for Hetzner
if (isset($result['server'])) {
$results = array_merge($results, $result['server']);
}
} else if ($action === 'ISO Images') {
// Handle ISO list for Hetzner
if (isset($result['isos'])) {
$results['isos'] = $result['isos'];
}
} else if ($action === 'Get Firewall rules') {
// For Hetzner, we need to check if server has firewalls attached
// and then fetch the firewall details separately
$rules = array();
// Check for firewall IDs in the correct location
$firewallIds = array();
if (isset($result['server']['public_net']['firewalls']) && !empty($result['server']['public_net']['firewalls'])) {
// Extract firewall IDs from the firewalls array
foreach ($result['server']['public_net']['firewalls'] as $firewall) {
if (isset($firewall['id'])) {
$firewallIds[] = $firewall['id'];
}
}
}
if (!empty($firewallIds)) {
// Server has firewalls attached, fetch the actual firewall rules
foreach ($firewallIds as $firewallId) {
// Fetch firewall details
$firewallParams = $params;
$firewallParams['action'] = 'Get Firewall Details';
$firewallParams['firewall_id'] = $firewallId;
try {
$firewallResult = ArkHostHetznerVPS_API($firewallParams);
if (isset($firewallResult['firewall']['rules'])) {
$inIndex = 0;
$outIndex = 0;
foreach ($firewallResult['firewall']['rules'] as $rule) {
// Process both inbound and outbound rules
if ($rule['direction'] === 'in' || $rule['direction'] === 'out') {
// Handle IPs based on direction
$ips = '0.0.0.0/0'; // Default when no IPs specified
if ($rule['direction'] === 'out') {
// For outbound rules, use destination_ips
if (isset($rule['destination_ips']) && is_array($rule['destination_ips'])) {
if (!empty($rule['destination_ips'])) {
$ips = implode(', ', $rule['destination_ips']);
}
}
} else {
// For inbound rules, use source_ips
if (isset($rule['source_ips']) && is_array($rule['source_ips'])) {
if (!empty($rule['source_ips'])) {
$ips = implode(', ', $rule['source_ips']);
}
}
}
// Handle port range or single port
$port = '';
if (isset($rule['port'])) {
$port = $rule['port'];
} elseif (isset($rule['port_range'])) {
$port = $rule['port_range'];
} else {
$port = 'Any';
}
// Generate ID based on direction and index
$ruleId = 'fw_' . $firewallId . '_' . $rule['direction'] . '_';
if ($rule['direction'] === 'in') {
$ruleId .= $inIndex;
$inIndex++;
} else {
$ruleId .= $outIndex;
$outIndex++;
}
$rules[] = array(
'id' => $ruleId,
'direction' => $rule['direction'],
'action' => 'ACCEPT',
'protocol' => strtoupper($rule['protocol']),
'port' => $port,
'source' => $ips
);
}
}
}
} catch (Exception $e) {
// If we can't fetch firewall details, show a message
$rules[] = array(
'id' => 'error_' . $firewallId,
'action' => 'INFO',
'protocol' => 'N/A',
'port' => 'N/A',
'source' => 'N/A',
'note' => 'Unable to fetch firewall ' . $firewallId . ' details'
);
}
}
if (empty($rules)) {
$results['message'] = 'This server has ' . count($firewallIds) . ' firewall(s) attached but no inbound rules configured.';
}
} else {
// No firewalls attached - don't show any rules
$results['message'] = 'No firewall attached to this server. All traffic is allowed by default. Create a firewall rule to enable protection.';
// Return empty rules array
$rules = array();
}
// Return rules with numeric keys for JavaScript compatibility
foreach ($rules as $index => $rule) {
$results[$index] = $rule;
}
} else if ($action === 'Reinstall') {
// Handle rebuild response - save new root password if provided
if (isset($result['root_password']) && !empty($result['root_password'])) {
// Save the new root password
Capsule::table('tblhosting')
->where('id', $params['serviceid'])
->update(['password' => encrypt($result['root_password'])]);
// Save the timestamp when password was set for expiration tracking (72 hours)
$params['model']->serviceProperties->save([
'ArkHostHetznerVPS|Password Set Time' => time()
]);
$results['root_password'] = $result['root_password'];
$results['message'] = 'Server rebuild initiated. New root password has been saved.';
} else {
$results['message'] = 'Server rebuild initiated. No new password generated (SSH keys used).';
}
$results = array_merge($results, is_array($result) ? $result : array('data' => $result));
} else if ($action === 'AssignFloatingIP') {
// Handle floating IP assignment - only allow assignment of service's own floating IP
$serviceFloatingIPId = $params['model']->serviceProperties->get('ArkHostHetznerVPS|Floating IP ID');
if ($serviceFloatingIPId && $serviceFloatingIPId === $params['floating_ip_id']) {
$assignResult = ArkHostHetznerVPS_API(array_merge($params, array('action' => 'Assign Floating IP')));
$results = array_merge($results, is_array($assignResult) ? $assignResult : array('data' => $assignResult));
} else {
$results['result'] = 'error';
$results['message'] = 'Invalid floating IP ID for this service';
}
} else if ($action === 'UnassignFloatingIP') {
// Handle floating IP unassignment - only allow unassignment of service's own floating IP
$serviceFloatingIPId = $params['model']->serviceProperties->get('ArkHostHetznerVPS|Floating IP ID');
if ($serviceFloatingIPId && $serviceFloatingIPId === $params['floating_ip_id']) {
$unassignResult = ArkHostHetznerVPS_API(array_merge($params, array('action' => 'Unassign Floating IP')));
$results = array_merge($results, is_array($unassignResult) ? $unassignResult : array('data' => $unassignResult));
} else {
$results['result'] = 'error';
$results['message'] = 'Invalid floating IP ID for this service';
}
} else if ($action === 'SetFloatingIPReverseDNS') {
// Handle floating IP reverse DNS update - only allow for service's own floating IP
$serviceFloatingIPId = $params['model']->serviceProperties->get('ArkHostHetznerVPS|Floating IP ID');
if ($serviceFloatingIPId && $serviceFloatingIPId === $params['floating_ip_id']) {
$dnsResult = ArkHostHetznerVPS_API(array_merge($params, array('action' => 'Change Floating IP DNS')));
$results = array_merge($results, is_array($dnsResult) ? $dnsResult : array('data' => $dnsResult));
} else {
$results['result'] = 'error';
$results['message'] = 'Invalid floating IP ID for this service';
}
} else if ($action === 'Reset root') {
// Handle password reset from client area
$results = array_merge($results, is_array($result) ? $result : array('data' => $result));
// Store the new root password and timestamp if provided
if (isset($result['root_password'])) {
Capsule::table('tblhosting')->where('id', $params['serviceid'])->update([
'password' => encrypt($result['root_password'])
]);
// Save the timestamp when password was set for expiration tracking (72 hours)
$params['model']->serviceProperties->save([
'ArkHostHetznerVPS|Password Set Time' => time()
]);
}
} else {
$results = array_merge($results, is_array($result) ? $result : array('data' => $result));
}
return array('jsonResponse' => $results);
} else {
throw new Exception('Action not allowed');
}
} catch (Exception $e) {
return array('jsonResponse' => array('result' => 'error', 'message' => $e->getMessage()));
}
}
function ArkHostHetznerVPS_DeliverFile(array $params) {
try {
$dir = __DIR__ . '/template/';
$file = App::getFromRequest('file');
$files = array('app.min.css', 'app.min.js');
if (in_array($file, $files)) {
$type = '';
if (function_exists('ob_gzhandler')) {
ob_start('ob_gzhandler');
}
if (strpos($file, '.js') !== false) {
$dir .= 'js/';
$type = 'application/javascript';
} else if (strpos($file, '.css') !== false) {
$dir .= 'css/';
$type = 'text/css';
} else {
$type = 'text/html';
}
header('Content-Type: ' . $type . '; charset=utf-8');
header('Cache-Control: max-age=604800, public');
echo file_get_contents($dir . $file);
WHMCS\Terminus::getInstance()->doExit();
} else {
throw new Exception('File not found');
}
} catch (Exception $err) {
ArkHostHetznerVPS_Error(__FUNCTION__, $params, $err);
return array('jsonResponse' => array('result' => 'error', 'message' => $err->getMessage()));
}
}
function ArkHostHetznerVPS_ClientAreaCustomButtonArray() {
$_LANG = ArkHostHetznerVPS_Translation();
return array(
$_LANG['Start'] => 'Start',
$_LANG['Stop'] => 'Stop',
$_LANG['Restart'] => 'Reboot',
$_LANG['Shutdown'] => 'Shutdown',
$_LANG['VNC'] => 'VNC',
);
}
function ArkHostHetznerVPS_ClientAreaAllowedFunctions() {
return array('ClientAreaAPI', 'DeliverFile');
}
function ArkHostHetznerVPS_ClientArea(array $params) {
if ($params['moduletype'] !== 'ArkHostHetznerVPS') return;
try {
// Get clean VPS ID - this handles migration from other modules
$cleanVpsId = ArkHostHetznerVPS_GetVPSID($params);
if (empty($cleanVpsId)) {
throw new Exception('VPS ID not found. Please check service configuration.');
}
// If we found a VPS ID but it's not stored in our format, store it now for future use
$storedVpsId = $params['model']->serviceProperties->get('ArkHostHetznerVPS|VPS ID');
if (empty($storedVpsId) && !empty($cleanVpsId)) {
// Store the VPS ID for future use (using our format)
$params['model']->serviceProperties->save([
'ArkHostHetznerVPS|VPS ID' => $cleanVpsId,
]);
}
// Check if password is empty (migration from other modules) and set a dummy password
if (empty($params['password'])) {
// Set a dummy password to prevent WHMCS from showing password reset form
// This is just for display - actual server access is via API
Capsule::table('tblhosting')
->where('id', $params['serviceid'])
->update(['password' => encrypt('managed-via-api')]);
// Reload params to get the updated password
$params['password'] = 'managed-via-api';
}
// Get server info and operating systems data
$params['action'] = 'Server Info';
$response = ArkHostHetznerVPS_API($params);
// Check if server info is valid
if (!is_array($response) || !isset($response['server'])) {
throw new Exception('Unable to retrieve server information from API');
}
// Extract server data from Hetzner response
$serverInfo = $response['server'];
$params['action'] = 'Operating Systems - Server';
$operatingSystemsTemp = ArkHostHetznerVPS_API($params);
// Check if operating systems data is valid
if (!is_array($operatingSystemsTemp)) {
$operatingSystemsTemp = array();
}
$dirImages = __DIR__ . '/template/img/';
$availableImages = glob($dirImages . '*.png');
$images = array();
foreach ($availableImages as $key => $image) {
$images[explode('.png', explode($dirImages, $image)[1])[0]] = 'data:image/png;base64,' . base64_encode(file_get_contents($image));
}
$dirOS = __DIR__ . '/template/img/os/';
$availableOS = glob($dirOS . '*.png');
$operatingSystems = array();
foreach ($availableOS as $key => $os) {
$availableOS[$key] = explode('.png', explode($dirOS, $os)[1])[0];
}
// Process operating systems data from Hetzner API
if (!empty($operatingSystemsTemp) && isset($operatingSystemsTemp['images'])) {
// Log the raw OS data for debugging
logModuleCall(
'ArkHostHetznerVPS',
'ProcessOperatingSystems',
array('image_count' => count($operatingSystemsTemp['images'])),
'Processing ' . count($operatingSystemsTemp['images']) . ' OS images',
'',
array()
);
foreach ($operatingSystemsTemp['images'] as $operatingSystem) {
// Skip non-system images
if ($operatingSystem['type'] !== 'system' || $operatingSystem['status'] !== 'available') {
continue;
}
// Use description for display, fall back to name
$displayName = $operatingSystem['description'] ?: $operatingSystem['name'];
$osName = strtolower($displayName);
// Determine the proper group based on OS name
if (strpos($osName, 'alma') !== false) {
$group = 'almalinux';
$groupName = 'AlmaLinux';
} elseif (strpos($osName, 'rocky') !== false) {
$group = 'rocky';
$groupName = 'Rocky Linux';
} elseif (strpos($osName, 'centos') !== false) {
$group = 'centos';
$groupName = 'CentOS';
} elseif (strpos($osName, 'debian') !== false) {
$group = 'debian';
$groupName = 'Debian';
} elseif (strpos($osName, 'ubuntu') !== false) {
$group = 'ubuntu';
$groupName = 'Ubuntu';
} elseif (strpos($osName, 'fedora') !== false) {
$group = 'fedora';
$groupName = 'Fedora';
} elseif (strpos($osName, 'opensuse') !== false || strpos($osName, 'suse') !== false) {
$group = 'opensuse';
$groupName = 'openSUSE';
} elseif (strpos($osName, 'windows') !== false) {
$group = 'windows';
$groupName = 'Windows';
} else {
// For any other OS, use 'others'
$group = 'others';
$groupName = 'Other Systems';
}
if (!isset($operatingSystems[$group])) {
// Map group names to correct image filenames
$imageFile = $group;
if ($group === 'rocky') {
$imageFile = 'rockylinux';
}
$image = file_get_contents($dirOS . (in_array($imageFile, $availableOS) ? $imageFile : 'others') . '.png');
$operatingSystems[$group] = array(
'name' => $groupName,
'image' => 'data:image/png;base64,' . base64_encode($image),
'versions' => array(),
);
}
// Check if this version already exists (avoid duplicates)
// Check both by ID and display name to avoid visual duplicates
$versionExists = false;
if (!empty($operatingSystems[$group]['versions'])) {
foreach ($operatingSystems[$group]['versions'] as $existingVersion) {
// Check if either the ID or display name already exists
if ($existingVersion['id'] === $operatingSystem['name'] ||
$existingVersion['name'] === $displayName) {
$versionExists = true;
// Log duplicate found
logModuleCall(
'ArkHostHetznerVPS',
'DuplicateOSFound',
array(
'group' => $group,
'existing_id' => $existingVersion['id'],
'existing_name' => $existingVersion['name'],
'new_id' => $operatingSystem['name'],
'new_name' => $displayName
),
'Duplicate OS version found',
'',
array()
);
break;
}
}
}
// Only add if not already present
if (!$versionExists) {
// Store both the API name (for rebuild) and display name
$operatingSystems[$group]['versions'][] = array(
'id' => $operatingSystem['name'], // This is what we send to API
'name' => $displayName // This is what we display
);
}
}
// Sort OS versions within each group for consistent display
foreach ($operatingSystems as $group => &$osData) {
if (!empty($osData['versions'])) {
usort($osData['versions'], function($a, $b) {
return strcmp($a['name'], $b['name']);
});
}
}
// Process operating system info for Hetzner
if (isset($serverInfo['image']) && !empty($operatingSystemsTemp['images'])) {
$osId = $serverInfo['image']['name'];
$imageType = $serverInfo['image']['type'] ?? 'system';
// If this is a backup or snapshot image, try to get the actual OS info from os_flavor/os_version
if (($imageType === 'backup' || $imageType === 'snapshot') && isset($serverInfo['image']['os_flavor'])) {
// Use os_flavor and os_version to construct a meaningful OS name
$osName = strtolower($serverInfo['image']['os_flavor']);
$displayName = ucfirst($serverInfo['image']['os_flavor']);
if (isset($serverInfo['image']['os_version']) && !empty($serverInfo['image']['os_version'])) {
$displayName .= ' ' . $serverInfo['image']['os_version'];
}
} else {
$osName = strtolower($serverInfo['image']['description'] ?? $osId);
$displayName = $serverInfo['image']['description'] ?? $osId;
}
// Find matching OS in available images (only for system images)
$currentOS = null;
if ($imageType === 'system') {
foreach ($operatingSystemsTemp['images'] as $os) {
if ($os['name'] === $osId) {
$currentOS = $os;
break;
}
}
}
if ($currentOS) {
$displayName = $currentOS['description'] ?? $currentOS['name'];
$osName = strtolower($displayName);
// Determine the proper group based on current OS name (same logic as above)
if (strpos($osName, 'alma') !== false) {
$group = 'almalinux';
} elseif (strpos($osName, 'rocky') !== false) {
$group = 'rocky';
} elseif (strpos($osName, 'centos') !== false) {
$group = 'centos';
} elseif (strpos($osName, 'debian') !== false) {
$group = 'debian';
} elseif (strpos($osName, 'ubuntu') !== false) {
$group = 'ubuntu';
} elseif (strpos($osName, 'fedora') !== false) {
$group = 'fedora';
} elseif (strpos($osName, 'opensuse') !== false || strpos($osName, 'suse') !== false) {
$group = 'opensuse';
} elseif (strpos($osName, 'windows') !== false) {
$group = 'windows';
} else {
$group = 'others';
}
// Map group names to correct image filenames
$imageFile = $group;
if ($group === 'rocky') {
$imageFile = 'rockylinux';
}
// Use the specific OS information
$serverInfo['operatingSystem'] = array(
'name' => $displayName,
'image' => isset($operatingSystems[$group]) ? $operatingSystems[$group]['image'] : 'data:image/png;base64,' . base64_encode(file_get_contents($dirOS . (in_array($imageFile, $availableOS) ? $imageFile : 'others') . '.png'))
);
} else {
// Fallback if OS not found - already have $displayName and $osName from above
// Just ensure they are set
if (!isset($displayName)) {
$displayName = $serverInfo['image']['description'] ?? $osId;
}
if (!isset($osName)) {
$osName = strtolower($displayName);
}
// Try to determine OS type from name
if (strpos($osName, 'alma') !== false) {
$imageFile = 'almalinux';
} elseif (strpos($osName, 'rocky') !== false) {
$imageFile = 'rockylinux';
} elseif (strpos($osName, 'centos') !== false) {
$imageFile = 'centos';
} elseif (strpos($osName, 'debian') !== false) {
$imageFile = 'debian';
} elseif (strpos($osName, 'ubuntu') !== false) {
$imageFile = 'ubuntu';
} elseif (strpos($osName, 'fedora') !== false) {
$imageFile = 'fedora';
} elseif (strpos($osName, 'opensuse') !== false || strpos($osName, 'suse') !== false) {
$imageFile = 'opensuse';
} elseif (strpos($osName, 'windows') !== false) {
$imageFile = 'windows';
} else {
$imageFile = 'others';
}
$serverInfo['operatingSystem'] = array(
'name' => $displayName,
'image' => 'data:image/png;base64,' . base64_encode(file_get_contents($dirOS . (in_array($imageFile, $availableOS) ? $imageFile : 'others') . '.png'))
);
}
}
}
// Set default OS info if not available
if (!isset($serverInfo['operatingSystem']) || !is_array($serverInfo['operatingSystem'])) {
$serverInfo['operatingSystem'] = array(
'name' => 'Unknown OS',
'image' => 'data:image/png;base64,' . base64_encode(file_get_contents($dirOS . 'others.png'))
);
}
// Map Hetzner status to template expectations
$serverInfo['statusImage'] = isset($images[$serverInfo['status']]) ? $images[$serverInfo['status']] : (isset($images['unknown']) ? $images['unknown'] : '');
$serverInfo['statusDescription'] = ucfirst($serverInfo['status']);
// Map Hetzner server data to expected format
$serverInfo['hostname'] = $serverInfo['name'] ?? 'N/A';
$serverInfo['ip'] = isset($serverInfo['public_net']['ipv4']['ip']) ? $serverInfo['public_net']['ipv4']['ip'] : 'N/A';
$serverInfo['ipv6'] = isset($serverInfo['public_net']['ipv6']['ip']) ? $serverInfo['public_net']['ipv6']['ip'] : 'N/A';
// Calculate uptime from created date
if (isset($serverInfo['created'])) {
$created = new DateTime($serverInfo['created']);
$now = new DateTime();
$diff = $now->diff($created);
$serverInfo['uptime_text'] = $diff->format('%a days, %h hours');
} else {
$serverInfo['uptime_text'] = 'N/A';
}
// Get server type details
$serverType = $serverInfo['server_type'] ?? array();
$serverInfo['cpu'] = $serverType['cores'] ?? 0;
$serverInfo['ram'] = $serverType['memory'] ?? 0;
$serverInfo['disk'] = $serverType['disk'] ?? 0;
// Try to fetch current metrics for the overview
try {
$metricsParams = $params;
$metricsParams['action'] = 'Graphs';
$metricsParams['time'] = 'hour'; // Get last hour for current usage
$metrics = ArkHostHetznerVPS_API($metricsParams);
// Extract latest CPU usage
if (isset($metrics['metrics']['time_series']['cpu']['values'])) {
$cpuValues = $metrics['metrics']['time_series']['cpu']['values'];
if (!empty($cpuValues)) {
$latestCpu = end($cpuValues);
$serverInfo['cpu_usage'] = round($latestCpu[1], 1);
}
}
// For bandwidth, we should use the outgoing_traffic from server info instead of calculating from metrics
// The metrics only show current bandwidth rate, not total usage
} catch (Exception $e) {
// If metrics fail, keep defaults
}
// Hetzner doesn't provide RAM usage or disk usage via API
$serverInfo['cpu_usage'] = $serverInfo['cpu_usage'] ?? 0;
$serverInfo['ram_usage'] = 0;
$serverInfo['disk_used'] = 0;
// Traffic information - Hetzner includes 20TB with all cloud servers
// Traffic usage is not available via the server API
$serverInfo['bandwidth'] = 20480; // 20TB in GB (20 * 1024)
$serverInfo['bandwidth_used'] = 0; // Not available via API
// Hetzner now returns location directly on the server. Retain a fallback
// for cached/older responses that still contain datacenter.location.
$locationInfo = array();
if (isset($serverInfo['location']) && is_array($serverInfo['location'])) {
$locationInfo = $serverInfo['location'];
} elseif (isset($serverInfo['datacenter']['location']) && is_array($serverInfo['datacenter']['location'])) {
$locationInfo = $serverInfo['datacenter']['location'];
}
$city = $locationInfo['city'] ?? '';
$country = $locationInfo['country'] ?? '';
if ($city && $country) {
$locationDisplay = $city . ', ' . $country;
} elseif ($city) {
$locationDisplay = $city;
} elseif ($country) {
$locationDisplay = $country;
} else {
$locationDisplay = 'N/A';
}
$serverInfo['location_display'] = $locationDisplay;
// Preserve the former display key for third-party/custom templates.
$serverInfo['datacenter'] = $locationDisplay;
// Get root password with expiration check (72 hours)
$passwordSetTime = $params['model']->serviceProperties->get('ArkHostHetznerVPS|Password Set Time');
$currentTime = time();
$expirationPeriod = 72 * 3600; // 72 hours in seconds
$hasPassword = !empty($params['password']) && $params['password'] !== 'managed-via-api';
// Backfill the timestamp for services created before expiry tracking existed.
// Without this, a missing timestamp meant the password was shown forever.
// The service registration date is the best estimate of when it was set.
if ($hasPassword && !$passwordSetTime) {
$regdate = Capsule::table('tblhosting')->where('id', $params['serviceid'])->value('regdate');
$passwordSetTime = ($regdate && $regdate !== '0000-00-00') ? strtotime($regdate) : $currentTime;
$params['model']->serviceProperties->save([
'ArkHostHetznerVPS|Password Set Time' => $passwordSetTime
]);
}
if ($hasPassword && ($currentTime - $passwordSetTime) < $expirationPeriod) {
// Password is still within the 72-hour window - WHMCS already decrypts it for us
$serverInfo['install_root'] = $params['password'];
} else {
// 72-hour window has elapsed: actually remove the install password from our
// systems so the stored copy matches what the client area promises.
if ($hasPassword) {
Capsule::table('tblhosting')->where('id', $params['serviceid'])->update(['password' => '']);
}
$serverInfo['install_root'] = '';
}
// Check if backups are enabled in module settings
$backupsEnabled = (ArkHostHetznerVPS_GetOption($params, 'backups') === 'on');
return array(
'templatefile' => 'template/clientarea_direct',
'templateVariables' => array(
'images' => $images,
'serverInfo' => $serverInfo,
'operatingSystems' => $operatingSystems,
'token' => generate_token('plain'),
'ADDONLANG' => ArkHostHetznerVPS_Translation(),
'backupsEnabled' => $backupsEnabled,
'productName' => $params['productname'] ?? $params['configoption1'] ?? 'VPS',
)
);
} catch (Exception $err) {
ArkHostHetznerVPS_Error(__FUNCTION__, $params, $err);
return array(
'templatefile' => 'template/error',
'templateVariables' => array(
'error' => $err->getMessage(),
'image' => 'data:image/png;base64,' . base64_encode(file_get_contents(__DIR__ . '/template/img/notice.png'))
)
);
}
}
function ArkHostHetznerVPS_Translation() {
$lang = Setting::getValue('Language');
$language = Lang::getName();
$_ADDONLANG = [];
if ($language === '') {
$language = $lang;
}
if ($language) {
$addonLangFile = ROOTDIR . '/modules/servers/ArkHostHetznerVPS/lang/' . $language . '.php';
if (file_exists($addonLangFile)) {
swapLang($language);
ob_start();
require $addonLangFile;
ob_end_clean();
}
}
if (count($_ADDONLANG) === 0) {
$addonLangFile = ROOTDIR . '/modules/servers/ArkHostHetznerVPS/lang/' . $lang . '.php';
if (file_exists($addonLangFile)) {
swapLang($lang);
ob_start();
require $addonLangFile;
ob_end_clean();
}
}
if (count($_ADDONLANG) === 0) {
$addonLangFile = ROOTDIR . '/modules/servers/ArkHostHetznerVPS/lang/english.php';
if (file_exists($addonLangFile)) {
ob_start();
require $addonLangFile;
ob_end_clean();
}
}
return $_ADDONLANG;
}
function ArkHostHetznerVPS_EnableRescue(array $params) {
try {
$params['action'] = 'Rescue Mode';
ArkHostHetznerVPS_API($params);
} catch (Exception $err) {
ArkHostHetznerVPS_Error(__FUNCTION__, $params, $err);
return 'Received the error: ' . $err->getMessage() . ' Check module debug log for more detailed error.';
}
return 'success';
}
function ArkHostHetznerVPS_ResetRoot(array $params) {
try {
$params['action'] = 'Reset root';
$result = ArkHostHetznerVPS_API($params);
// Store the new root password if provided
if (isset($result['root_password'])) {
Capsule::table('tblhosting')->where('id', $params['serviceid'])->update([
'password' => encrypt($result['root_password'])
]);
// Save the timestamp when password was set for expiration tracking (72 hours)
$params['model']->serviceProperties->save([
'ArkHostHetznerVPS|Password Set Time' => time()
]);
}
} catch (Exception $err) {
ArkHostHetznerVPS_Error(__FUNCTION__, $params, $err);
return 'Received the error: ' . $err->getMessage() . ' Check module debug log for more detailed error.';
}
return 'success';
}
function ArkHostHetznerVPS_CreateSnapshot(array $params) {
try {
$params['action'] = 'Create Snapshot';
$params['description'] = 'Manual snapshot from WHMCS';
ArkHostHetznerVPS_API($params);
} catch (Exception $err) {
ArkHostHetznerVPS_Error(__FUNCTION__, $params, $err);
return 'Received the error: ' . $err->getMessage() . ' Check module debug log for more detailed error.';
}
return 'success';
}
function ArkHostHetznerVPS_Shutdown(array $params) {
try {
$params['action'] = 'Shutdown';
ArkHostHetznerVPS_API($params);
} catch (Exception $err) {
ArkHostHetznerVPS_Error(__FUNCTION__, $params, $err);
return 'Received the error: ' . $err->getMessage() . ' Check module debug log for more detailed error.';
}
return 'success';
}
function ArkHostHetznerVPS_EnableBackups(array $params) {
try {
$params['action'] = 'Enable Backups';
ArkHostHetznerVPS_API($params);
} catch (Exception $err) {
ArkHostHetznerVPS_Error(__FUNCTION__, $params, $err);
return 'Received the error: ' . $err->getMessage() . ' Check module debug log for more detailed error.';
}
return 'success';
}
function ArkHostHetznerVPS_DisableBackups(array $params) {
try {
$params['action'] = 'Disable Backups';
ArkHostHetznerVPS_API($params);
} catch (Exception $err) {
ArkHostHetznerVPS_Error(__FUNCTION__, $params, $err);
return 'Received the error: ' . $err->getMessage() . ' Check module debug log for more detailed error.';
}
return 'success';
}