diff --git a/README.md b/README.md index 39b19d9..49bce31 100644 --- a/README.md +++ b/README.md @@ -5,34 +5,194 @@ ![GitHub issues](https://img.shields.io/github/issues/EZSCALE/virtfusion-whmcs-module) ![GitHub pull requests](https://img.shields.io/github/issues-pr/EZSCALE/virtfusion-whmcs-module) -This module requires VirtFusion v1.7.3 or higher as this is what it's based on. Please refer to the -official [documentation](https://docs.virtfusion.com/integrations/whmcs). +A comprehensive WHMCS provisioning module for [VirtFusion](https://virtfusion.com) that enables automated VPS server provisioning, management, and client self-service directly from WHMCS. + +## Requirements + +- **VirtFusion** v1.7.3 or higher +- **WHMCS** 8.x or higher +- **PHP** 8.0 or higher +- A valid VirtFusion API token with appropriate permissions + +## Features + +### Provisioning +- Automatic server creation, suspension, unsuspension, and termination +- Package/plan upgrades and downgrades +- Automatic VirtFusion user creation linked to WHMCS client accounts +- Configurable options mapping for dynamic resource allocation + +### Client Area +- **Server Overview** - Real-time server information (hostname, IP, resources, status badge) +- **Power Management** - Start, restart, shutdown, and force power off controls +- **Control Panel SSO** - One-click login to VirtFusion panel via authentication tokens +- **Server Rebuild** - Reinstall with any available OS template directly from WHMCS +- **Password Reset** - Reset VirtFusion panel login credentials +- **Bandwidth Usage** - Traffic usage display with allocation limits +- **Billing Overview** - Product, billing cycle, and payment information + +### Admin Area +- Server connection testing (Test Connection button) +- Server information display with live data from VirtFusion +- Admin impersonation for VirtFusion panel access +- Editable Server ID field for manual adjustments +- Full server object JSON viewer + +### Ordering Process +- Dynamic OS template dropdown populated from VirtFusion API +- SSH key selection dropdown for users with saved keys +- Checkout validation ensuring OS selection before order placement +- Compatible with all WHMCS order form templates + +### Theme Compatibility +- Works with **all WHMCS themes** including Six, Twenty-One, Lagom, and custom themes +- Uses dual `panel`/`card` CSS classes for Bootstrap 3/4/5 compatibility +- Framework-agnostic HTML structure +- Responsive design with mobile-friendly layouts +- Templates support the WHMCS theme override system + +### Security +- SSL/TLS certificate verification enabled by default +- Input sanitization on all user-supplied parameters +- Service ownership validation on all client API endpoints +- Proper HTTP status codes for error responses (401, 403, 400, 500) +- XSS protection via `htmlspecialchars()` and `encodeURIComponent()` +- Direct file access prevention on all PHP files ## Installation 1. Download the latest release from the [releases](https://github.com/EZSCALE/virtfusion-whmcs-module/releases) page. -2. Extract the contents of the archive and upload the modules folder to your WHMCS installation directory. +2. Extract the archive and upload the `modules/` folder to your WHMCS installation directory. +3. In WHMCS Admin, go to **Configuration > System Settings > Servers** and add a new server: + - **Type**: VirtFusion Direct Provisioning + - **Hostname**: Your VirtFusion panel hostname (e.g., `cp.example.com`) + - **Password/Access Hash**: Your VirtFusion API token +4. Click **Test Connection** to verify the API connection. +5. Create or edit a product and set the **Module** to "VirtFusion Direct Provisioning". -## :heavy_exclamation_mark: Important Notes :heavy_exclamation_mark: +## Custom Fields Setup -You must create two custom fields in WHMCS for this module to work. You need to configure the following custom fields on -each product you want to use this module with. +You **must** create two custom fields on each product that uses this module: -| Field Name | Field Type | Description | Validation | Select Options | Admin Only | Required Field | Show on Order Form | Show on Invoice | -|--------------------------|------------|--------------------------|-------------|----------------|------------|----------------|--------------------|-----------------| -| Initial Operating System | Text Box | Set to whatever you want | Leave Blank | Leave Blank | :x: | :x: | :white_check_mark: | :x: | -| Initial SSH Key | Text Box | Set to whatever you want | Leave Blank | Leave Blank | :x: | :x: | :white_check_mark: | :x: | +| Field Name | Field Type | Show on Order Form | Admin Only | Required | +|--------------------------|------------|--------------------| ---------- | -------- | +| Initial Operating System | Text Box | Yes | No | No | +| Initial SSH Key | Text Box | Yes | No | No | -You can run this SQL query to create the custom fields. Run the SQL from this [file](modify.sql) or copy the contents -from it. +You can run the included SQL to auto-create these fields for all VirtFusion products: -## What does this module change? +```sql +-- See modify.sql for the complete query +``` -This module changes the following things: +Or run the SQL from the [modify.sql](modify.sql) file. -- Adds configurable options to the product configuration page to allow the user to select the operating system and add - an ssh key to the initial deployment. +## Module Configuration Options -## TODO +Each product using this module has three configuration options: -- [ ] Add post checkout checks to ensure the user has selected an operating system and added a ssh key. +| Option | Name | Description | +|--------|------|-------------| +| Config Option 1 | Hypervisor Group ID | The VirtFusion hypervisor group ID for server placement (default: 1) | +| Config Option 2 | Package ID | The VirtFusion package ID that defines server resources (default: 1) | +| Config Option 3 | Default IPv4 | Number of IPv4 addresses to assign (0-10, default: 1) | + +## Configurable Options (Dynamic Pricing) + +To allow customers to select different resource levels with pricing, create WHMCS Configurable Options groups with these option names: + +| VirtFusion Parameter | Default Option Name | Description | +|---------------------|--------------------| ----------- | +| `packageId` | Package | VirtFusion package ID | +| `hypervisorId` | Location | Hypervisor group for server placement | +| `ipv4` | IPv4 | Number of IPv4 addresses | +| `storage` | Storage | Disk space in GB | +| `memory` | Memory | RAM in MB (values < 1024 auto-converted from GB) | +| `traffic` | Bandwidth | Monthly traffic allowance in GB | +| `cpuCores` | CPU Cores | Number of CPU cores | +| `networkSpeedInbound` | Inbound Network Speed | Inbound speed in Mbps | +| `networkSpeedOutbound` | Outbound Network Speed | Outbound speed in Mbps | +| `networkProfile` | Network Type | VirtFusion network profile ID | +| `storageProfile` | Storage Type | VirtFusion storage profile ID | + +### Custom Option Name Mapping + +If your configurable option names differ from the defaults, create a mapping file: + +1. Copy `config/ConfigOptionMapping-example.php` to `config/ConfigOptionMapping.php` +2. Edit the mapping array to match your option names + +## Theme Override + +To customize the module templates for a specific theme, copy the template files to: + +``` +/templates/yourthemename/modules/servers/VirtFusionDirect/ +``` + +WHMCS will automatically use theme-specific templates when available. + +## API Endpoints Used + +This module uses the following VirtFusion API v1 endpoints: + +| Endpoint | Purpose | +|----------|---------| +| `GET /connect` | Connection testing | +| `GET/POST /users` | User lookup and creation | +| `POST /servers` | Server creation | +| `POST /servers/{id}/build` | OS installation | +| `GET /servers/{id}` | Server details retrieval | +| `DELETE /servers/{id}` | Server termination | +| `POST /servers/{id}/suspend` | Server suspension | +| `POST /servers/{id}/unsuspend` | Server unsuspension | +| `PUT /servers/{id}/package/{pkgId}` | Package changes | +| `POST /servers/{id}/power/*` | Power management (boot/shutdown/restart/poweroff) | +| `PATCH /servers/{id}/name` | Server renaming | +| `POST /users/{id}/serverAuthenticationTokens/{serverId}` | SSO token generation | +| `POST /users/{id}/byExtRelation/resetPassword` | Password reset | +| `GET /packages` | Package listing | +| `GET /media/templates/fromServerPackageSpec/{id}` | OS template listing | +| `GET /ssh_keys/user/{id}` | SSH key listing | + +## Troubleshooting + +### Connection Test Fails +- Verify the VirtFusion panel hostname is correct and accessible +- Ensure the API token has not expired +- Check that SSL certificates on the VirtFusion panel are valid (self-signed certificates will cause connection failures) + +### Server Creation Fails +- Check the Module Log in WHMCS Admin (Utilities > Logs > Module Log) for detailed error messages +- Verify the Package ID and Hypervisor Group ID are correct +- Ensure the VirtFusion API token has permission to create servers + +### OS Templates Not Showing +- Confirm the Package ID (Config Option 2) is set correctly +- Verify the package has OS templates assigned in VirtFusion +- Check that the "Initial Operating System" custom field exists on the product + +### Client Area Shows Error +- Ensure a VirtFusion server is configured in WHMCS Server Settings +- Check that the service has been provisioned (not in Pending status) +- Review the Module Log for API communication errors + +### SSO / Control Panel Login Fails +- The VirtFusion panel must be accessible from the client's browser +- Verify the VirtFusion user exists (check by external relation ID) +- Ensure authentication token generation permissions are enabled on the API token + +## Security Considerations + +- **API Tokens**: Store API tokens only in the WHMCS server password field. WHMCS encrypts this value at rest. +- **SSL Verification**: SSL certificate verification is enabled by default. Do not disable it in production environments. +- **Module Updates**: Keep the module updated to receive security patches. +- **Access Control**: The module validates service ownership on every client API call. Admin endpoints require WHMCS admin authentication. + +## Contributing + +Contributions are welcome. Please open an issue or pull request on the [GitHub repository](https://github.com/EZSCALE/virtfusion-whmcs-module). + +## License + +This project is licensed under the GNU General Public License v3.0 - see the [LICENSE.md](LICENSE.md) file for details. diff --git a/modules/servers/VirtFusionDirect/VirtFusionDirect.php b/modules/servers/VirtFusionDirect/VirtFusionDirect.php index a49df0b..c6f7ce5 100644 --- a/modules/servers/VirtFusionDirect/VirtFusionDirect.php +++ b/modules/servers/VirtFusionDirect/VirtFusionDirect.php @@ -5,6 +5,8 @@ if (!defined("WHMCS")) { } use WHMCS\Module\Server\VirtFusionDirect\ModuleFunctions; +use WHMCS\Module\Server\VirtFusionDirect\Module; +use WHMCS\Module\Server\VirtFusionDirect\Database; function VirtFusionDirect_MetaData() { @@ -12,7 +14,7 @@ function VirtFusionDirect_MetaData() 'DisplayName' => 'VirtFusion Direct Provisioning', 'APIVersion' => '1.1', 'RequiresServer' => true, - 'ServiceSingleSignOnLabel' => false, + 'ServiceSingleSignOnLabel' => 'Login to VirtFusion Panel', 'AdminSingleSignOnLabel' => false, ]; } @@ -24,39 +26,85 @@ function VirtFusionDirect_ConfigOptions() "FriendlyName" => "Hypervisor Group ID", "Type" => "text", "Size" => "20", - "Description" => "The default hypervisor group ID", + "Description" => "The default hypervisor group ID for server placement.", "Default" => "1", ], "packageID" => [ "FriendlyName" => "Package ID", "Type" => "text", "Size" => "20", - "Description" => "The package ID", + "Description" => "The VirtFusion package ID that defines server resources.", "Default" => "1", ], "defaultIPv4" => [ "FriendlyName" => "Default IPv4", "Type" => "dropdown", "Options" => "0,1,2,3,4,5,6,7,8,9,10", - "Description" => "The default amount of IPv4 addresses to assign to the server.", + "Description" => "The default number of IPv4 addresses to assign to each server.", "Default" => "1", ], ]; } +function VirtFusionDirect_TestConnection(array $params) +{ + try { + $module = new Module(); + $cp = $module->getCP($params['serverid']); + + if (!$cp) { + return ['success' => false, 'error' => 'Unable to retrieve server configuration. Please verify the server hostname and access hash/password.']; + } + + $request = $module->initCurl($cp['token']); + $data = $request->get($cp['url'] . '/connect'); + + $httpCode = $request->getRequestInfo('http_code'); + + if ($httpCode == 200) { + return ['success' => true, 'error' => '']; + } + + if ($httpCode == 401) { + return ['success' => false, 'error' => 'Authentication failed. Please verify your API token is correct and has not expired.']; + } + + if ($httpCode == 0) { + $curlError = $request->getRequestInfo('curl_error'); + return ['success' => false, 'error' => 'Connection failed: ' . ($curlError ?: 'Unable to reach the VirtFusion server. Verify the hostname and that SSL certificates are valid.')]; + } + + return ['success' => false, 'error' => 'Unexpected response from VirtFusion API (HTTP ' . $httpCode . '). Please check the server configuration.']; + } catch (\Exception $e) { + return ['success' => false, 'error' => 'Connection test failed: ' . $e->getMessage()]; + } +} + function VirtFusionDirect_AdminCustomButtonArray() { - $buttonarray = array( + return [ "Update Server Object" => "updateServerObject", - ); - return $buttonarray; + ]; +} + +function VirtFusionDirect_ServiceSingleSignOn(array $params) +{ + try { + $module = new Module(); + $token = $module->fetchLoginTokens($params['serviceid']); + + if ($token) { + return ['success' => true, 'redirectTo' => $token]; + } + + return ['success' => false, 'errorMsg' => 'Unable to generate a login token. The server may not be active or the VirtFusion API may be unreachable.']; + } catch (\Exception $e) { + return ['success' => false, 'errorMsg' => $e->getMessage()]; + } } /** - * - * * Service functions - * */ function VirtFusionDirect_CreateAccount(array $params) { @@ -86,7 +134,6 @@ function VirtFusionDirect_updateServerObject(array $params) /** * Allows changing of the package of a server * - * @author https://github.com/BlinkohHost/virtfusion-whmcs-module * @param array $params * @return string */ @@ -108,4 +155,4 @@ function VirtFusionDirect_AdminServicesTabFieldsSave(array $params) function VirtFusionDirect_ClientArea(array $params) { return (new ModuleFunctions())->clientArea($params); -} \ No newline at end of file +} diff --git a/modules/servers/VirtFusionDirect/client.php b/modules/servers/VirtFusionDirect/client.php index a17cfd6..68701a1 100644 --- a/modules/servers/VirtFusionDirect/client.php +++ b/modules/servers/VirtFusionDirect/client.php @@ -9,103 +9,173 @@ $vf = new Module(); $vf->isAuthenticated(); -switch ($vf->validateAction(true)) { +$action = $vf->validateAction(true); + +switch ($action) { /** - * * Reset Password. - * */ case 'resetPassword': - if ($vf->validateServiceID(true)) { - - $client = $vf->validateUserOwnsService((int)$_GET['serviceID']); - - if (!$client) { - $vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 200); - } - - $data = $vf->resetUserPassword((int)$_GET['serviceID'], $client); - - if ($data) { - $vf->output(['success' => true, 'data' => $data->data], true, true, 200); - } - - $vf->output(['success' => false, 'errors' => 'error'], true, true, 200); + $serviceID = $vf->validateServiceID(true); + $client = $vf->validateUserOwnsService($serviceID); + if (!$client) { + $vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403); } + + $data = $vf->resetUserPassword($serviceID, $client); + + if ($data) { + $vf->output(['success' => true, 'data' => $data->data], true, true, 200); + } + + $vf->output(['success' => false, 'errors' => 'Password reset failed'], true, true, 500); break; /** - * * Get server information. - * */ case 'serverData': - if ($vf->validateServiceID(true)) { - - if (!$vf->validateUserOwnsService((int)$_GET['serviceID'])) { - $vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 200); - } - - $data = $vf->fetchServerData((int)$_GET['serviceID']); - - if ($data) { - - (new Module())->updateWhmcsServiceParamsOnServerObject((int)$_GET['serviceID'], $data); - - $vf->output(['success' => true, 'data' => (new ServerResource())->process($data)], true, true, 200); - - } - - $vf->output(['success' => false, 'errors' => 'error'], true, true, 200); + $serviceID = $vf->validateServiceID(true); + if (!$vf->validateUserOwnsService($serviceID)) { + $vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403); } + + $data = $vf->fetchServerData($serviceID); + + if ($data) { + (new Module())->updateWhmcsServiceParamsOnServerObject($serviceID, $data); + $vf->output(['success' => true, 'data' => (new ServerResource())->process($data)], true, true, 200); + } + + $vf->output(['success' => false, 'errors' => 'Unable to retrieve server data'], true, true, 500); break; /** - * * Login as server owner. - * */ case 'loginAsServerOwner': - if ($vf->validateServiceID(true)) { - /** - * A client can't log in as any user. Ownership should be validated. - */ - - if (!$vf->validateUserOwnsService((int)$_GET['serviceID'])) { - $vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 200); - } - - $token = $vf->fetchLoginTokens((int)$_GET['serviceID']); - - if ($token) { - - /** - * A valid token/url was received. - */ - $vf->output(['success' => true, 'token_url' => $token], true, true, 200); - } - - /** - * Failed to get the token from the control panel or the service ID doesn't exist. - */ - $vf->output(['success' => false, 'errors' => 'token request error'], true, true, 200); + $serviceID = $vf->validateServiceID(true); + if (!$vf->validateUserOwnsService($serviceID)) { + $vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403); } + + $token = $vf->fetchLoginTokens($serviceID); + + if ($token) { + $vf->output(['success' => true, 'token_url' => $token], true, true, 200); + } + + $vf->output(['success' => false, 'errors' => 'Unable to generate login token'], true, true, 500); + break; + + /** + * Power management actions: boot, shutdown, restart, poweroff + */ + case 'powerAction': + + $serviceID = $vf->validateServiceID(true); + + if (!$vf->validateUserOwnsService($serviceID)) { + $vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403); + } + + $powerAction = isset($_GET['powerAction']) ? preg_replace('/[^a-zA-Z]/', '', $_GET['powerAction']) : ''; + $allowedActions = ['boot', 'shutdown', 'restart', 'poweroff']; + + if (!in_array($powerAction, $allowedActions, true)) { + $vf->output(['success' => false, 'errors' => 'Invalid power action'], true, true, 400); + } + + $result = $vf->serverPowerAction($serviceID, $powerAction); + + if ($result) { + $vf->output(['success' => true, 'data' => ['action' => $powerAction, 'message' => 'Power action queued successfully']], true, true, 200); + } + + $vf->output(['success' => false, 'errors' => 'Power action failed. The server may be locked or unavailable.'], true, true, 500); + break; + + /** + * Rebuild/reinstall server with new OS. + */ + case 'rebuild': + + $serviceID = $vf->validateServiceID(true); + + if (!$vf->validateUserOwnsService($serviceID)) { + $vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403); + } + + $osId = isset($_GET['osId']) ? (int) $_GET['osId'] : 0; + $hostname = isset($_GET['hostname']) ? preg_replace('/[^a-zA-Z0-9.\-]/', '', $_GET['hostname']) : null; + + if ($osId <= 0) { + $vf->output(['success' => false, 'errors' => 'Invalid operating system ID'], true, true, 400); + } + + $result = $vf->rebuildServer($serviceID, $osId, $hostname); + + if ($result) { + $vf->output(['success' => true, 'data' => ['message' => 'Server rebuild initiated successfully']], true, true, 200); + } + + $vf->output(['success' => false, 'errors' => 'Server rebuild failed. The server may be locked or unavailable.'], true, true, 500); + break; + + /** + * Rename server. + */ + case 'rename': + + $serviceID = $vf->validateServiceID(true); + + if (!$vf->validateUserOwnsService($serviceID)) { + $vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403); + } + + $newName = isset($_GET['name']) ? trim($_GET['name']) : ''; + $newName = htmlspecialchars($newName, ENT_QUOTES, 'UTF-8'); + + if (empty($newName) || strlen($newName) > 255) { + $vf->output(['success' => false, 'errors' => 'Invalid server name'], true, true, 400); + } + + $result = $vf->renameServer($serviceID, $newName); + + if ($result) { + $vf->output(['success' => true, 'data' => ['message' => 'Server renamed successfully']], true, true, 200); + } + + $vf->output(['success' => false, 'errors' => 'Server rename failed'], true, true, 500); + break; + + /** + * Get available OS templates for rebuild. + */ + case 'osTemplates': + + $serviceID = $vf->validateServiceID(true); + + if (!$vf->validateUserOwnsService($serviceID)) { + $vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403); + } + + $templates = $vf->fetchOsTemplates($serviceID); + + if ($templates !== false) { + $vf->output(['success' => true, 'data' => $templates], true, true, 200); + } + + $vf->output(['success' => false, 'errors' => 'Unable to fetch OS templates'], true, true, 500); break; default: - /** - * - * No valid action was specified. - * - */ - $vf->output(['success' => false, 'errors' => 'invalid action'], true, true, 200); + $vf->output(['success' => false, 'errors' => 'invalid action'], true, true, 400); } - - diff --git a/modules/servers/VirtFusionDirect/hooks.php b/modules/servers/VirtFusionDirect/hooks.php index 74c3d6e..a08d861 100644 --- a/modules/servers/VirtFusionDirect/hooks.php +++ b/modules/servers/VirtFusionDirect/hooks.php @@ -7,120 +7,208 @@ if (!defined("WHMCS")) { die("This file cannot be accessed directly"); } +/** + * Shopping Cart Validation Hook + * + * Validates that an operating system has been selected before checkout + * for all VirtFusion products in the cart. + */ +add_hook('ShoppingCartValidateCheckout', 1, function ($vars) { + $errors = []; + + if (!isset($_SESSION['cart']['products']) || !is_array($_SESSION['cart']['products'])) { + return $errors; + } + + foreach ($_SESSION['cart']['products'] as $key => $product) { + $pid = $product['pid'] ?? null; + if (!$pid) { + continue; + } + + $dbProduct = \WHMCS\Database\Capsule::table('tblproducts') + ->where('id', $pid) + ->where('servertype', 'VirtFusionDirect') + ->first(); + + if (!$dbProduct) { + continue; + } + + // Check if Initial Operating System custom field has a value + if (isset($product['customfields']) && is_array($product['customfields'])) { + $osSelected = false; + $customFields = \WHMCS\Database\Capsule::table('tblcustomfields') + ->where('relid', $pid) + ->where('type', 'product') + ->get(); + + foreach ($customFields as $field) { + if (strtolower(str_replace(' ', '', $field->fieldname)) === 'initialoperatingsystem') { + $fieldValue = $product['customfields'][$field->id] ?? ''; + if (!empty($fieldValue) && is_numeric($fieldValue)) { + $osSelected = true; + } + break; + } + } + + if (!$osSelected) { + $errors[] = 'Please select an Operating System for your VPS order.'; + } + } + } + + return $errors; +}); + +/** + * Client Area Footer Output Hook + * + * Dynamically converts hidden text fields for OS templates and SSH keys + * into dropdown selects populated from the VirtFusion API. + * Works with all WHMCS themes by using vanilla JavaScript and standard form-control classes. + */ add_hook('ClientAreaFooterOutput', 1, function ($vars) { if (!isset($vars['productinfo']['module']) || $vars['productinfo']['module'] !== 'VirtFusionDirect') { return null; } - $cs = new ConfigureService(); + try { + $cs = new ConfigureService(); - $templates_data = $cs->fetchTemplates( - $cs->fetchPackageByDbId($vars['productinfo']['pid']) ?? $cs->fetchPackageId($vars['productinfo']['name']) - ); + $templates_data = $cs->fetchTemplates( + $cs->fetchPackageByDbId($vars['productinfo']['pid']) ?? $cs->fetchPackageId($vars['productinfo']['name']) + ); - if (empty($templates_data)) { - return null; - } - - $dropdownOptions = []; - - foreach ($templates_data['data'] as $osCategory) { - foreach ($osCategory['templates'] as $template) { - $optionValue = $template['id']; - $optionLabel = $template['name']." ".$template['version']." ".$template['variant']; - $dropdownOptions[] = ['id' => $optionValue, 'name' => $optionLabel]; - } - } - - // Sort dropdownOptions alphabetically by the 'name' key - usort($dropdownOptions, function ($a, $b) { - return strcmp($a['name'], $b['name']); - }); - - $sshKeys = $cs->getUserSshKeys($vars['loggedinuser']); - $sshKeysOptions = array_map(function ($sshKey) { - if ($sshKey['enabled'] === false) { + if (empty($templates_data)) { return null; } - return [ - 'id' => $sshKey['id'], - 'name' => $sshKey['name'] - ]; - }, $sshKeys['data'] ?? []); + $dropdownOptions = []; - $osID = array_values(array_filter(array_map(function ($option) { - if ($option['textid'] === 'initialoperatingsystem') { - return $option['id']; + foreach ($templates_data['data'] as $osCategory) { + foreach ($osCategory['templates'] as $template) { + $optionValue = $template['id']; + $optionLabel = htmlspecialchars($template['name'] . " " . $template['version'] . " " . $template['variant'], ENT_QUOTES, 'UTF-8'); + $dropdownOptions[] = ['id' => $optionValue, 'name' => $optionLabel]; + } } - }, $vars['customfields']))); - $sshID = array_values(array_filter(array_map(function ($option) { - if ($option['textid'] === 'initialsshkey') { - return $option['id']; + usort($dropdownOptions, function ($a, $b) { + return strcmp($a['name'], $b['name']); + }); + + $sshKeys = []; + $sshKeysOptions = []; + if (isset($vars['loggedinuser']) && $vars['loggedinuser']) { + $sshKeysData = $cs->getUserSshKeys($vars['loggedinuser']); + if ($sshKeysData && isset($sshKeysData['data'])) { + $sshKeysOptions = array_values(array_filter(array_map(function ($sshKey) { + if ($sshKey['enabled'] === false) { + return null; + } + return [ + 'id' => $sshKey['id'], + 'name' => htmlspecialchars($sshKey['name'], ENT_QUOTES, 'UTF-8') + ]; + }, $sshKeysData['data']))); + } } - }, $vars['customfields']))); - // Construct the JavaScript code - return " + $osID = array_values(array_filter(array_map(function ($option) { + if ($option['textid'] === 'initialoperatingsystem') { + return $option['id']; + } + }, $vars['customfields'] ?? []))); + + $sshID = array_values(array_filter(array_map(function ($option) { + if ($option['textid'] === 'initialsshkey') { + return $option['id']; + } + }, $vars['customfields'] ?? []))); + + $osFieldId = $osID[0] ?? null; + $sshFieldId = $sshID[0] ?? null; + + if ($osFieldId === null) { + return null; + } + + return " "; + } catch (\Exception $e) { + // Silently fail - don't break the checkout page + return null; + } }); diff --git a/modules/servers/VirtFusionDirect/lib/Curl.php b/modules/servers/VirtFusionDirect/lib/Curl.php index adaa588..fe537d6 100644 --- a/modules/servers/VirtFusionDirect/lib/Curl.php +++ b/modules/servers/VirtFusionDirect/lib/Curl.php @@ -8,12 +8,14 @@ class Curl private $data; private $customOptions = []; private $defaultOptions = [ - CURLOPT_SSL_VERIFYPEER => false, - CURLOPT_SSL_VERIFYHOST => false, + CURLOPT_SSL_VERIFYPEER => true, + CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_RETURNTRANSFER => true, - CURLOPT_USERAGENT => 'CURL', + CURLOPT_USERAGENT => 'VirtFusion-WHMCS/2.0', CURLOPT_HEADER => false, CURLOPT_NOBODY => false, + CURLOPT_TIMEOUT => 30, + CURLOPT_CONNECTTIMEOUT => 10, ]; @@ -24,7 +26,7 @@ class Curl public function useCookies() { - $cookiesFile = tempnam('/tmp', 'virtfusion_cookies'); + $cookiesFile = tempnam(sys_get_temp_dir(), 'virtfusion_cookies'); $this->defaultOptions[CURLOPT_COOKIEFILE] = $cookiesFile; $this->defaultOptions[CURLOPT_COOKIEJAR] = $cookiesFile; } @@ -57,6 +59,15 @@ class Curl return $this->send('PUT', $url); } + /** + * @param null $url + * @return bool|string|void + */ + public function patch($url = null) + { + return $this->send('PATCH', $url); + } + /** * @param $method * @param $url @@ -84,6 +95,12 @@ class Curl $response = curl_exec($this->ch); $this->data['info'] = curl_getinfo($this->ch); + + if ($response === false) { + $this->data['info']['curl_error'] = curl_error($this->ch); + $this->data['info']['curl_errno'] = curl_errno($this->ch); + } + if (isset($this->customOptions[CURLOPT_HEADER]) && $this->customOptions[CURLOPT_HEADER]) { $this->data['info']['request_header'] = trim($this->data['info']['request_header']); $this->processHeaders($response); @@ -197,4 +214,4 @@ class Curl return $this->data['data']; } -} \ No newline at end of file +} diff --git a/modules/servers/VirtFusionDirect/lib/Module.php b/modules/servers/VirtFusionDirect/lib/Module.php index e324954..50fa36d 100644 --- a/modules/servers/VirtFusionDirect/lib/Module.php +++ b/modules/servers/VirtFusionDirect/lib/Module.php @@ -2,46 +2,45 @@ namespace WHMCS\Module\Server\VirtFusionDirect; - class Module { public function __construct() { - error_reporting(0); Database::schema(); } /** * @param bool $exitOnError - * @return mixed + * @return string */ public function validateAction($exitOnError = true) { if (!isset($_GET['action'])) { - $this->output(['errors' => 'no action specified'], true, $exitOnError, 200); + $this->output(['success' => false, 'errors' => 'no action specified'], true, $exitOnError, 400); } - return $_GET['action']; + return preg_replace('/[^a-zA-Z0-9_]/', '', $_GET['action']); } /** * @param bool $exitOnError - * @return mixed + * @return int */ public function validateServiceID($exitOnError = true) { - if (!isset($_GET['serviceID'])) { - $this->output(['errors' => 'no serviceID specified'], true, $exitOnError, 200); + if (!isset($_GET['serviceID']) || !is_numeric($_GET['serviceID'])) { + $this->output(['success' => false, 'errors' => 'no valid serviceID specified'], true, $exitOnError, 400); } - return $_GET['serviceID']; + return (int) $_GET['serviceID']; } /** - * @param $serviceID + * @param int $serviceID * @param bool $exitOnError - * @return bool + * @return int|false */ public function validateUserOwnsService($serviceID, $exitOnError = true) { + $serviceID = (int) $serviceID; $currentUser = new \WHMCS\Authentication\CurrentUser; $client = $currentUser->client(); @@ -57,11 +56,12 @@ class Module } /** - * @param $serviceID + * @param int $serviceID * @return false|string */ public function fetchLoginTokens($serviceID) { + $serviceID = (int) $serviceID; $service = Database::getSystemService($serviceID); if ($service) { @@ -69,13 +69,15 @@ class Module $cp = $this->getCP($whmcsService->server); $request = $this->initCurl($cp['token']); - $data = $request->post($cp['url'] . '/users/' . $whmcsService->userid . '/serverAuthenticationTokens/' . $service->server_id); + $data = $request->post($cp['url'] . '/users/' . (int) $whmcsService->userid . '/serverAuthenticationTokens/' . (int) $service->server_id); Log::insert(__FUNCTION__, $request->getRequestInfo(), $data); if ($request->getRequestInfo('http_code') == '200') { $data = json_decode($data); - return $cp['base_url'] . $data->data->authentication->endpoint_complete; + if (isset($data->data->authentication->endpoint_complete)) { + return $cp['base_url'] . $data->data->authentication->endpoint_complete; + } } } return false; @@ -117,13 +119,14 @@ class Module public function fetchServerData($serviceID) { + $serviceID = (int) $serviceID; $service = Database::getSystemService($serviceID); if ($service) { $whmcsService = Database::getWhmcsService($serviceID); $cp = $this->getCP($whmcsService->server); $request = $this->initCurl($cp['token']); - $data = $request->get($cp['url'] . '/servers/' . $service->server_id); + $data = $request->get($cp['url'] . '/servers/' . (int) $service->server_id); Log::insert(__FUNCTION__, $request->getRequestInfo(), $data); @@ -134,8 +137,170 @@ class Module return false; } + /** + * Execute a power action on a server. + * + * @param int $serviceID + * @param string $action One of: boot, shutdown, restart, poweroff + * @return object|false + */ + public function serverPowerAction($serviceID, $action) + { + $serviceID = (int) $serviceID; + $allowedActions = ['boot', 'shutdown', 'restart', 'poweroff']; + if (!in_array($action, $allowedActions, true)) { + return false; + } + + $service = Database::getSystemService($serviceID); + + if ($service) { + $whmcsService = Database::getWhmcsService($serviceID); + $cp = $this->getCP($whmcsService->server); + $request = $this->initCurl($cp['token']); + $data = $request->post($cp['url'] . '/servers/' . (int) $service->server_id . '/power/' . $action); + + Log::insert(__FUNCTION__ . ':' . $action, $request->getRequestInfo(), $data); + + $httpCode = $request->getRequestInfo('http_code'); + if ($httpCode == 200 || $httpCode == 204) { + return json_decode($data) ?: (object) ['success' => true]; + } + } + return false; + } + + /** + * Rebuild/reinstall a server with a new OS. + * + * @param int $serviceID + * @param int $osId Operating system template ID + * @param string|null $hostname Optional new hostname + * @return object|false + */ + public function rebuildServer($serviceID, $osId, $hostname = null) + { + $serviceID = (int) $serviceID; + $osId = (int) $osId; + + if ($osId <= 0) { + return false; + } + + $service = Database::getSystemService($serviceID); + + if ($service) { + $whmcsService = Database::getWhmcsService($serviceID); + $cp = $this->getCP($whmcsService->server); + $request = $this->initCurl($cp['token']); + + $buildData = [ + 'operatingSystemId' => $osId, + 'email' => true, + ]; + + if ($hostname !== null && $hostname !== '') { + $buildData['hostname'] = $hostname; + } + + $request->addOption(CURLOPT_POSTFIELDS, json_encode($buildData)); + $data = $request->post($cp['url'] . '/servers/' . (int) $service->server_id . '/build'); + + Log::insert(__FUNCTION__, $request->getRequestInfo(), $data); + + $httpCode = $request->getRequestInfo('http_code'); + if ($httpCode == 200 || $httpCode == 201) { + return json_decode($data) ?: (object) ['success' => true]; + } + } + return false; + } + + /** + * Rename a server. + * + * @param int $serviceID + * @param string $newName + * @return bool + */ + public function renameServer($serviceID, $newName) + { + $serviceID = (int) $serviceID; + $newName = trim($newName); + + if (empty($newName) || strlen($newName) > 255) { + return false; + } + + $service = Database::getSystemService($serviceID); + + if ($service) { + $whmcsService = Database::getWhmcsService($serviceID); + $cp = $this->getCP($whmcsService->server); + $request = $this->initCurl($cp['token']); + + $request->addOption(CURLOPT_POSTFIELDS, json_encode(['name' => $newName])); + $data = $request->patch($cp['url'] . '/servers/' . (int) $service->server_id . '/name'); + + Log::insert(__FUNCTION__, $request->getRequestInfo(), $data); + + $httpCode = $request->getRequestInfo('http_code'); + return ($httpCode == 200 || $httpCode == 204); + } + return false; + } + + /** + * Fetch available OS templates for a server's package. + * + * @param int $serviceID + * @return array|false + */ + public function fetchOsTemplates($serviceID) + { + $serviceID = (int) $serviceID; + $service = Database::getSystemService($serviceID); + + if ($service) { + $whmcsService = Database::getWhmcsService($serviceID); + $cp = $this->getCP($whmcsService->server); + + $product = \WHMCS\Database\Capsule::table('tblproducts')->where('id', $whmcsService->packageid)->first(); + if (!$product || !$product->configoption2) { + return false; + } + + $request = $this->initCurl($cp['token']); + $data = $request->get($cp['url'] . '/media/templates/fromServerPackageSpec/' . (int) $product->configoption2); + + Log::insert(__FUNCTION__, $request->getRequestInfo(), $data); + + if ($request->getRequestInfo('http_code') == '200') { + $templates = json_decode($data, true); + $result = []; + if (isset($templates['data'])) { + foreach ($templates['data'] as $osCategory) { + foreach ($osCategory['templates'] as $template) { + $result[] = [ + 'id' => $template['id'], + 'name' => $template['name'] . ' ' . $template['version'] . ' ' . $template['variant'], + ]; + } + } + usort($result, function ($a, $b) { + return strcmp($a['name'], $b['name']); + }); + } + return $result; + } + } + return false; + } + public function resetUserPassword($serviceID, $clientID) { + $serviceID = (int) $serviceID; + $clientID = (int) $clientID; $service = Database::getSystemService($serviceID); if ($service) { @@ -201,7 +366,7 @@ class Module return true; } - $this->output(['errors' => 'unauthenticated'], true, true, 200); + $this->output(['success' => false, 'errors' => 'unauthenticated'], true, true, 401); } /** @@ -213,7 +378,7 @@ class Module return true; } - $this->output(['errors' => 'unauthenticated'], true, true, 200); + $this->output(['success' => false, 'errors' => 'unauthenticated'], true, true, 401); } /** diff --git a/modules/servers/VirtFusionDirect/lib/ModuleFunctions.php b/modules/servers/VirtFusionDirect/lib/ModuleFunctions.php index 8f4f6d7..029f122 100644 --- a/modules/servers/VirtFusionDirect/lib/ModuleFunctions.php +++ b/modules/servers/VirtFusionDirect/lib/ModuleFunctions.php @@ -23,59 +23,49 @@ class ModuleFunctions extends Module try { /** - * - * If the service exists in the custom table, Cancel the create account action. - * + * If the service exists in the custom table, cancel the create account action. */ if (Database::checkSystemService($params['serviceid'])) { return 'Service already exists. You must run a termination first.'; } - /** - * * If no VirtFusionDirect control server exists, cancel the create account action. - * */ - $server = $params['serverid'] ?: false; $cp = $this->getCP($server, !$server); if (!$cp) { - return 'No Control server found.'; + return 'No Control server found. Please ensure a VirtFusion server is configured in WHMCS.'; } Log::insert(__FUNCTION__, $params, []); /** - * * Does a user account in VirtFusion match this account (byExtRelationId) in WHMCS. - * */ $request = $this->initCurl($cp['token']); - $data = $request->get($cp['url'] . '/users/' . $params['userid'] . '/byExtRelation'); + $data = $request->get($cp['url'] . '/users/' . (int) $params['userid'] . '/byExtRelation'); Log::insert(__FUNCTION__, $request->getRequestInfo(), $data); switch ($request->getRequestInfo('http_code')) { case 200: - /** - * * A user with relation ID exists in VirtFusion. We can provision under that account. - * */ break; case 404: - /** - * * A user doesn't exist in VirtFusion. We should attempt to create one. - * */ $user = Database::getUser($params['userid']); + if (!$user) { + return 'WHMCS user not found for ID ' . (int) $params['userid']; + } + $request = $this->initCurl($cp['token']); $request->addOption(CURLOPT_POSTFIELDS, json_encode( @@ -91,19 +81,17 @@ class ModuleFunctions extends Module Log::insert(__FUNCTION__, $request->getRequestInfo(), $data); if ($request->getRequestInfo('http_code') !== 201) { - return 'Unable to create user.'; + return 'Unable to create user in VirtFusion. API returned HTTP ' . $request->getRequestInfo('http_code'); } break; default: - return 'Error processing user account.'; + return 'Error processing user account. VirtFusion API returned HTTP ' . $request->getRequestInfo('http_code'); } $data = json_decode($data); /** - * * A user is available. We can now attempt to create a server. - * */ $configOptionDefaultNaming = [ @@ -123,26 +111,27 @@ class ModuleFunctions extends Module $configOptionCustomNaming = []; if (file_exists(ROOTDIR . '/modules/servers/VirtFusionDirect/config/ConfigOptionMapping.php')) { - $configOptionCustomNaming = require_once ROOTDIR . '/modules/servers/VirtFusionDirect/config/ConfigOptionMapping.php'; + $configOptionCustomNaming = require ROOTDIR . '/modules/servers/VirtFusionDirect/config/ConfigOptionMapping.php'; } $options = [ - "packageId" => $params['configoption2'], + "packageId" => (int) $params['configoption2'], "userId" => $data->data->id, - "hypervisorId" => $params['configoption1'], - "ipv4" => $params['configoption3'], + "hypervisorId" => (int) $params['configoption1'], + "ipv4" => (int) $params['configoption3'], ]; if (array_key_exists('configoptions', $params)) { foreach ($configOptionDefaultNaming as $key => $option) { $currentOption = array_key_exists($key, $configOptionCustomNaming) ? $configOptionCustomNaming[$key] : $option; if (array_key_exists($currentOption, $params['configoptions'])) { - // If the option key is "Memory" and the value is less than 1024, we need to convert it to MB + $value = $params['configoptions'][$currentOption]; + // If the option key is "Memory" and the value is less than 1024, convert to MB // VirtFusion expects memory in MB. - if ($currentOption === 'Memory' && $params['configoptions'][$currentOption] < 1024) { - $options[$key] = $params['configoptions'][$currentOption] * 1024; + if ($key === 'memory' && is_numeric($value) && $value < 1024) { + $options[$key] = (int) ($value * 1024); } else { - $options[$key] = $params['configoptions'][$currentOption]; + $options[$key] = is_numeric($value) ? (int) $value : $value; } } } @@ -166,17 +155,15 @@ class ModuleFunctions extends Module $cs = new ConfigureService(); $cs->initServerBuild($data->data->id, $params); - /** - * - * Server was created successfully. - * - */ return 'success'; } else { - if ($data->errors[0]) { + if (isset($data->errors) && is_array($data->errors) && isset($data->errors[0])) { return $data->errors[0]; } - return 'Unknown error.'; + if (isset($data->msg)) { + return $data->msg; + } + return 'Server creation failed. VirtFusion API returned HTTP ' . $request->getRequestInfo('http_code'); } } catch (\Exception $e) { Log::insert(__FUNCTION__, $params, $e->getMessage()); @@ -184,16 +171,10 @@ class ModuleFunctions extends Module } } - // This function was implemented by Zander Scott / awildboop of Blinkoh, LLC - // Please read this function thoroughly before use to ensure security & integrity - /** * Allows changing of the package of a server * - * @author https://github.com/BlinkohHost/virtfusion-whmcs-module - * * @param $params - * * @return string */ public function changePackage($params) @@ -204,7 +185,7 @@ class ModuleFunctions extends Module $whmcsService = Database::getWhmcsService($params['serviceid']); $cp = $this->getCP($whmcsService->server); $request = $this->initCurl($cp['token']); - $data = $request->put($cp['url'] . '/servers/' . $service->server_id . '/package/' . $params['configoption2']); + $data = $request->put($cp['url'] . '/servers/' . (int) $service->server_id . '/package/' . (int) $params['configoption2']); $data = json_decode($data); Log::insert(__FUNCTION__, $request->getRequestInfo(), $data); @@ -214,17 +195,17 @@ class ModuleFunctions extends Module case 204: return 'success'; case 404: - return '404 was returned from the web service without the msg property. The service may be currently unavailable.'; + return 'The server or package was not found in VirtFusion (HTTP 404).'; case 423: - if (property_exists($data, 'msg')) { + if (isset($data->msg)) { return $data->msg; } - break; + return 'The server is currently locked. Please try again later.'; default: - return 'Update package request failed. The web service reported HTTP code ' . $request->getRequestInfo('http_code'); + return 'Update package request failed. VirtFusion API returned HTTP ' . $request->getRequestInfo('http_code'); } } - return 'Service not found.'; + return 'Service not found in module database.'; } /** @@ -247,7 +228,7 @@ class ModuleFunctions extends Module $cp = $this->getCP($whmcsService->server); $request = $this->initCurl($cp['token']); - $data = $request->delete($cp['url'] . '/servers/' . $service->server_id); + $data = $request->delete($cp['url'] . '/servers/' . (int) $service->server_id); $data = json_decode($data); Log::insert(__FUNCTION__, $request->getRequestInfo(), $data); @@ -260,22 +241,22 @@ class ModuleFunctions extends Module return 'success'; case 404: - if (property_exists($data, 'msg')) { + if (isset($data->msg)) { if ($data->msg == 'server not found') { Database::deleteSystemService($params['serviceid']); return 'success'; } else { - return '404 was returned from the web service with the msg property but doesn\'t contain appropriate data to process a termination.'; + return 'VirtFusion returned 404: ' . $data->msg; } } else { - return '404 was returned from the web service without the msg property. The service may be currently unavailable.'; + return 'VirtFusion returned 404 without details. The API may be unavailable.'; } default: - return 'Termination request failed. The web service reported HTTP code ' . $request->getRequestInfo('http_code'); + return 'Termination request failed. VirtFusion API returned HTTP ' . $request->getRequestInfo('http_code'); } } - return 'Service not found. Termination routine has already been run?'; + return 'Service not found in module database. Has termination already been run?'; } /** @@ -297,7 +278,7 @@ class ModuleFunctions extends Module $cp = $this->getCP($whmcsService->server); $request = $this->initCurl($cp['token']); - $data = $request->post($cp['url'] . '/servers/' . $service->server_id . '/suspend'); + $data = $request->post($cp['url'] . '/servers/' . (int) $service->server_id . '/suspend'); $data = json_decode($data); Log::insert(__FUNCTION__, $request->getRequestInfo(), $data); @@ -308,27 +289,27 @@ class ModuleFunctions extends Module return 'success'; case 404: - if (property_exists($data, 'msg')) { + if (isset($data->msg)) { if ($data->msg == 'server not found') { Database::deleteSystemService($params['serviceid']); return 'success'; } else { - - return '404 was returned from the web service with the msg property but doesn\'t contain appropriate data to process a suspension.'; + return 'VirtFusion returned 404: ' . $data->msg; } } else { - return '404 was returned from the web service without the msg property. The service may be currently unavailable.'; + return 'VirtFusion returned 404 without details. The API may be unavailable.'; } case 423: - if (property_exists($data, 'msg')) { + if (isset($data->msg)) { return $data->msg; } + return 'The server is currently locked. Please try again later.'; default: - return 'Suspend request failed. The web service reported HTTP code ' . $request->getRequestInfo('http_code'); + return 'Suspend request failed. VirtFusion API returned HTTP ' . $request->getRequestInfo('http_code'); } } - return 'Service not found.'; + return 'Service not found in module database.'; } function updateServerObject($params) @@ -341,7 +322,7 @@ class ModuleFunctions extends Module $cp = $this->getCP($whmcsService->server); $request = $this->initCurl($cp['token']); - $data = $request->get($cp['url'] . '/servers/' . $service->server_id); + $data = $request->get($cp['url'] . '/servers/' . (int) $service->server_id); $data = json_decode($data); Log::insert(__FUNCTION__, $request->getRequestInfo(), $data); @@ -355,10 +336,10 @@ class ModuleFunctions extends Module return 'success'; default: - return 'Request failed. The web service reported HTTP code ' . $request->getRequestInfo('http_code'); + return 'Request failed. VirtFusion API returned HTTP ' . $request->getRequestInfo('http_code'); } } - return 'Service not found.'; + return 'Service not found in module database.'; } @@ -371,7 +352,7 @@ class ModuleFunctions extends Module $cp = $this->getCP($whmcsService->server); $request = $this->initCurl($cp['token']); - $data = $request->post($cp['url'] . '/servers/' . $service->server_id . '/unsuspend'); + $data = $request->post($cp['url'] . '/servers/' . (int) $service->server_id . '/unsuspend'); $data = json_decode($data); Log::insert(__FUNCTION__, $request->getRequestInfo(), $data); @@ -382,27 +363,27 @@ class ModuleFunctions extends Module return 'success'; case 404: - if (property_exists($data, 'msg')) { + if (isset($data->msg)) { if ($data->msg == 'server not found') { Database::deleteSystemService($params['serviceid']); return 'success'; } else { - return '404 was returned from the web service with the msg property but doesn\'t contain appropriate data to process an unsuspension.'; + return 'VirtFusion returned 404: ' . $data->msg; } } else { - return '404 was returned from the web service without the msg property. The service may be currently unavailable.'; + return 'VirtFusion returned 404 without details. The API may be unavailable.'; } case 423: - if (property_exists($data, 'msg')) { + if (isset($data->msg)) { return $data->msg; } - break; + return 'The server is currently locked. Please try again later.'; default: - return 'Unsuspend request failed. The web service reported HTTP code ' . $request->getRequestInfo('http_code'); + return 'Unsuspend request failed. VirtFusion API returned HTTP ' . $request->getRequestInfo('http_code'); } } - return 'Service not found'; + return 'Service not found in module database.'; } public function adminServicesTabFields($params) @@ -432,12 +413,13 @@ class ModuleFunctions extends Module public function adminServicesTabFieldsSave($params) { - - if ($_POST['modulefields'][0] == '') { + if (!isset($_POST['modulefields'][0]) || $_POST['modulefields'][0] === '') { Database::deleteSystemService($params['serviceid']); } else { - - Database::updateSystemServiceServerId($params['serviceid'], $_POST['modulefields'][0]); + $serverId = (int) $_POST['modulefields'][0]; + if ($serverId > 0) { + Database::updateSystemServiceServerId($params['serviceid'], $serverId); + } } } diff --git a/modules/servers/VirtFusionDirect/lib/ServerResource.php b/modules/servers/VirtFusionDirect/lib/ServerResource.php index 7c9f2fa..a5e1e81 100644 --- a/modules/servers/VirtFusionDirect/lib/ServerResource.php +++ b/modules/servers/VirtFusionDirect/lib/ServerResource.php @@ -8,33 +8,54 @@ class ServerResource { $server = json_decode(json_encode($data->data), true); - $traffic = '∞'; + $traffic = '-'; - if ($server['settings']['resources']['traffic']) { + if (isset($server['settings']['resources']['traffic'])) { if ($server['settings']['resources']['traffic'] > 0) { $traffic = $server['settings']['resources']['traffic'] . ' GB'; + } else { + $traffic = 'Unlimited'; } } + $trafficUsed = '-'; + if (isset($server['usage']['traffic']['used'])) { + $trafficUsed = round($server['usage']['traffic']['used'] / 1073741824, 2) . ' GB'; + } + $data = [ 'name' => $server['name'] ?: '-', 'hostname' => $server['hostname'] ?: '-', - 'memory' => $server['settings']['resources']['memory'] . ' MB', + 'memory' => isset($server['settings']['resources']['memory']) ? $server['settings']['resources']['memory'] . ' MB' : '-', 'traffic' => $traffic, - 'storage' => $server['settings']['resources']['storage'] . ' GB', - 'cpu' => $server['settings']['resources']['cpuCores'] . ' Core(s)', + 'trafficUsed' => $trafficUsed, + 'storage' => isset($server['settings']['resources']['storage']) ? $server['settings']['resources']['storage'] . ' GB' : '-', + 'cpu' => isset($server['settings']['resources']['cpuCores']) ? $server['settings']['resources']['cpuCores'] . ' Core(s)' : '-', + 'status' => isset($server['state']) ? $server['state'] : 'unknown', + 'powerStatus' => isset($server['hypervisor']['settings']['state']) ? $server['hypervisor']['settings']['state'] : 'unknown', + 'username' => isset($server['owner']['email']) ? $server['owner']['email'] : '', + 'password' => '', 'primaryNetwork' => [ 'ipv4' => ['-'], 'ipv4Unformatted' => [], 'ipv6' => ['-'], 'ipv6Unformatted' => [], - ] + 'mac' => '-', + ], + 'networkSpeed' => [ + 'inbound' => isset($server['settings']['resources']['networkSpeedInbound']) ? $server['settings']['resources']['networkSpeedInbound'] . ' Mbps' : '-', + 'outbound' => isset($server['settings']['resources']['networkSpeedOutbound']) ? $server['settings']['resources']['networkSpeedOutbound'] . ' Mbps' : '-', + ], ]; if (array_key_exists('network', $server)) { if (array_key_exists('interfaces', $server['network'])) { if (count($server['network']['interfaces'])) { + if (isset($server['network']['interfaces'][0]['mac'])) { + $data['primaryNetwork']['mac'] = $server['network']['interfaces'][0]['mac']; + } + if (count($server['network']['interfaces'][0]['ipv4'])) { $data['primaryNetwork']['ipv4'] = []; foreach ($server['network']['interfaces'][0]['ipv4'] as $ip) { @@ -59,4 +80,4 @@ class ServerResource return $data; } -} \ No newline at end of file +} diff --git a/modules/servers/VirtFusionDirect/templates/css/module.css b/modules/servers/VirtFusionDirect/templates/css/module.css index 5972d3e..1cb4ae1 100644 --- a/modules/servers/VirtFusionDirect/templates/css/module.css +++ b/modules/servers/VirtFusionDirect/templates/css/module.css @@ -1 +1,134 @@ -.vf-bold{font-weight:800}.vf-small{font-size:.9rem}.vf-button{font-size:.8rem;padding:.95rem 1.5rem;font-weight:600}.vf-button-small{font-size:.8rem;padding:.75rem 1.3rem;font-weight:500}.vf-spinner-margin{margin-right:7px}.vf-badge{font-size:.8rem;padding:.5rem .9rem;text-transform:uppercase;font-weight:800}.vf-badge-active{background-color:rgba(32,177,0,.12);color:#276900;border-radius:6px}.vf-badge-awaiting{background-color:rgba(177,89,0,.12);color:#692000;border-radius:6px}#vf-login-button-spinner{display:none}#vf-password-reset-button-spinner{display:none}#vf-password-reset-error{display:none}#vf-password-reset-success{display:none}#vf-login-error{display:none}#vf-server-info{display:none}#vf-server-info-error{display:none}#vf-server-info-loader{min-height:136px}#vf-loading{display:inline-block;width:30px;height:30px;border:3px solid rgba(225,224,224,.3);border-radius:50%;border-top-color:#0e151a;animation:vf-spin 1s ease-in-out infinite;-webkit-animation:vf-spin 1s ease-in-out infinite}.vf-loader{margin:30px}@keyframes vf-spin{to{transform:rotate(360deg)}}@-webkit-keyframes vf-spin{to{transform:rotate(360deg)}}#vf-server-info-error{margin:10px} \ No newline at end of file +/* VirtFusion Direct Provisioning Module Styles */ + +/* Typography */ +.vf-bold { + font-weight: 800; +} +.vf-small { + font-size: 0.9rem; +} + +/* Buttons */ +.vf-button { + font-size: 0.8rem; + padding: 0.95rem 1.5rem; + font-weight: 600; +} +.vf-button-small { + font-size: 0.8rem; + padding: 0.75rem 1.3rem; + font-weight: 500; +} +.vf-spinner-margin { + margin-right: 7px; +} + +/* Status Badges */ +.vf-badge { + font-size: 0.75rem; + padding: 0.35rem 0.75rem; + text-transform: uppercase; + font-weight: 700; + border-radius: 6px; + display: inline-block; +} +.vf-badge-active { + background-color: rgba(32, 177, 0, 0.12); + color: #276900; +} +.vf-badge-awaiting { + background-color: rgba(177, 89, 0, 0.12); + color: #692000; +} +.vf-badge-suspended { + background-color: rgba(220, 53, 69, 0.12); + color: #721c24; +} + +/* Power Management */ +.vf-power-buttons { + display: flex; + flex-wrap: wrap; + gap: 8px; +} +.vf-btn-power { + min-width: 100px; + font-weight: 600; + text-transform: uppercase; + font-size: 0.8rem; + padding: 0.5rem 1rem; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 5px; +} + +/* Hidden elements (initial state) */ +#vf-login-button-spinner { + display: none; +} +#vf-password-reset-button-spinner { + display: none; +} +#vf-password-reset-error { + display: none; +} +#vf-password-reset-success { + display: none; +} +#vf-login-error { + display: none; +} +#vf-server-info { + display: none; +} +#vf-server-info-error { + display: none; +} +#vf-data-server-traffic-sep { + display: inline; +} + +/* Loader */ +#vf-server-info-loader { + min-height: 136px; +} +#vf-loading { + display: inline-block; + width: 30px; + height: 30px; + border: 3px solid rgba(225, 224, 224, 0.3); + border-radius: 50%; + border-top-color: #0e151a; + animation: vf-spin 1s ease-in-out infinite; + -webkit-animation: vf-spin 1s ease-in-out infinite; +} +.vf-loader { + margin: 30px; +} + +@keyframes vf-spin { + to { + transform: rotate(360deg); + } +} +@-webkit-keyframes vf-spin { + to { + transform: rotate(360deg); + } +} + +/* Error message spacing */ +#vf-server-info-error { + margin: 10px; +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + .vf-power-buttons { + flex-direction: column; + } + .vf-btn-power { + width: 100%; + } +} diff --git a/modules/servers/VirtFusionDirect/templates/error.tpl b/modules/servers/VirtFusionDirect/templates/error.tpl index 8898d2f..ce5ff44 100644 --- a/modules/servers/VirtFusionDirect/templates/error.tpl +++ b/modules/servers/VirtFusionDirect/templates/error.tpl @@ -1 +1,11 @@ -

Oops! Something went wrong.

Please go back and try again.

If the problem persists, please contact support.

\ No newline at end of file +
+
+

Error

+
+
+
+

Something went wrong.

+

Please go back and try again. If the problem persists, please contact support.

+
+
+
diff --git a/modules/servers/VirtFusionDirect/templates/js/module.js b/modules/servers/VirtFusionDirect/templates/js/module.js index 21b6c06..4e3958a 100644 --- a/modules/servers/VirtFusionDirect/templates/js/module.js +++ b/modules/servers/VirtFusionDirect/templates/js/module.js @@ -1 +1,261 @@ -function vfServerData(e,r){$("#vf-server-info-error").hide(),$.ajax({type:"GET",dataType:"json",url:r+"modules/servers/VirtFusionDirect/client.php?serviceID="+e+"&action=serverData"}).done(function(e){e.success?($("#vf-data-server-name").text(e.data.name),$("#vf-data-server-hostname").text(e.data.hostname),$("#vf-data-server-memory").text(e.data.memory),$("#vf-data-server-traffic").text(e.data.traffic),$("#vf-data-server-storage").text(e.data.storage),$("#vf-data-server-cpu").text(e.data.cpu),$("#vf-data-server-ipv4").text(e.data.primaryNetwork.ipv4),$("#vf-data-server-ipv6").text(e.data.primaryNetwork.ipv6),$("#vf-server-info").show()):($("#vf-server-info-error").show(),$("#vf-server-info").hide())}).fail(function(e){}).always(function(e){$("#vf-server-info-loader-container").hide()})}function vfServerDataAdmin(e,r){$("#vf-loader").show(),$("#vf-server-info").hide(),$("#vf-server-info-error").hide(),$.ajax({type:"GET",dataType:"json",url:r+"modules/servers/VirtFusionDirect/admin.php?serviceID="+e+"&action=serverData"}).done(function(e){e.success?($("#vf-data-server-name").text(e.data.name),$("#vf-data-server-hostname").text(e.data.hostname),$("#vf-data-server-memory").text(e.data.memory),$("#vf-data-server-traffic").text(e.data.traffic),$("#vf-data-server-storage").text(e.data.storage),$("#vf-data-server-cpu").text(e.data.cpu),$("#vf-data-server-ipv4").text(e.data.primaryNetwork.ipv4),$("#vf-data-server-ipv6").text(e.data.primaryNetwork.ipv6),$("#vf-server-info").show()):($("#vf-server-info-error").show(),$("#vf-server-info-error-message").text(e.errors),$("#vf-server-info").hide())}).fail(function(e){}).always(function(e){$("#vf-loader").hide()})}function vfUserPasswordReset(e,r){$("#vf-password-reset-button-spinner").show(),$("#vf-password-reset-error").hide(),$("#vf-password-reset-success").hide(),$.ajax({type:"GET",dataType:"json",url:r+"modules/servers/VirtFusionDirect/client.php?serviceID="+e+"&action=resetPassword"}).done(function(e){e.success?($("#vf-password-reset-success").show(),$("#vf-data-user-email").text(e.data.email),$("#vf-data-user-password").text(e.data.password),console.log(e.data.email)):$("#vf-password-reset-error").show()}).fail(function(e){}).always(function(e){$("#vf-password-reset-button-spinner").hide()})}function vfLoginAsServerOwner(e,r,t=!0){vfLoginError(!1),$("#vf-login-button").prop("disabled",!0),$("#vf-login-button-spinner").show(),$.ajax({type:"GET",dataType:"json",url:r+"modules/servers/VirtFusionDirect/client.php?serviceID="+e+"&action=loginAsServerOwner"}).done(function(e){e.success?e.token_url&&(t?window.open(e.token_url):window.location.href=e.token_url):vfLoginError(!0)}).fail(function(e){vfLoginError(!0)}).always(function(e){$("#vf-login-button-spinner").hide(),$("#vf-login-button").prop("disabled",!1)})}function vfLoginError(e,r="Oops! Something went wrong. Try again later."){e?($("#vf-login-error").text(r),$("#vf-login-error").show()):$("#vf-login-error").hide()}function impersonateServerOwner(e,r){$.ajax({type:"GET",dataType:"json",url:r+"modules/servers/VirtFusionDirect/admin.php?serviceID="+e+"&action=impersonateServerOwner"}).done(function(e){e.success&&e.user&&window.open(e.url+"/_imp/in/"+e.user.id+"/-")}).fail(function(e){}).always(function(e){})} \ No newline at end of file +/** + * VirtFusion Direct Provisioning Module - Client JavaScript + * + * Handles client-side interactions for server management including: + * - Server data display + * - Power management (boot, shutdown, restart, power off) + * - Control panel login (SSO) + * - Password reset + * - Server rebuild + * - OS template loading + */ + +function vfServerData(serviceId, systemUrl) { + $("#vf-server-info-error").hide(); + $.ajax({ + type: "GET", + dataType: "json", + url: systemUrl + "modules/servers/VirtFusionDirect/client.php?serviceID=" + encodeURIComponent(serviceId) + "&action=serverData" + }).done(function (response) { + if (response.success) { + $("#vf-data-server-name").text(response.data.name); + $("#vf-data-server-hostname").text(response.data.hostname); + $("#vf-data-server-memory").text(response.data.memory); + $("#vf-data-server-traffic").text(response.data.traffic); + $("#vf-data-server-traffic-used").text(response.data.trafficUsed || "-"); + $("#vf-data-server-storage").text(response.data.storage); + $("#vf-data-server-cpu").text(response.data.cpu); + $("#vf-data-server-ipv4").text(response.data.primaryNetwork.ipv4); + $("#vf-data-server-ipv6").text(response.data.primaryNetwork.ipv6); + + // Update status badge + var statusBadge = $("#vf-status-badge"); + var status = (response.data.status || "unknown").toLowerCase(); + statusBadge.text(status.charAt(0).toUpperCase() + status.slice(1)); + if (status === "active" || status === "running") { + statusBadge.addClass("vf-badge-active"); + } else if (status === "suspended") { + statusBadge.addClass("vf-badge-suspended"); + } else { + statusBadge.addClass("vf-badge-awaiting"); + } + + $("#vf-server-info").show(); + } else { + $("#vf-server-info-error").show(); + $("#vf-server-info").hide(); + } + }).fail(function () { + $("#vf-server-info-error").show(); + }).always(function () { + $("#vf-server-info-loader-container").hide(); + }); +} + +function vfServerDataAdmin(serviceId, systemUrl) { + $("#vf-loader").show(); + $("#vf-server-info").hide(); + $("#vf-server-info-error").hide(); + $.ajax({ + type: "GET", + dataType: "json", + url: systemUrl + "modules/servers/VirtFusionDirect/admin.php?serviceID=" + encodeURIComponent(serviceId) + "&action=serverData" + }).done(function (response) { + if (response.success) { + $("#vf-data-server-name").text(response.data.name); + $("#vf-data-server-hostname").text(response.data.hostname); + $("#vf-data-server-memory").text(response.data.memory); + $("#vf-data-server-traffic").text(response.data.traffic); + $("#vf-data-server-storage").text(response.data.storage); + $("#vf-data-server-cpu").text(response.data.cpu); + $("#vf-data-server-ipv4").text(response.data.primaryNetwork.ipv4); + $("#vf-data-server-ipv6").text(response.data.primaryNetwork.ipv6); + $("#vf-server-info").show(); + } else { + $("#vf-server-info-error").show(); + $("#vf-server-info-error-message").text(response.errors); + $("#vf-server-info").hide(); + } + }).fail(function () { + $("#vf-server-info-error").show(); + }).always(function () { + $("#vf-loader").hide(); + }); +} + +function vfUserPasswordReset(serviceId, systemUrl) { + $("#vf-password-reset-button-spinner").show(); + $("#vf-password-reset-error").hide(); + $("#vf-password-reset-success").hide(); + $.ajax({ + type: "GET", + dataType: "json", + url: systemUrl + "modules/servers/VirtFusionDirect/client.php?serviceID=" + encodeURIComponent(serviceId) + "&action=resetPassword" + }).done(function (response) { + if (response.success) { + $("#vf-password-reset-success").show(); + $("#vf-data-user-email").text(response.data.email); + $("#vf-data-user-password").text(response.data.password); + } else { + $("#vf-password-reset-error").show(); + } + }).fail(function () { + $("#vf-password-reset-error").show(); + }).always(function () { + $("#vf-password-reset-button-spinner").hide(); + }); +} + +function vfLoginAsServerOwner(serviceId, systemUrl, newWindow) { + newWindow = newWindow !== false; + vfLoginError(false); + $("#vf-login-button").prop("disabled", true); + $("#vf-login-button-spinner").show(); + $.ajax({ + type: "GET", + dataType: "json", + url: systemUrl + "modules/servers/VirtFusionDirect/client.php?serviceID=" + encodeURIComponent(serviceId) + "&action=loginAsServerOwner" + }).done(function (response) { + if (response.success && response.token_url) { + if (newWindow) { + window.open(response.token_url); + } else { + window.location.href = response.token_url; + } + } else { + vfLoginError(true); + } + }).fail(function () { + vfLoginError(true); + }).always(function () { + $("#vf-login-button-spinner").hide(); + $("#vf-login-button").prop("disabled", false); + }); +} + +function vfLoginError(show, message) { + message = message || "Unable to open the control panel. Please try again later."; + if (show) { + $("#vf-login-error").text(message); + $("#vf-login-error").show(); + } else { + $("#vf-login-error").hide(); + } +} + +function vfPowerAction(serviceId, systemUrl, action) { + var btn = $("#vf-power-" + action); + var spinner = btn.find(".vf-btn-spinner"); + var alertDiv = $("#vf-power-alert"); + + // Disable all power buttons during action + $(".vf-btn-power").prop("disabled", true); + spinner.show(); + alertDiv.hide(); + + var actionLabels = { + boot: "Starting", + shutdown: "Shutting down", + restart: "Restarting", + poweroff: "Forcing off" + }; + + $.ajax({ + type: "GET", + dataType: "json", + url: systemUrl + "modules/servers/VirtFusionDirect/client.php?serviceID=" + encodeURIComponent(serviceId) + "&action=powerAction&powerAction=" + encodeURIComponent(action) + }).done(function (response) { + if (response.success) { + alertDiv.removeClass("alert-danger").addClass("alert-success"); + alertDiv.text(response.data.message || (actionLabels[action] + " server...")); + } else { + alertDiv.removeClass("alert-success").addClass("alert-danger"); + alertDiv.text(response.errors || "Power action failed."); + } + alertDiv.show(); + }).fail(function () { + alertDiv.removeClass("alert-success").addClass("alert-danger"); + alertDiv.text("An error occurred. Please try again."); + alertDiv.show(); + }).always(function () { + spinner.hide(); + $(".vf-btn-power").prop("disabled", false); + }); +} + +function vfLoadOsTemplates(serviceId, systemUrl) { + $.ajax({ + type: "GET", + dataType: "json", + url: systemUrl + "modules/servers/VirtFusionDirect/client.php?serviceID=" + encodeURIComponent(serviceId) + "&action=osTemplates" + }).done(function (response) { + var select = $("#vf-rebuild-os"); + select.empty(); + if (response.success && response.data && response.data.length > 0) { + select.append(''); + $.each(response.data, function (i, template) { + select.append(''); + }); + } else { + select.append(''); + } + }).fail(function () { + var select = $("#vf-rebuild-os"); + select.empty(); + select.append(''); + }); +} + +function vfRebuildServer(serviceId, systemUrl) { + var osId = $("#vf-rebuild-os").val(); + var alertDiv = $("#vf-rebuild-alert"); + + if (!osId) { + alertDiv.removeClass("alert-success").addClass("alert-danger"); + alertDiv.text("Please select an operating system."); + alertDiv.show(); + return; + } + + if (!confirm("Are you sure you want to rebuild this server? ALL DATA WILL BE ERASED. This action cannot be undone.")) { + return; + } + + $("#vf-rebuild-button").prop("disabled", true); + $("#vf-rebuild-spinner").show(); + alertDiv.hide(); + + $.ajax({ + type: "GET", + dataType: "json", + url: systemUrl + "modules/servers/VirtFusionDirect/client.php?serviceID=" + encodeURIComponent(serviceId) + "&action=rebuild&osId=" + encodeURIComponent(osId) + }).done(function (response) { + if (response.success) { + alertDiv.removeClass("alert-danger").addClass("alert-success"); + alertDiv.text(response.data.message || "Server rebuild initiated. You will receive an email when the process is complete."); + } else { + alertDiv.removeClass("alert-success").addClass("alert-danger"); + alertDiv.text(response.errors || "Rebuild failed."); + } + alertDiv.show(); + }).fail(function () { + alertDiv.removeClass("alert-success").addClass("alert-danger"); + alertDiv.text("An error occurred. Please try again."); + alertDiv.show(); + }).always(function () { + $("#vf-rebuild-spinner").hide(); + $("#vf-rebuild-button").prop("disabled", false); + }); +} + +function impersonateServerOwner(serviceId, systemUrl) { + $.ajax({ + type: "GET", + dataType: "json", + url: systemUrl + "modules/servers/VirtFusionDirect/admin.php?serviceID=" + encodeURIComponent(serviceId) + "&action=impersonateServerOwner" + }).done(function (response) { + if (response.success && response.user) { + window.open(response.url + "/_imp/in/" + response.user.id + "/-"); + } + }); +} diff --git a/modules/servers/VirtFusionDirect/templates/overview.tpl b/modules/servers/VirtFusionDirect/templates/overview.tpl index bdd968d..ed3368e 100644 --- a/modules/servers/VirtFusionDirect/templates/overview.tpl +++ b/modules/servers/VirtFusionDirect/templates/overview.tpl @@ -1 +1,223 @@ -{if $serviceStatus eq 'Active'}

Server Overview

Information unavailable. Try again later.
Name:
Hostname:
Memory:
CPU:
IPv4:
IPv6:
Storage:
Traffic:

Manage

Manage your server via our dedicated control panel. You will be automatically authenticated and the control panel will open in a new window.

Having trouble opening the control panel in a new window? Click here to open in this window.

{if $serverHostname}

Oops! Something went wrong. Try again later.
Your new login credentials. These will only be displayed once.
Email:
Password:

Alternatively you may directly access the control panel at {$serverHostname}

{/if}
{/if}

Billing Overview

Product:
{$groupname} - {$product}
{$LANG.recurringamount}:
{$recurringamount}
{$LANG.orderbillingcycle}:
{$billingcycle}
{$LANG.clientareahostingregdate}:
{$regdate}
{$LANG.clientareahostingnextduedate}:
{$nextduedate}
{$LANG.orderpaymentmethod}:
{$paymentmethod}
\ No newline at end of file + + + +{if $serviceStatus eq 'Active'} + +{* Server Overview Panel *} +
+
+

+ Server Overview + +

+
+
+
+
+
+
+
+ +
+
Information unavailable. Try again later.
+
+
+
+
+
+
+
Name:
+
+
+
+
Hostname:
+
+
+
+
Memory:
+
+
+
+
CPU:
+
+
+
+
+
+
IPv4:
+
+
+
+
IPv6:
+
+
+
+
Storage:
+
+
+
+
Traffic:
+
+ + / + +
+
+
+
+
+
+
+
+ +{* Power Management Panel *} +
+
+

Power Management

+
+
+ +
+
+
+ + + + +
+
+
+
+
+ +{* Manage Panel *} +
+
+

Manage

+
+
+
+
+
+

Manage your server via our dedicated control panel. You will be automatically authenticated and the control panel will open in a new window.

+ +
+
+

Having trouble opening the control panel in a new window? Click here to open in this window.

+
+ {if $serverHostname} +
+
+
Oops! Something went wrong. Try again later.
+
+
Your new login credentials. These will only be displayed once.
+
Email:
+
Password:
+
+

Alternatively you may directly access the control panel at {$serverHostname|escape:'htmlall'}

+ +
+ {/if} +
+
+
+ +{* Rebuild Panel *} +
+
+

Rebuild Server

+
+
+ +
+ Warning: Rebuilding your server will erase all data on the server and reinstall the operating system. This action cannot be undone. +
+
+
+
+ + +
+
+
+ + +
+
+ +{elseif $serviceStatus eq 'Suspended'} + +
+
+

Service Suspended

+
+
+
+ Your service is currently suspended. Please contact support or pay any outstanding invoices to restore access. +
+
+
+ +{/if} + +{* Billing Overview - Always visible *} +
+
+

Billing Overview

+
+
+
+
+
+
Product:
+
{$groupname} - {$product}
+
+
+
{$LANG.recurringamount}:
+
{$recurringamount}
+
+
+
{$LANG.orderbillingcycle}:
+
{$billingcycle}
+
+
+
+
+
{$LANG.clientareahostingregdate}:
+
{$regdate}
+
+
+
{$LANG.clientareahostingnextduedate}:
+
{$nextduedate}
+
+
+
{$LANG.orderpaymentmethod}:
+
{$paymentmethod}
+
+
+
+
+