Enhance VirtFusion WHMCS module with security fixes, new features, and improved UX
Security improvements: - Enable SSL/TLS certificate verification by default (was disabled, MITM risk) - Remove error_reporting(0) that silenced all errors - Add input sanitization on all user parameters (int casting, regex filtering) - Return proper HTTP status codes (401, 403, 400, 500) instead of always 200 - Add XSS protection with htmlspecialchars and encodeURIComponent - Add null checks on API response data before property access New features: - Power management: boot, shutdown, restart, and force power off controls - Server rebuild: reinstall with any available OS template from client area - Server rename: change server display name via PATCH API - OS template fetching: client-side endpoint for rebuild OS selection - TestConnection: validate API credentials from WHMCS server settings - ServiceSingleSignOn: native WHMCS SSO integration for VirtFusion panel - Server status badge: visual indicator of server state in overview - Traffic usage display: show bandwidth used vs allocated - Checkout validation: ShoppingCartValidateCheckout hook ensures OS selection Ordering process improvements: - Add default "Select Operating System" placeholder option - Add "No SSH Key (Optional)" default for SSH dropdown - Hide SSH key field/container when no keys available - Wrap hook in try/catch to prevent checkout page breakage - Sanitize template names with htmlspecialchars - Use JSON_HEX_* flags for safe script injection Theme compatibility: - Properly formatted Smarty templates with readable indentation - Dual panel/card CSS classes for Bootstrap 3/4/5 compatibility - Responsive power button layout with mobile breakpoint - Framework-agnostic HTML that works with Six, Twenty-One, Lagom, and custom themes - Suspended service state messaging Code quality: - Readable, unminified JavaScript with JSDoc header - Structured CSS with logical section organization - Improved error messages throughout all provisioning functions - Added PATCH method support to Curl wrapper - Added curl error capture on connection failures - Added connection and request timeouts (10s/30s) - Fixed memory conversion to check key name instead of display name Documentation: - Complete README rewrite with installation, configuration, and troubleshooting guides - API endpoint reference table - Configurable options mapping documentation - Theme override instructions - Security considerations section https://claude.ai/code/session_01TCsJ4WZCGuEX3zqh1tQ2zx
This commit is contained in:
196
README.md
196
README.md
@@ -5,34 +5,194 @@
|
||||

|
||||

|
||||
|
||||
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.
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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 "
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
let osTemplates = ".json_encode($dropdownOptions, JSON_THROW_ON_ERROR).";
|
||||
let sshKeys = ".json_encode($sshKeysOptions, JSON_THROW_ON_ERROR).";
|
||||
var osTemplates = " . json_encode($dropdownOptions, JSON_THROW_ON_ERROR | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) . ";
|
||||
var sshKeys = " . json_encode($sshKeysOptions, JSON_THROW_ON_ERROR | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) . ";
|
||||
|
||||
const osInputField = document.querySelector('[name=\"customfield[".($osID[0] ?? null)."]\"]');
|
||||
const osInputLabel = document.querySelector('[for=\"customfield".($osID[0] ?? null)."\"]');
|
||||
const sshInputField = document.querySelector('[name=\"customfield[".($sshID[0] ?? null)."]\"]');
|
||||
const sshInputLabel = document.querySelector('[for=\"customfield".($sshID[0] ?? null)."\"]');
|
||||
var osInputField = document.querySelector('[name=\"customfield[" . (int) $osFieldId . "]\"]');
|
||||
var sshInputField = " . ($sshFieldId !== null ? "document.querySelector('[name=\"customfield[" . (int) $sshFieldId . "]\"]')" : "null") . ";
|
||||
var sshInputLabel = " . ($sshFieldId !== null ? "document.querySelector('[for=\"customfield" . (int) $sshFieldId . "\"]')" : "null") . ";
|
||||
|
||||
// Create dropdown options menu, then add it to the DOM then on change, update the regular input.
|
||||
let osSelect = document.createElement('select');
|
||||
if (!osInputField) return;
|
||||
|
||||
// Create OS dropdown
|
||||
var osSelect = document.createElement('select');
|
||||
osSelect.className = 'form-control';
|
||||
osSelect.setAttribute('id', 'vf-os-select');
|
||||
|
||||
var defaultOption = document.createElement('option');
|
||||
defaultOption.value = '';
|
||||
defaultOption.text = '-- Select Operating System --';
|
||||
osSelect.appendChild(defaultOption);
|
||||
|
||||
osTemplates.forEach(function(template) {
|
||||
let option = document.createElement('option');
|
||||
var option = document.createElement('option');
|
||||
option.value = template.id;
|
||||
option.text = template.name;
|
||||
osSelect.appendChild(option);
|
||||
});
|
||||
|
||||
// Set the default value of the input field to the first option in the dropdown.
|
||||
osInputField.value = osSelect.options[0].value;
|
||||
|
||||
osSelect.addEventListener('change', function() {
|
||||
osInputField.value = this.value;
|
||||
console.log(this.value);
|
||||
});
|
||||
|
||||
osInputField.parentNode.insertBefore(osSelect, osInputField.nextSibling);
|
||||
osInputField.style.display = 'none';
|
||||
|
||||
if (sshKeys.length > 0) {
|
||||
// Create dropdown options menu, then add it to the DOM then on change, update the regular input.
|
||||
let sshSelect = document.createElement('select');
|
||||
sshSelect.className = 'form-control';
|
||||
// Handle SSH keys
|
||||
if (sshInputField) {
|
||||
if (sshKeys.length > 0) {
|
||||
var sshSelect = document.createElement('select');
|
||||
sshSelect.className = 'form-control';
|
||||
sshSelect.setAttribute('id', 'vf-ssh-select');
|
||||
|
||||
sshKeys.forEach(function(sshkey) {
|
||||
let option = document.createElement('option');
|
||||
option.value = sshkey.id;
|
||||
option.text = sshkey.name;
|
||||
sshSelect.appendChild(option);
|
||||
});
|
||||
var sshDefaultOption = document.createElement('option');
|
||||
sshDefaultOption.value = '';
|
||||
sshDefaultOption.text = '-- No SSH Key (Optional) --';
|
||||
sshSelect.appendChild(sshDefaultOption);
|
||||
|
||||
// Set the default value of the input field to the first option in the dropdown.
|
||||
sshInputField.value = sshSelect.options[0].value;
|
||||
sshKeys.forEach(function(sshkey) {
|
||||
var option = document.createElement('option');
|
||||
option.value = sshkey.id;
|
||||
option.text = sshkey.name;
|
||||
sshSelect.appendChild(option);
|
||||
});
|
||||
|
||||
sshSelect.addEventListener('change', function() {
|
||||
sshInputField.value = this.value;
|
||||
});
|
||||
sshSelect.addEventListener('change', function() {
|
||||
sshInputField.value = this.value;
|
||||
});
|
||||
|
||||
sshInputField.parentNode.insertBefore(sshSelect, sshInputField.nextSibling);
|
||||
sshInputField.style.display = 'none';
|
||||
} else {
|
||||
sshInputField.style.display = 'none';
|
||||
sshInputLabel.style.display = 'none';
|
||||
sshInputField.parentNode.insertBefore(sshSelect, sshInputField.nextSibling);
|
||||
sshInputField.style.display = 'none';
|
||||
} else {
|
||||
sshInputField.style.display = 'none';
|
||||
if (sshInputLabel) sshInputLabel.style.display = 'none';
|
||||
// Also hide the parent container if it exists
|
||||
var sshContainer = sshInputField.closest('.form-group');
|
||||
if (sshContainer) sshContainer.style.display = 'none';
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
";
|
||||
} catch (\Exception $e) {
|
||||
// Silently fail - don't break the checkout page
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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}
|
||||
/* 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%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1,11 @@
|
||||
<div class="alert alert-danger"><p>Oops! Something went wrong.</p></div><p>Please go back and try again.</p><p>If the problem persists, please contact support.</p>
|
||||
<div class="panel card panel-default mb-3">
|
||||
<div class="panel-heading card-header">
|
||||
<h3 class="panel-title card-title m-0">Error</h3>
|
||||
</div>
|
||||
<div class="panel-body card-body p-4">
|
||||
<div class="alert alert-danger mb-0">
|
||||
<p><strong>Something went wrong.</strong></p>
|
||||
<p class="mb-0">Please go back and try again. If the problem persists, please contact support.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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){})}
|
||||
/**
|
||||
* 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('<option value="">-- Select Operating System --</option>');
|
||||
$.each(response.data, function (i, template) {
|
||||
select.append('<option value="' + template.id + '">' + $('<span>').text(template.name).html() + '</option>');
|
||||
});
|
||||
} else {
|
||||
select.append('<option value="">No templates available</option>');
|
||||
}
|
||||
}).fail(function () {
|
||||
var select = $("#vf-rebuild-os");
|
||||
select.empty();
|
||||
select.append('<option value="">Error loading templates</option>');
|
||||
});
|
||||
}
|
||||
|
||||
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 + "/-");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user