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:
@@ -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)."\"]');
|
||||
|
||||
// Create dropdown options menu, then add it to the DOM then on change, update the regular input.
|
||||
let osSelect = document.createElement('select');
|
||||
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") . ";
|
||||
|
||||
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';
|
||||
|
||||
sshKeys.forEach(function(sshkey) {
|
||||
let option = document.createElement('option');
|
||||
option.value = sshkey.id;
|
||||
option.text = sshkey.name;
|
||||
sshSelect.appendChild(option);
|
||||
});
|
||||
|
||||
// Set the default value of the input field to the first option in the dropdown.
|
||||
sshInputField.value = sshSelect.options[0].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';
|
||||
}
|
||||
|
||||
// Handle SSH keys
|
||||
if (sshInputField) {
|
||||
if (sshKeys.length > 0) {
|
||||
var sshSelect = document.createElement('select');
|
||||
sshSelect.className = 'form-control';
|
||||
sshSelect.setAttribute('id', 'vf-ssh-select');
|
||||
|
||||
var sshDefaultOption = document.createElement('option');
|
||||
sshDefaultOption.value = '';
|
||||
sshDefaultOption.text = '-- No SSH Key (Optional) --';
|
||||
sshSelect.appendChild(sshDefaultOption);
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user