mirror of
https://gitlab.com/ArkHost/WHMCS-ArkHost-HetznerVPS.git
synced 2026-07-24 15:55:53 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a19e78d51 | ||
|
|
fc2d2d4df6 | ||
|
|
26bdabc534 | ||
|
|
815d1a442b | ||
|
|
372bff51eb | ||
|
|
feb2c7c805 |
@@ -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.
|
||||
- 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
|
||||
- Create Floating IP: On/Off
|
||||
- Cloud-Init YAML: Optional custom cloud-init configuration
|
||||
@@ -84,17 +84,19 @@ Create in Setup → Products/Services → Configurable Options:
|
||||
- `No|None`
|
||||
- `Yes|1 Floating IP`
|
||||
|
||||
**2\. Datacenter Selection**
|
||||
**2\. Location Selection**
|
||||
|
||||
- Option Name: `datacenter`
|
||||
- Option Name: `Location`
|
||||
- Option Type: `Dropdown`
|
||||
- Options:
|
||||
- `fsn1-dc14|Falkenstein, Germany`
|
||||
- `nbg1-dc3|Nuremberg, Germany`
|
||||
- `hel1-dc2|Helsinki, Finland`
|
||||
- `ash-dc1|Ashburn, USA`
|
||||
- `hil-dc1|Hillsboro, USA`
|
||||
- `sin-dc1|Singapore, Singapore`
|
||||
- `fsn1|Falkenstein, Germany`
|
||||
- `nbg1|Nuremberg, Germany`
|
||||
- `hel1|Helsinki, Finland`
|
||||
- `ash|Ashburn, USA`
|
||||
- `hil|Hillsboro, USA`
|
||||
- `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**
|
||||
|
||||
@@ -306,33 +308,22 @@ runcmd:
|
||||
|
||||
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:
|
||||
- Premium Support: €100/hour (2 hour minimum)
|
||||
- Email: support@arkhost.com (paid support only)
|
||||
If something isn't working as expected:
|
||||
|
||||
### Before Requesting Support
|
||||
- English only
|
||||
- Must provide complete error logs
|
||||
- Must provide steps to reproduce
|
||||
- Vague reports like "doesn't work" will be ignored
|
||||
1. Check the module log first (Configuration → System Logs → Module Log). It usually points straight to the cause.
|
||||
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.
|
||||
|
||||
### GitHub Issues
|
||||
- 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.
|
||||
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.
|
||||
|
||||
## License
|
||||
|
||||
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
|
||||
|
||||
@@ -340,6 +331,26 @@ Check out our other WHMCS modules at [arkhost.com/whmcs-modules.php](https://ar
|
||||
|
||||
## 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
|
||||
**Added:**
|
||||
- Shutdown button functionality (graceful server shutdown)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* WHMCS Server Module - Hetzner VPS
|
||||
*
|
||||
* @package WHMCS
|
||||
* @version 1.2.1
|
||||
* @version 1.2.5
|
||||
* @copyright Copyright (c) ArkHost 2025
|
||||
* @author ArkHost <support@arkhost.com>
|
||||
* @link https://arkhost.com
|
||||
@@ -104,6 +104,57 @@ function ArkHostHetznerVPS_GetConfigurableOption(array $params, $optionName) {
|
||||
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) {
|
||||
$url = 'https://api.hetzner.cloud/v1/';
|
||||
$data = [];
|
||||
@@ -125,8 +176,8 @@ function ArkHostHetznerVPS_API(array $params) {
|
||||
$method = 'GET';
|
||||
break;
|
||||
|
||||
case 'Datacenters':
|
||||
$url .= 'datacenters';
|
||||
case 'Locations':
|
||||
$url .= 'locations?per_page=50';
|
||||
$method = 'GET';
|
||||
break;
|
||||
|
||||
@@ -160,10 +211,11 @@ function ArkHostHetznerVPS_API(array $params) {
|
||||
)
|
||||
);
|
||||
|
||||
// Add datacenter if specified
|
||||
$datacenter = ArkHostHetznerVPS_GetOption($params, 'datacenter');
|
||||
if ($datacenter) {
|
||||
$data['datacenter'] = $datacenter;
|
||||
// Hetzner removed the datacenter request property on 1 July 2026.
|
||||
// Normalize legacy values such as fsn1-dc14 before sending location.
|
||||
$location = ArkHostHetznerVPS_GetLocationOption($params);
|
||||
if ($location) {
|
||||
$data['location'] = $location;
|
||||
}
|
||||
|
||||
// Handle backups
|
||||
@@ -345,11 +397,11 @@ function ArkHostHetznerVPS_API(array $params) {
|
||||
break;
|
||||
|
||||
case 'Create backup':
|
||||
// Use snapshots
|
||||
// Create manual backup (still type=backup, but with distinct description)
|
||||
$url .= 'servers/' . ArkHostHetznerVPS_GetVPSID($params) . '/actions/create_image';
|
||||
$method = 'POST';
|
||||
$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'
|
||||
);
|
||||
break;
|
||||
@@ -975,9 +1027,11 @@ function ArkHostHetznerVPS_ConfigOptions() {
|
||||
'Type' => 'dropdown',
|
||||
'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(
|
||||
'FriendlyName' => 'Datacenter',
|
||||
'Description' => 'The datacenter location for the server.',
|
||||
'FriendlyName' => 'Location',
|
||||
'Description' => 'The Hetzner location for the server (Configurable option: Location or legacy datacenter).',
|
||||
'Type' => 'dropdown',
|
||||
'Options' => array(),
|
||||
),
|
||||
@@ -993,7 +1047,7 @@ function ArkHostHetznerVPS_ConfigOptions() {
|
||||
),
|
||||
'floating_ip_location' => array(
|
||||
'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',
|
||||
'Options' => array(),
|
||||
),
|
||||
@@ -1082,19 +1136,31 @@ function ArkHostHetznerVPS_ConfigOptions() {
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch datacenters
|
||||
$params['action'] = 'Datacenters';
|
||||
$datacenters = ArkHostHetznerVPS_API($params);
|
||||
// Fetch locations. Hetzner's datacenter endpoints are deprecated
|
||||
// and are scheduled for removal after 1 October 2026.
|
||||
$params['action'] = 'Locations';
|
||||
$locations = ArkHostHetznerVPS_API($params);
|
||||
|
||||
if (isset($datacenters['datacenters'])) {
|
||||
foreach ($datacenters['datacenters'] as $datacenter) {
|
||||
$array['datacenter']['Options'] += array(
|
||||
$datacenter['name'] => $datacenter['description'] . ' (' . $datacenter['location']['city'] . ')'
|
||||
);
|
||||
// Also populate floating IP locations
|
||||
$array['floating_ip_location']['Options'] += array(
|
||||
$datacenter['location']['name'] => $datacenter['location']['city'] . ', ' . $datacenter['location']['country']
|
||||
);
|
||||
if (isset($locations['locations'])) {
|
||||
foreach ($locations['locations'] as $location) {
|
||||
if (empty($location['name'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$place = array_filter(array(
|
||||
$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'],
|
||||
]);
|
||||
|
||||
// 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
|
||||
if (isset($create['root_password'])) {
|
||||
Capsule::table('tblhosting')->where('id', $params['serviceid'])->update([
|
||||
@@ -1201,7 +1284,9 @@ function ArkHostHetznerVPS_CreateAccount(array $params) {
|
||||
$floatingIpParams = $params;
|
||||
$floatingIpParams['action'] = 'Create Floating IP';
|
||||
$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['assign_to_server'] = true;
|
||||
|
||||
@@ -1353,6 +1438,10 @@ function ArkHostHetznerVPS_VNC(array $params) {
|
||||
<style>
|
||||
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; }
|
||||
#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; }
|
||||
#screen { flex: 1; display: flex; width: 100%; height: 100%; background: #000; position: relative; }
|
||||
#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>
|
||||
</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="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() {
|
||||
updateStatus("Connecting to VNC server...", "warning");
|
||||
|
||||
@@ -1515,7 +1644,30 @@ function ArkHostHetznerVPS_AdminLink(array $params) {
|
||||
|
||||
if (isset($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;
|
||||
@@ -1540,35 +1692,8 @@ function ArkHostHetznerVPS_ClientAreaAPI(array $params) {
|
||||
// Check backup permissions
|
||||
$backupActions = array('Create backup', 'Delete backup', 'Restore backup');
|
||||
if (in_array($action, $backupActions)) {
|
||||
// Check if backups are enabled either via module settings or configurable options
|
||||
$backupsEnabled = false;
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
// Check if backups are enabled in module settings
|
||||
$backupsEnabled = (ArkHostHetznerVPS_GetOption($params, 'backups') === 'on');
|
||||
|
||||
if (!$backupsEnabled) {
|
||||
return array('jsonResponse' => array(
|
||||
@@ -1634,8 +1759,9 @@ function ArkHostHetznerVPS_ClientAreaAPI(array $params) {
|
||||
$createParams = $params;
|
||||
$createParams['action'] = 'Create Floating IP';
|
||||
$createParams['ip_type'] = 'ipv4';
|
||||
$createParams['location'] = ArkHostHetznerVPS_GetOption($params, 'floating_ip_location') ?:
|
||||
ArkHostHetznerVPS_GetOption($params, 'datacenter');
|
||||
$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;
|
||||
|
||||
@@ -1805,7 +1931,8 @@ function ArkHostHetznerVPS_ClientAreaAPI(array $params) {
|
||||
'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']
|
||||
'type' => $image['type'],
|
||||
'status' => $image['status'] ?? 'available' // Include status from Hetzner API
|
||||
);
|
||||
$backupIndex++;
|
||||
}
|
||||
@@ -2260,16 +2387,31 @@ function ArkHostHetznerVPS_ClientArea(array $params) {
|
||||
// Process operating system info for Hetzner
|
||||
if (isset($serverInfo['image']) && !empty($operatingSystemsTemp['images'])) {
|
||||
$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;
|
||||
if ($imageType === 'system') {
|
||||
foreach ($operatingSystemsTemp['images'] as $os) {
|
||||
if ($os['name'] === $osId) {
|
||||
$currentOS = $os;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($currentOS) {
|
||||
$displayName = $currentOS['description'] ?? $currentOS['name'];
|
||||
@@ -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'))
|
||||
);
|
||||
} else {
|
||||
// Fallback if OS not found - use image info from server
|
||||
// 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) {
|
||||
@@ -2407,32 +2554,64 @@ function ArkHostHetznerVPS_ClientArea(array $params) {
|
||||
$serverInfo['bandwidth'] = 20480; // 20TB in GB (20 * 1024)
|
||||
$serverInfo['bandwidth_used'] = 0; // Not available via API
|
||||
|
||||
// Add datacenter info
|
||||
$serverInfo['datacenter'] = isset($serverInfo['datacenter']['description']) ? $serverInfo['datacenter']['description'] : 'N/A';
|
||||
$serverInfo['location'] = isset($serverInfo['datacenter']['location']['city']) ? $serverInfo['datacenter']['location']['city'] : 'N/A';
|
||||
// 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
|
||||
|
||||
if (!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
|
||||
if (!$passwordSetTime || ($currentTime - $passwordSetTime) < $expirationPeriod) {
|
||||
// Password is still valid - WHMCS already decrypts it for us in $params
|
||||
// No need to call decrypt() as $params['password'] is already plain text
|
||||
$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 {
|
||||
// Password has expired (timestamp exists and is older than 72 hours)
|
||||
$serverInfo['install_root'] = '';
|
||||
// 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' => '']);
|
||||
}
|
||||
} else {
|
||||
$serverInfo['install_root'] = '';
|
||||
}
|
||||
|
||||
// For now, always show backups tab - the API will handle permissions
|
||||
$backupsEnabled = true;
|
||||
// Check if backups are enabled in module settings
|
||||
$backupsEnabled = (ArkHostHetznerVPS_GetOption($params, 'backups') === 'on');
|
||||
|
||||
return array(
|
||||
'templatefile' => 'template/clientarea_direct',
|
||||
@@ -2443,6 +2622,7 @@ function ArkHostHetznerVPS_ClientArea(array $params) {
|
||||
'token' => generate_token('plain'),
|
||||
'ADDONLANG' => ArkHostHetznerVPS_Translation(),
|
||||
'backupsEnabled' => $backupsEnabled,
|
||||
'productName' => $params['productname'] ?? $params['configoption1'] ?? 'VPS',
|
||||
)
|
||||
);
|
||||
} catch (Exception $err) {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* WHMCS Server Module - Hetzner VPS Hooks
|
||||
*
|
||||
* @package WHMCS
|
||||
* @version 1.2.1
|
||||
* @version 1.2.5
|
||||
* @copyright Copyright (c) ArkHost 2025
|
||||
* @author ArkHost <support@arkhost.com>
|
||||
* @link https://arkhost.com
|
||||
@@ -70,4 +70,3 @@ add_hook('ClientAreaHeadOutput', 1, function($vars) {
|
||||
</style>
|
||||
HTML;
|
||||
});
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ $_ADDONLANG['Overview']['ResourceAllocation'] = 'Resource Allocation';
|
||||
$_ADDONLANG['Overview']['QuickActions'] = 'Quick Actions';
|
||||
$_ADDONLANG['Overview']['Hostname'] = 'Hostname';
|
||||
$_ADDONLANG['Overview']['Status'] = 'Status';
|
||||
$_ADDONLANG['Overview']['OS'] = 'Operating System';
|
||||
$_ADDONLANG['Overview']['OS'] = 'OS';
|
||||
$_ADDONLANG['Overview']['Location'] = 'Location';
|
||||
$_ADDONLANG['Overview']['CPU_Cores'] = 'vCPU Cores';
|
||||
$_ADDONLANG['Overview']['Memory'] = 'Memory';
|
||||
@@ -143,7 +143,7 @@ $_ADDONLANG['Settings']['Password']['Submit'] = 'Reset Password';
|
||||
### 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']['OS'] = 'Operating System';
|
||||
$_ADDONLANG['Settings']['Reinstall']['OS'] = 'OS';
|
||||
$_ADDONLANG['Settings']['Reinstall']['Version'] = 'CHOOSE VERSION';
|
||||
$_ADDONLANG['Settings']['Reinstall']['Submit'] = 'Reinstall';
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<script type="text/javascript">
|
||||
// Initialize template variables for external script
|
||||
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 csrfToken = '{$token}';
|
||||
var webRoot = '{$WEB_ROOT}';
|
||||
@@ -207,11 +207,11 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<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>
|
||||
<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>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -521,8 +521,10 @@ function updateBackupTable(data) {
|
||||
backupDate = backupDate.split('T')[0];
|
||||
}
|
||||
|
||||
// Determine backup type
|
||||
var backupType = backup.type === 'backup' ? lang.backups.automatic : lang.backups.manual;
|
||||
// Determine backup type based on description
|
||||
// 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)
|
||||
var sizeStr = 'N/A';
|
||||
@@ -530,22 +532,40 @@ function updateBackupTable(data) {
|
||||
sizeStr = backup.size + ' ' + lang.general.gb;
|
||||
}
|
||||
|
||||
// Status is always available for Hetzner backups
|
||||
var statusBadge = '<span class="badge badge-success">' + lang.backups.available + '</span>';
|
||||
// Determine status badge based on size first (more reliable than API status)
|
||||
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
|
||||
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 =
|
||||
'<td>' + backupDate + '</td>' +
|
||||
'<td>' + sizeStr + '</td>' +
|
||||
'<td>' + backupType + '</td>' +
|
||||
'<td>' + statusBadge + '</td>' +
|
||||
'<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>' +
|
||||
'</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>' +
|
||||
'</button>' +
|
||||
'</td>';
|
||||
|
||||
@@ -68,6 +68,68 @@
|
||||
font-weight: 600;
|
||||
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 {
|
||||
border-bottom: 2px solid #3c7fb4;
|
||||
margin-bottom: 20px;
|
||||
|
||||
Reference in New Issue
Block a user