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:
Claude
2026-02-07 12:18:11 +00:00
parent 7b87fdcc3f
commit c93072b1c6
12 changed files with 1468 additions and 293 deletions

196
README.md
View File

@@ -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.

View File

@@ -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
*/

View File

@@ -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);
}

View File

@@ -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;
}
});

View File

@@ -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);

View File

@@ -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);
}
/**

View File

@@ -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);
}
}
}

View File

@@ -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) {

View File

@@ -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%;
}
}

View File

@@ -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>

View File

@@ -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