6 Commits
Author SHA1 Message Date
Yuri Karamian 7a19e78d51 update v1.2.5 2026-07-15 00:03:43 +02:00
Yuri Karamian fc2d2d4df6 docs: update README 2026-06-22 01:18:13 +02:00
Yuri Karamian 26bdabc534 v1.2.4: fix IP/username display and 72h password removal
- Store IP on the service at provisioning + backfill existing services
- Set service username to root to match the VPS login
- Actually delete the install password after the 72h window (was still
  visible via the eye icon, including legacy services without a timestamp)

Fixes #16
2026-06-22 01:13:55 +02:00
Yuri Karamian 815d1a442b v1.2.3 2026-05-14 22:57:14 +02:00
Yuri 372bff51eb lagom theme css fix 2025-11-13 16:17:16 +01:00
Yuri feb2c7c805 v1.2.1-fix 2025-11-13 13:38:15 +01:00
7 changed files with 402 additions and 130 deletions
+38 -27
View File
@@ -59,7 +59,7 @@ WHMCS server module for Hetzner Cloud VPS management.
- Server Type: `cx23`, `cx33`, `cx43`, `cx53` (Cost-Optimized), `cpx11`, `cpx21`, `cax11`, `ccx13`, etc. - Server Type: `cx23`, `cx33`, `cx43`, `cx53` (Cost-Optimized), `cpx11`, `cpx21`, `cax11`, `ccx13`, etc.
- Operating System: `ubuntu-22.04`, `debian-11`, `rocky-9`, etc. - Operating System: `ubuntu-22.04`, `debian-11`, `rocky-9`, etc.
- Datacenter: `fsn1`, `nbg1`, `hel1`, `ash`, `hil`, `sin` - Location: `fsn1`, `nbg1`, `hel1`, `ash`, `hil`, `sin`
- Backups: On/Off - Backups: On/Off
- Create Floating IP: On/Off - Create Floating IP: On/Off
- Cloud-Init YAML: Optional custom cloud-init configuration - Cloud-Init YAML: Optional custom cloud-init configuration
@@ -84,17 +84,19 @@ Create in Setup → Products/Services → Configurable Options:
- `No|None` - `No|None`
- `Yes|1 Floating IP` - `Yes|1 Floating IP`
**2\. Datacenter Selection** **2\. Location Selection**
- Option Name: `datacenter` - Option Name: `Location`
- Option Type: `Dropdown` - Option Type: `Dropdown`
- Options: - Options:
- `fsn1-dc14|Falkenstein, Germany` - `fsn1|Falkenstein, Germany`
- `nbg1-dc3|Nuremberg, Germany` - `nbg1|Nuremberg, Germany`
- `hel1-dc2|Helsinki, Finland` - `hel1|Helsinki, Finland`
- `ash-dc1|Ashburn, USA` - `ash|Ashburn, USA`
- `hil-dc1|Hillsboro, USA` - `hil|Hillsboro, USA`
- `sin-dc1|Singapore, Singapore` - `sin|Singapore, Singapore`
Existing configurable options named `datacenter` remain supported. Legacy values such as `fsn1-dc14` are automatically converted to their corresponding location (`fsn1`) when provisioning.
**3\. Server Type** **3\. Server Type**
@@ -306,33 +308,22 @@ runcmd:
Enable module debugging: Configuration → System Logs → Module Log Enable module debugging: Configuration → System Logs → Module Log
## Support Policy ## Support
This is free, open-source software provided AS-IS. This module is free and open source, and we maintain it in our spare time. We hope it makes running Hetzner Cloud on WHMCS a little easier for you.
**NO SUPPORT** is provided except through paid channels: If something isn't working as expected:
- Premium Support: €100/hour (2 hour minimum)
- Email: support@arkhost.com (paid support only)
### Before Requesting Support 1. Check the module log first (Configuration → System Logs → Module Log). It usually points straight to the cause.
- English only 2. Open an issue with what you saw, the steps that led to it, and the relevant log output. The more detail you share, the quicker we can help.
- Must provide complete error logs
- Must provide steps to reproduce
- Vague reports like "doesn't work" will be ignored
### GitHub Issues We can't promise a response time, but we read every report and genuinely appreciate bug reports, ideas, and pull requests. Contributions are always welcome.
- For documented bugs only
- Requires complete technical information
- Issues lacking proper information will be closed immediately
- This is not a support forum
If you need help with basic installation or configuration, purchase premium support.
## License ## License
MIT License - see [LICENSE](LICENSE) file for details. MIT License - see [LICENSE](LICENSE) file for details.
Free and open source. Commercial support available. Free and open source. Use it, fork it, improve it.
## Other ArkHost Modules ## Other ArkHost Modules
@@ -340,6 +331,26 @@ Check out our other WHMCS modules at [arkhost.com/whmcs-modules.php](https://ar
## Changelog ## Changelog
### v1.2.5
**Fixed:**
- Restored server provisioning after Hetzner removed the deprecated `datacenter` request property on July 1, 2026
- Replaced datacenter discovery with the Hetzner Locations API
- Updated client-area location display for the current server response format
- Added automatic conversion of legacy datacenter values such as `fsn1-dc14` to `fsn1`
### v1.2.4
**Fixed:**
- IP address now stored on the service at provisioning (and backfilled for existing services) so it displays in WHMCS instead of showing blank
- Service username now set to `root` to match the actual VPS login
- Install password now actually removed after the 72-hour window (was staying visible via the eye icon, including for older services that predated expiry tracking)
### v1.2.3
**Added:**
- VNC console key combination bar with Ctrl+Alt+Del, modifier keys (Shift, Alt, Ctrl), Tab, Enter, Del, Backspace, Esc, Shift+Up, and Fullscreen toggle
### v1.2.2
- WHMCS v9 compatibility: Updated Smarty template syntax for Smarty v4
### v1.2.1 ### v1.2.1
**Added:** **Added:**
- Shutdown button functionality (graceful server shutdown) - Shutdown button functionality (graceful server shutdown)
@@ -3,7 +3,7 @@
* WHMCS Server Module - Hetzner VPS * WHMCS Server Module - Hetzner VPS
* *
* @package WHMCS * @package WHMCS
* @version 1.2.1 * @version 1.2.5
* @copyright Copyright (c) ArkHost 2025 * @copyright Copyright (c) ArkHost 2025
* @author ArkHost <support@arkhost.com> * @author ArkHost <support@arkhost.com>
* @link https://arkhost.com * @link https://arkhost.com
@@ -104,6 +104,57 @@ function ArkHostHetznerVPS_GetConfigurableOption(array $params, $optionName) {
return null; return null;
} }
/**
* Convert a legacy Hetzner datacenter name (for example fsn1-dc14) to the
* corresponding location name (fsn1). Location names are normalized to
* lowercase, while numeric IDs remain unchanged.
*/
function ArkHostHetznerVPS_NormalizeLocation($location) {
if (!is_scalar($location)) {
return '';
}
$location = trim((string)$location);
if ($location === '') {
return '';
}
if (preg_match('/^([a-z0-9]+)-dc[0-9]+$/i', $location, $matches)) {
return strtolower($matches[1]);
}
return strtolower($location);
}
/**
* Resolve the server location while remaining compatible with configurable
* options named either "Location" or the former "Datacenter".
*/
function ArkHostHetznerVPS_GetLocationOption(array $params, $default = NULL) {
foreach (array('configoptions', 'customfields') as $collection) {
if (!isset($params[$collection]) || !is_array($params[$collection])) {
continue;
}
// Prefer the current option name if both current and legacy options
// happen to exist on the same product.
foreach (array('location', 'datacenter') as $optionName) {
foreach ($params[$collection] as $name => $value) {
$normalizedName = strtolower(trim((string)$name));
if ($normalizedName === $optionName && $value !== '') {
return ArkHostHetznerVPS_NormalizeLocation($value);
}
}
}
}
// Keep using the original module setting key so existing WHMCS products
// retain their configoption3 value after upgrading.
return ArkHostHetznerVPS_NormalizeLocation(
ArkHostHetznerVPS_GetOption($params, 'datacenter', $default)
);
}
function ArkHostHetznerVPS_API(array $params) { function ArkHostHetznerVPS_API(array $params) {
$url = 'https://api.hetzner.cloud/v1/'; $url = 'https://api.hetzner.cloud/v1/';
$data = []; $data = [];
@@ -125,8 +176,8 @@ function ArkHostHetznerVPS_API(array $params) {
$method = 'GET'; $method = 'GET';
break; break;
case 'Datacenters': case 'Locations':
$url .= 'datacenters'; $url .= 'locations?per_page=50';
$method = 'GET'; $method = 'GET';
break; break;
@@ -160,10 +211,11 @@ function ArkHostHetznerVPS_API(array $params) {
) )
); );
// Add datacenter if specified // Hetzner removed the datacenter request property on 1 July 2026.
$datacenter = ArkHostHetznerVPS_GetOption($params, 'datacenter'); // Normalize legacy values such as fsn1-dc14 before sending location.
if ($datacenter) { $location = ArkHostHetznerVPS_GetLocationOption($params);
$data['datacenter'] = $datacenter; if ($location) {
$data['location'] = $location;
} }
// Handle backups // Handle backups
@@ -345,11 +397,11 @@ function ArkHostHetznerVPS_API(array $params) {
break; break;
case 'Create backup': case 'Create backup':
// Use snapshots // Create manual backup (still type=backup, but with distinct description)
$url .= 'servers/' . ArkHostHetznerVPS_GetVPSID($params) . '/actions/create_image'; $url .= 'servers/' . ArkHostHetznerVPS_GetVPSID($params) . '/actions/create_image';
$method = 'POST'; $method = 'POST';
$data = array( $data = array(
'description' => 'Backup created on ' . date('Y-m-d H:i:s'), 'description' => 'Manual backup - ' . date('Y-m-d H:i:s'),
'type' => 'backup' 'type' => 'backup'
); );
break; break;
@@ -975,9 +1027,11 @@ function ArkHostHetznerVPS_ConfigOptions() {
'Type' => 'dropdown', 'Type' => 'dropdown',
'Options' => array(), 'Options' => array(),
), ),
// Keep the original array key and position for existing WHMCS product
// settings. Only the Hetzner-facing value and admin label have changed.
'datacenter' => array( 'datacenter' => array(
'FriendlyName' => 'Datacenter', 'FriendlyName' => 'Location',
'Description' => 'The datacenter location for the server.', 'Description' => 'The Hetzner location for the server (Configurable option: Location or legacy datacenter).',
'Type' => 'dropdown', 'Type' => 'dropdown',
'Options' => array(), 'Options' => array(),
), ),
@@ -993,7 +1047,7 @@ function ArkHostHetznerVPS_ConfigOptions() {
), ),
'floating_ip_location' => array( 'floating_ip_location' => array(
'FriendlyName' => 'Floating IP Location', 'FriendlyName' => 'Floating IP Location',
'Description' => 'Location for floating IPs (same as datacenter)', 'Description' => 'Location for floating IPs (normally the same as the server location).',
'Type' => 'dropdown', 'Type' => 'dropdown',
'Options' => array(), 'Options' => array(),
), ),
@@ -1082,19 +1136,31 @@ function ArkHostHetznerVPS_ConfigOptions() {
} }
} }
// Fetch datacenters // Fetch locations. Hetzner's datacenter endpoints are deprecated
$params['action'] = 'Datacenters'; // and are scheduled for removal after 1 October 2026.
$datacenters = ArkHostHetznerVPS_API($params); $params['action'] = 'Locations';
$locations = ArkHostHetznerVPS_API($params);
if (isset($datacenters['datacenters'])) { if (isset($locations['locations'])) {
foreach ($datacenters['datacenters'] as $datacenter) { foreach ($locations['locations'] as $location) {
$array['datacenter']['Options'] += array( if (empty($location['name'])) {
$datacenter['name'] => $datacenter['description'] . ' (' . $datacenter['location']['city'] . ')' continue;
); }
// Also populate floating IP locations
$array['floating_ip_location']['Options'] += array( $place = array_filter(array(
$datacenter['location']['name'] => $datacenter['location']['city'] . ', ' . $datacenter['location']['country'] $location['city'] ?? '',
); $location['country'] ?? ''
));
$description = !empty($location['description'])
? $location['description']
: $location['name'];
$label = $description;
if (!empty($place)) {
$label .= ' (' . implode(', ', $place) . ')';
}
$array['datacenter']['Options'][$location['name']] = $label;
$array['floating_ip_location']['Options'][$location['name']] = $label;
} }
} }
} }
@@ -1176,6 +1242,23 @@ function ArkHostHetznerVPS_CreateAccount(array $params) {
'ArkHostHetznerVPS|VPS ID' => $create['server']['id'], 'ArkHostHetznerVPS|VPS ID' => $create['server']['id'],
]); ]);
// Store the primary IP address and login username so they display in WHMCS.
// Without this, the admin service page shows a blank Dedicated IP and the
// auto-generated order username instead of the real VPS login ("root").
$primaryIp = '';
if (!empty($create['server']['public_net']['ipv4']['ip'])) {
$primaryIp = $create['server']['public_net']['ipv4']['ip'];
} elseif (!empty($create['server']['public_net']['ipv6']['ip'])) {
// IPv6-only servers report the network as <addr>/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 // Store the root password if provided
if (isset($create['root_password'])) { if (isset($create['root_password'])) {
Capsule::table('tblhosting')->where('id', $params['serviceid'])->update([ Capsule::table('tblhosting')->where('id', $params['serviceid'])->update([
@@ -1201,7 +1284,9 @@ function ArkHostHetznerVPS_CreateAccount(array $params) {
$floatingIpParams = $params; $floatingIpParams = $params;
$floatingIpParams['action'] = 'Create Floating IP'; $floatingIpParams['action'] = 'Create Floating IP';
$floatingIpParams['ip_type'] = $ipType; $floatingIpParams['ip_type'] = $ipType;
$floatingIpParams['location'] = ArkHostHetznerVPS_GetOption($params, 'floating_ip_location'); $floatingIpParams['location'] = ArkHostHetznerVPS_NormalizeLocation(
ArkHostHetznerVPS_GetOption($params, 'floating_ip_location')
);
$floatingIpParams['description'] = 'Server ID: ' . $create['server']['id']; $floatingIpParams['description'] = 'Server ID: ' . $create['server']['id'];
$floatingIpParams['assign_to_server'] = true; $floatingIpParams['assign_to_server'] = true;
@@ -1353,6 +1438,10 @@ function ArkHostHetznerVPS_VNC(array $params) {
<style> <style>
body { margin: 0; background-color: #000; height: 100vh; display: flex; flex-direction: column; font-family: Arial, sans-serif; } body { margin: 0; background-color: #000; height: 100vh; display: flex; flex-direction: column; font-family: Arial, sans-serif; }
#top_bar { background-color: #333; color: white; padding: 10px; text-align: center; font-size: 14px; } #top_bar { background-color: #333; color: white; padding: 10px; text-align: center; font-size: 14px; }
#key_bar { background-color: #2a2a2a; padding: 8px 10px; text-align: center; display: flex; gap: 6px; justify-content: center; flex-wrap: wrap; }
#key_bar button { background-color: #555; color: white; border: 1px solid #777; padding: 5px 12px; border-radius: 4px; cursor: pointer; font-size: 12px; font-family: Arial, sans-serif; }
#key_bar button:hover { background-color: #777; }
#key_bar button:active { background-color: #999; }
#status { text-align: center; padding: 10px; background: #555; color: white; } #status { text-align: center; padding: 10px; background: #555; color: white; }
#screen { flex: 1; display: flex; width: 100%; height: 100%; background: #000; position: relative; } #screen { flex: 1; display: flex; width: 100%; height: 100%; background: #000; position: relative; }
#screen canvas { margin: auto; } #screen canvas { margin: auto; }
@@ -1369,6 +1458,19 @@ function ArkHostHetznerVPS_VNC(array $params) {
Password: <span style="font-family: monospace; background: #444; padding: 2px 6px; border-radius: 3px;">' . htmlspecialchars($vncPassword) . '</span> Password: <span style="font-family: monospace; background: #444; padding: 2px 6px; border-radius: 3px;">' . htmlspecialchars($vncPassword) . '</span>
</div> </div>
</div> </div>
<div id="key_bar">
<button onclick="sendCtrlAltDel()">Ctrl + Alt + Del</button>
<button onclick="sendKey(0xFFE3, \'ShiftLeft\')">Shift</button>
<button onclick="sendKey(0xFFE9, \'AltLeft\')">Alt</button>
<button onclick="sendKey(0xFFE1, \'ControlLeft\')">Ctrl</button>
<button onclick="sendKey(0xFF09, \'Tab\')">Tab</button>
<button onclick="sendKey(0xFF0D, \'Enter\')">Enter</button>
<button onclick="sendKey(0xFFFF, \'Delete\')">Del</button>
<button onclick="sendKey(0xFF08, \'Backspace\')">Backspace</button>
<button onclick="sendKey(0xFF1B, \'Escape\')">Esc</button>
<button onclick="sendKey(0xFFE3, \'ShiftLeft\'); sendKey(0xFF52, \'ArrowUp\'); sendKey(0xFFE3, \'ShiftLeft\', false);">Shift+Up</button>
<button onclick="toggleFullscreen()">Fullscreen</button>
</div>
<div id="status">Initializing VNC Client...</div> <div id="status">Initializing VNC Client...</div>
<div id="screen"></div> <div id="screen"></div>
@@ -1393,6 +1495,33 @@ function ArkHostHetznerVPS_VNC(array $params) {
} }
} }
window.sendCtrlAltDel = function sendCtrlAltDel() {
if (rfb) {
rfb.sendCtrlAltDel();
rfb.focus();
}
};
window.sendKey = function sendKey(keysym, code, down) {
if (rfb) {
if (down === undefined) {
rfb.sendKey(keysym, code, true);
rfb.sendKey(keysym, code, false);
} else {
rfb.sendKey(keysym, code, down);
}
rfb.focus();
}
};
window.toggleFullscreen = function toggleFullscreen() {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen();
} else {
document.exitFullscreen();
}
};
function connect() { function connect() {
updateStatus("Connecting to VNC server...", "warning"); updateStatus("Connecting to VNC server...", "warning");
@@ -1515,7 +1644,30 @@ function ArkHostHetznerVPS_AdminLink(array $params) {
if (isset($serverInfo['server'])) { if (isset($serverInfo['server'])) {
$server = $serverInfo['server']; $server = $serverInfo['server'];
return '<i class="fa fa-server"></i> Status: ' . $server['status'] . '<br><i class="fa fa-network-wired"></i> IP: ' . $server['public_net']['ipv4']['ip'];
// 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 '<i class="fa fa-server"></i> Status: ' . $server['status'] . '<br><i class="fa fa-network-wired"></i> IP: ' . ($primaryIp !== '' ? $primaryIp : 'N/A');
} }
return 'Server ID: ' . $vpsId; return 'Server ID: ' . $vpsId;
@@ -1540,35 +1692,8 @@ function ArkHostHetznerVPS_ClientAreaAPI(array $params) {
// Check backup permissions // Check backup permissions
$backupActions = array('Create backup', 'Delete backup', 'Restore backup'); $backupActions = array('Create backup', 'Delete backup', 'Restore backup');
if (in_array($action, $backupActions)) { if (in_array($action, $backupActions)) {
// Check if backups are enabled either via module settings or configurable options // Check if backups are enabled in module settings
$backupsEnabled = false; $backupsEnabled = (ArkHostHetznerVPS_GetOption($params, 'backups') === 'on');
// Check module product settings
if (isset($params['backups']) && $params['backups'] == 'on') {
$backupsEnabled = true;
}
// Check configurable options
if (isset($params['configoptions']['Backups']) && $params['configoptions']['Backups'] == 'Yes') {
$backupsEnabled = true;
}
// If not enabled in WHMCS settings, check if backups are actually enabled on Hetzner's side
if (!$backupsEnabled) {
try {
$serverInfoParams = $params;
$serverInfoParams['action'] = 'Server Info';
$serverInfoResult = ArkHostHetznerVPS_API($serverInfoParams);
// Check if the server has backups enabled (indicated by backup_window being set)
if (isset($serverInfoResult['server']['backup_window']) && $serverInfoResult['server']['backup_window'] !== null) {
$backupsEnabled = true;
}
} catch (Exception $e) {
// If we can't check, assume not enabled
$backupsEnabled = false;
}
}
if (!$backupsEnabled) { if (!$backupsEnabled) {
return array('jsonResponse' => array( return array('jsonResponse' => array(
@@ -1634,8 +1759,9 @@ function ArkHostHetznerVPS_ClientAreaAPI(array $params) {
$createParams = $params; $createParams = $params;
$createParams['action'] = 'Create Floating IP'; $createParams['action'] = 'Create Floating IP';
$createParams['ip_type'] = 'ipv4'; $createParams['ip_type'] = 'ipv4';
$createParams['location'] = ArkHostHetznerVPS_GetOption($params, 'floating_ip_location') ?: $createParams['location'] = ArkHostHetznerVPS_NormalizeLocation(
ArkHostHetznerVPS_GetOption($params, 'datacenter'); ArkHostHetznerVPS_GetOption($params, 'floating_ip_location')
) ?: ArkHostHetznerVPS_GetLocationOption($params);
$createParams['description'] = 'WHMCS Service ID: ' . $params['serviceid']; $createParams['description'] = 'WHMCS Service ID: ' . $params['serviceid'];
$createParams['assign_to_server'] = true; $createParams['assign_to_server'] = true;
@@ -1805,7 +1931,8 @@ function ArkHostHetznerVPS_ClientAreaAPI(array $params) {
'name' => $image['description'] ?? $image['name'], 'name' => $image['description'] ?? $image['name'],
'created' => $image['created'], 'created' => $image['created'],
'size' => isset($image['image_size']) ? round($image['image_size'], 2) : 0, // Already in GB from API 'size' => isset($image['image_size']) ? round($image['image_size'], 2) : 0, // Already in GB from API
'type' => $image['type'] 'type' => $image['type'],
'status' => $image['status'] ?? 'available' // Include status from Hetzner API
); );
$backupIndex++; $backupIndex++;
} }
@@ -2260,14 +2387,29 @@ function ArkHostHetznerVPS_ClientArea(array $params) {
// Process operating system info for Hetzner // Process operating system info for Hetzner
if (isset($serverInfo['image']) && !empty($operatingSystemsTemp['images'])) { if (isset($serverInfo['image']) && !empty($operatingSystemsTemp['images'])) {
$osId = $serverInfo['image']['name']; $osId = $serverInfo['image']['name'];
$osName = strtolower($serverInfo['image']['description'] ?? $osId); $imageType = $serverInfo['image']['type'] ?? 'system';
// Find matching OS in available images // 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; $currentOS = null;
foreach ($operatingSystemsTemp['images'] as $os) { if ($imageType === 'system') {
if ($os['name'] === $osId) { foreach ($operatingSystemsTemp['images'] as $os) {
$currentOS = $os; if ($os['name'] === $osId) {
break; $currentOS = $os;
break;
}
} }
} }
@@ -2308,9 +2450,14 @@ function ArkHostHetznerVPS_ClientArea(array $params) {
'image' => isset($operatingSystems[$group]) ? $operatingSystems[$group]['image'] : 'data:image/png;base64,' . base64_encode(file_get_contents($dirOS . (in_array($imageFile, $availableOS) ? $imageFile : 'others') . '.png')) '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 { } else {
// Fallback if OS not found - use image info from server // Fallback if OS not found - already have $displayName and $osName from above
$displayName = $serverInfo['image']['description'] ?? $osId; // Just ensure they are set
$osName = strtolower($displayName); if (!isset($displayName)) {
$displayName = $serverInfo['image']['description'] ?? $osId;
}
if (!isset($osName)) {
$osName = strtolower($displayName);
}
// Try to determine OS type from name // Try to determine OS type from name
if (strpos($osName, 'alma') !== false) { if (strpos($osName, 'alma') !== false) {
@@ -2407,32 +2554,64 @@ function ArkHostHetznerVPS_ClientArea(array $params) {
$serverInfo['bandwidth'] = 20480; // 20TB in GB (20 * 1024) $serverInfo['bandwidth'] = 20480; // 20TB in GB (20 * 1024)
$serverInfo['bandwidth_used'] = 0; // Not available via API $serverInfo['bandwidth_used'] = 0; // Not available via API
// Add datacenter info // Hetzner now returns location directly on the server. Retain a fallback
$serverInfo['datacenter'] = isset($serverInfo['datacenter']['description']) ? $serverInfo['datacenter']['description'] : 'N/A'; // for cached/older responses that still contain datacenter.location.
$serverInfo['location'] = isset($serverInfo['datacenter']['location']['city']) ? $serverInfo['datacenter']['location']['city'] : 'N/A'; $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) // Get root password with expiration check (72 hours)
$passwordSetTime = $params['model']->serviceProperties->get('ArkHostHetznerVPS|Password Set Time'); $passwordSetTime = $params['model']->serviceProperties->get('ArkHostHetznerVPS|Password Set Time');
$currentTime = time(); $currentTime = time();
$expirationPeriod = 72 * 3600; // 72 hours in seconds $expirationPeriod = 72 * 3600; // 72 hours in seconds
if (!empty($params['password']) && $params['password'] !== 'managed-via-api') { $hasPassword = !empty($params['password']) && $params['password'] !== 'managed-via-api';
// If no timestamp exists, show the password (backward compatibility or initial setup)
// If timestamp exists, check if it's within the 72-hour window // Backfill the timestamp for services created before expiry tracking existed.
if (!$passwordSetTime || ($currentTime - $passwordSetTime) < $expirationPeriod) { // Without this, a missing timestamp meant the password was shown forever.
// Password is still valid - WHMCS already decrypts it for us in $params // The service registration date is the best estimate of when it was set.
// No need to call decrypt() as $params['password'] is already plain text if ($hasPassword && !$passwordSetTime) {
$serverInfo['install_root'] = $params['password']; $regdate = Capsule::table('tblhosting')->where('id', $params['serviceid'])->value('regdate');
} else { $passwordSetTime = ($regdate && $regdate !== '0000-00-00') ? strtotime($regdate) : $currentTime;
// Password has expired (timestamp exists and is older than 72 hours) $params['model']->serviceProperties->save([
$serverInfo['install_root'] = ''; '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 { } 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'] = ''; $serverInfo['install_root'] = '';
} }
// For now, always show backups tab - the API will handle permissions // Check if backups are enabled in module settings
$backupsEnabled = true; $backupsEnabled = (ArkHostHetznerVPS_GetOption($params, 'backups') === 'on');
return array( return array(
'templatefile' => 'template/clientarea_direct', 'templatefile' => 'template/clientarea_direct',
@@ -2443,6 +2622,7 @@ function ArkHostHetznerVPS_ClientArea(array $params) {
'token' => generate_token('plain'), 'token' => generate_token('plain'),
'ADDONLANG' => ArkHostHetznerVPS_Translation(), 'ADDONLANG' => ArkHostHetznerVPS_Translation(),
'backupsEnabled' => $backupsEnabled, 'backupsEnabled' => $backupsEnabled,
'productName' => $params['productname'] ?? $params['configoption1'] ?? 'VPS',
) )
); );
} catch (Exception $err) { } catch (Exception $err) {
+1 -2
View File
@@ -3,7 +3,7 @@
* WHMCS Server Module - Hetzner VPS Hooks * WHMCS Server Module - Hetzner VPS Hooks
* *
* @package WHMCS * @package WHMCS
* @version 1.2.1 * @version 1.2.5
* @copyright Copyright (c) ArkHost 2025 * @copyright Copyright (c) ArkHost 2025
* @author ArkHost <support@arkhost.com> * @author ArkHost <support@arkhost.com>
* @link https://arkhost.com * @link https://arkhost.com
@@ -70,4 +70,3 @@ add_hook('ClientAreaHeadOutput', 1, function($vars) {
</style> </style>
HTML; HTML;
}); });
@@ -69,7 +69,7 @@ $_ADDONLANG['Overview']['ResourceAllocation'] = 'Resource Allocation';
$_ADDONLANG['Overview']['QuickActions'] = 'Quick Actions'; $_ADDONLANG['Overview']['QuickActions'] = 'Quick Actions';
$_ADDONLANG['Overview']['Hostname'] = 'Hostname'; $_ADDONLANG['Overview']['Hostname'] = 'Hostname';
$_ADDONLANG['Overview']['Status'] = 'Status'; $_ADDONLANG['Overview']['Status'] = 'Status';
$_ADDONLANG['Overview']['OS'] = 'Operating System'; $_ADDONLANG['Overview']['OS'] = 'OS';
$_ADDONLANG['Overview']['Location'] = 'Location'; $_ADDONLANG['Overview']['Location'] = 'Location';
$_ADDONLANG['Overview']['CPU_Cores'] = 'vCPU Cores'; $_ADDONLANG['Overview']['CPU_Cores'] = 'vCPU Cores';
$_ADDONLANG['Overview']['Memory'] = 'Memory'; $_ADDONLANG['Overview']['Memory'] = 'Memory';
@@ -143,7 +143,7 @@ $_ADDONLANG['Settings']['Password']['Submit'] = 'Reset Password';
### Reinstall ### Reinstall
$_ADDONLANG['Settings']['Reinstall']['Title'] = 'Reinstall'; $_ADDONLANG['Settings']['Reinstall']['Title'] = 'Reinstall';
$_ADDONLANG['Settings']['Reinstall']['Description'] = 'Please understand that by reinstalling, all the data will be wiped from the server. This action is irreversible!'; $_ADDONLANG['Settings']['Reinstall']['Description'] = 'Please understand that by reinstalling, all the data will be wiped from the server. This action is irreversible!';
$_ADDONLANG['Settings']['Reinstall']['OS'] = 'Operating System'; $_ADDONLANG['Settings']['Reinstall']['OS'] = 'OS';
$_ADDONLANG['Settings']['Reinstall']['Version'] = 'CHOOSE VERSION'; $_ADDONLANG['Settings']['Reinstall']['Version'] = 'CHOOSE VERSION';
$_ADDONLANG['Settings']['Reinstall']['Submit'] = 'Reinstall'; $_ADDONLANG['Settings']['Reinstall']['Submit'] = 'Reinstall';
@@ -13,7 +13,7 @@
<script type="text/javascript"> <script type="text/javascript">
// Initialize template variables for external script // Initialize template variables for external script
var productURL = '{$WEB_ROOT}/clientarea.php?action=productdetails&id={$serviceid}'; var productURL = '{$WEB_ROOT}/clientarea.php?action=productdetails&id={$serviceid}';
var serverInfoInitial = JSON.parse('{$serverInfo|@json_encode}'); var serverInfoInitial = JSON.parse('{$serverInfo|json_encode}');
var serviceId = '{$serviceid}'; var serviceId = '{$serviceid}';
var csrfToken = '{$token}'; var csrfToken = '{$token}';
var webRoot = '{$WEB_ROOT}'; var webRoot = '{$WEB_ROOT}';
@@ -207,11 +207,11 @@
</tr> </tr>
<tr> <tr>
<td class="text-muted">{$ADDONLANG.Overview.Location}:</td> <td class="text-muted">{$ADDONLANG.Overview.Location}:</td>
<td><i class="fa fa-map-marker-alt mr-1"></i>{$serverInfo['datacenter']}</td> <td><i class="fa fa-map-marker-alt mr-1"></i>{$serverInfo['location_display']}</td>
</tr> </tr>
<tr> <tr>
<td class="text-muted">{$ADDONLANG.Overview.ServerType}:</td> <td class="text-muted">{$ADDONLANG.Overview.ServerType}:</td>
<td><strong>{$serverInfo['server_type']['description']|default:$ADDONLANG.ServerTypes.Standard}</strong></td> <td><strong>{$productName}</strong></td>
</tr> </tr>
</table> </table>
</div> </div>
@@ -521,8 +521,10 @@ function updateBackupTable(data) {
backupDate = backupDate.split('T')[0]; backupDate = backupDate.split('T')[0];
} }
// Determine backup type // Determine backup type based on description
var backupType = backup.type === 'backup' ? lang.backups.automatic : lang.backups.manual; // Manual backups have descriptions starting with "Manual backup -"
var backupName = backup.name || '';
var backupType = backupName.indexOf('Manual backup -') === 0 ? lang.backups.manual : lang.backups.automatic;
// Format size (already in GB from API) // Format size (already in GB from API)
var sizeStr = 'N/A'; var sizeStr = 'N/A';
@@ -530,22 +532,40 @@ function updateBackupTable(data) {
sizeStr = backup.size + ' ' + lang.general.gb; sizeStr = backup.size + ' ' + lang.general.gb;
} }
// Status is always available for Hetzner backups // Determine status badge based on size first (more reliable than API status)
var statusBadge = '<span class="badge badge-success">' + lang.backups.available + '</span>'; var statusBadge;
if (!backup.size || backup.size === 0 || backup.size === '0') {
// Backup is still being created (size not available yet)
statusBadge = '<span class="badge badge-warning">' + lang.backups.creating + '</span>';
} else if (backup.status === 'available' || backup.size > 0) {
// Backup is ready (has a size)
statusBadge = '<span class="badge badge-success">' + lang.backups.available + '</span>';
} else if (backup.status === 'creating') {
// Status says creating but has size (unlikely but possible)
statusBadge = '<span class="badge badge-warning">' + lang.backups.creating + '</span>';
} else {
// Other statuses (error, etc)
statusBadge = '<span class="badge badge-danger">' + (backup.status || lang.backups.error) + '</span>';
}
// Use backup ID for Hetzner // Use backup ID for Hetzner
var backupId = backup.id || backup.file; var backupId = backup.id || backup.file;
// Disable buttons if backup is still being created (size not available)
var isCreating = (!backup.size || backup.size === 0 || backup.size === '0');
var disabledAttr = isCreating ? ' disabled' : '';
var disabledClass = isCreating ? ' disabled' : '';
row.innerHTML = row.innerHTML =
'<td>' + backupDate + '</td>' + '<td>' + backupDate + '</td>' +
'<td>' + sizeStr + '</td>' + '<td>' + sizeStr + '</td>' +
'<td>' + backupType + '</td>' + '<td>' + backupType + '</td>' +
'<td>' + statusBadge + '</td>' + '<td>' + statusBadge + '</td>' +
'<td style="white-space: nowrap;">' + '<td style="white-space: nowrap;">' +
'<button class="btn btn-xs btn-primary mr-1" style="padding: 4px 8px; font-size: 12px;" onclick="restoreBackup(\'' + backupId + '\'); return false;" title="' + lang.restore + '">' + '<button class="btn btn-xs btn-primary mr-1' + disabledClass + '" style="padding: 4px 8px; font-size: 12px;" onclick="restoreBackup(\'' + backupId + '\'); return false;" title="' + lang.restore + '"' + disabledAttr + '>' +
'<i class="fa fa-undo"></i>' + '<i class="fa fa-undo"></i>' +
'</button>' + '</button>' +
'<button class="btn btn-xs btn-danger" style="padding: 4px 8px; font-size: 12px;" onclick="deleteBackup(\'' + backupId + '\'); return false;" title="' + lang.delete + '">' + '<button class="btn btn-xs btn-danger' + disabledClass + '" style="padding: 4px 8px; font-size: 12px;" onclick="deleteBackup(\'' + backupId + '\'); return false;" title="' + lang.delete + '"' + disabledAttr + '>' +
'<i class="fa fa-trash"></i>' + '<i class="fa fa-trash"></i>' +
'</button>' + '</button>' +
'</td>'; '</td>';
@@ -68,6 +68,68 @@
font-weight: 600; font-weight: 600;
transition: all 0.3s ease; transition: all 0.3s ease;
} }
.arkhost-vps-container .btn-danger,
.arkhost-vps-container button.btn-danger,
.arkhost-vps-container .btn.btn-danger,
.arkhost-vps-container button.btn.btn-danger {
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%) !important;
background-color: #dc3545 !important;
color: white !important;
border: none !important;
border-color: #dc3545 !important;
padding: 10px 20px !important;
border-radius: 12px !important;
font-weight: 600 !important;
transition: all 0.3s ease !important;
box-shadow: 0 2px 8px rgba(220, 53, 69, 0.3) !important;
opacity: 1 !important;
}
.arkhost-vps-container .btn-danger:hover,
.arkhost-vps-container button.btn-danger:hover,
.arkhost-vps-container .btn.btn-danger:hover,
.arkhost-vps-container button.btn.btn-danger:hover,
.arkhost-vps-container .btn-danger:focus,
.arkhost-vps-container button.btn-danger:focus {
background: linear-gradient(135deg, #c82333 0%, #bd2130 100%) !important;
background-color: #c82333 !important;
color: white !important;
border-color: #c82333 !important;
box-shadow: 0 4px 12px rgba(220, 53, 69, 0.5) !important;
transform: translateY(-1px) !important;
opacity: 1 !important;
}
.arkhost-vps-container .btn-danger:active,
.arkhost-vps-container button.btn-danger:active {
background: #bd2130 !important;
background-color: #bd2130 !important;
border-color: #bd2130 !important;
color: white !important;
}
/* Extra specific selectors for theme compatibility (Lagom, etc) */
.arkhost-vps-container .card .btn-danger,
.arkhost-vps-container .card-body .btn-danger,
.arkhost-vps-container .tab-pane .btn-danger,
#ArkHostVPS .btn-danger,
#ArkHostVPS button.btn-danger {
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%) !important;
background-color: #dc3545 !important;
color: #ffffff !important;
border-color: #dc3545 !important;
opacity: 1 !important;
}
.arkhost-vps-container .card .btn-danger:hover,
.arkhost-vps-container .card-body .btn-danger:hover,
.arkhost-vps-container .tab-pane .btn-danger:hover,
#ArkHostVPS .btn-danger:hover,
#ArkHostVPS button.btn-danger:hover {
background: linear-gradient(135deg, #c82333 0%, #bd2130 100%) !important;
background-color: #c82333 !important;
color: #ffffff !important;
border-color: #c82333 !important;
opacity: 1 !important;
}
.arkhost-vps-container .head { .arkhost-vps-container .head {
border-bottom: 2px solid #3c7fb4; border-bottom: 2px solid #3c7fb4;
margin-bottom: 20px; margin-bottom: 20px;