changed structure to match WHMCS

This commit is contained in:
2023-09-10 17:28:14 -04:00
parent 85af5bd090
commit d95cf91361
17 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,8 @@
# VirtFusion Direct Provisioning Module for WHMCS
This module requires VirtFusion v1.7.3 or higher as this is what it's based on. Please refer to the official [documenataion](https://docs.virtfusion.com/integrations/whmcs).
## Changes in this module
- Allows using the configurable options quantity method for assigning Memory in Gigabytes instead of Megabyte. So you can use whole numbers to represent GB instead.
For example, prior to this change for two gigabytes of RAM you'd need to set 2048 as either the min or max amount, now you can just input 2 for 2GB.

View File

@@ -0,0 +1,104 @@
<?php
if (!defined("WHMCS")) {
die("This file cannot be accessed directly");
}
use WHMCS\Module\Server\VirtFusionDirect\ModuleFunctions;
function VirtFusionDirect_MetaData()
{
return [
'DisplayName' => 'VirtFusion Direct Provisioning',
'APIVersion' => '1.1',
'RequiresServer' => true,
'ServiceSingleSignOnLabel' => false,
'AdminSingleSignOnLabel' => false,
];
}
function VirtFusionDirect_ConfigOptions()
{
return [
"defaultHypervisorGroupId" => [
"FriendlyName" => "Hypervisor Group ID",
"Type" => "text",
"Size" => "20",
"Description" => "The default hypervisor group ID",
"Default" => "1",
],
"packageID" => [
"FriendlyName" => "Package ID",
"Type" => "text",
"Size" => "20",
"Description" => "The package ID",
"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.",
"Default" => "1",
],
];
}
function VirtFusionDirect_AdminCustomButtonArray()
{
$buttonarray = array(
"Update Server Object" => "updateServerObject",
);
return $buttonarray;
}
/**
*
*
* Service functions
*
*/
function VirtFusionDirect_CreateAccount(array $params)
{
return (new ModuleFunctions())->createAccount($params);
}
function VirtFusionDirect_SuspendAccount(array $params)
{
return (new ModuleFunctions())->suspendAccount($params);
}
function VirtFusionDirect_UnsuspendAccount(array $params)
{
return (new ModuleFunctions())->unsuspendAccount($params);
}
function VirtFusionDirect_TerminateAccount(array $params)
{
return (new ModuleFunctions())->terminateAccount($params);
}
function VirtFusionDirect_updateServerObject(array $params)
{
return (new ModuleFunctions())->updateServerObject($params);
}
function VirtFusionDirect_ChangePackage(array $params)
{
return 'success';
}
function VirtFusionDirect_AdminServicesTabFields(array $params)
{
return (new ModuleFunctions())->adminServicesTabFields($params);
}
function VirtFusionDirect_AdminServicesTabFieldsSave(array $params)
{
(new ModuleFunctions())->adminServicesTabFieldsSave($params);
}
function VirtFusionDirect_ClientArea(array $params)
{
return (new ModuleFunctions())->clientArea($params);
}

View File

@@ -0,0 +1,85 @@
<?php
require dirname(__DIR__, 3) . '/init.php';
use WHMCS\Module\Server\VirtFusionDirect\Database;
use WHMCS\Module\Server\VirtFusionDirect\Module;
use WHMCS\Module\Server\VirtFusionDirect\ServerResource;
$vf = new Module();
$vf->adminOnly();
switch ($vf->validateAction(true)) {
/**
*
* Get server information.
*
*/
case 'serverData':
if ($vf->validateServiceID(true)) {
/** No need to validate ownership **/
$whmcsService = Database::getWhmcsService((int)$_GET['serviceID']);
if (!$whmcsService) {
$vf->output(['success' => false, 'errors' => 'Service not found.'], true, true, 200);
}
if ($whmcsService->domainstatus == 'Pending' || $whmcsService->domainstatus == 'Terminated' || $whmcsService->domainstatus == 'Cancelled' || $whmcsService->domainstatus == 'Fraud') {
$vf->output(['success' => false, 'errors' => 'Server is not Active, Suspended or Completed. Not fetching remote data.'], true, true, 200);
}
$data = $vf->fetchServerData((int)$_GET['serviceID']);
if (!$data) {
$vf->output(['success' => false, 'errors' => 'No data returned from VirtFusion.'], true, true, 200);
}
(new Module())->updateWhmcsServiceParamsOnServerObject((int)$_GET['serviceID'], $data);
$vf->output(['success' => true, 'data' => (new ServerResource())->process($data)], true, true, 200);
}
break;
/**
*
* Impersonate server owner.
*
*/
case 'impersonateServerOwner':
if ($vf->validateServiceID(true)) {
$service = Database::getSystemService((int)$_GET['serviceID']);
if (!$service) {
$vf->output(['success' => false, 'errors' => 'Service not found'], true, true, 200);
}
$whmcsService = Database::getWhmcsService((int)$_GET['serviceID']);
$cp = $vf->getCP($whmcsService->server);
$request = $vf->initCurl($cp['token']);
$data = $request->get($cp['url'] . '/users/' . $whmcsService->userid . '/byExtRelation');
if ($request->getRequestInfo('http_code') === 200) {
$vf->output(['success' => true, 'url' => $cp['base_url'], 'user' => json_decode($data, true)['data']], true, true, 200);
}
$vf->output(['success' => false, 'errors' => 'Received HTTP code ' . $request->getRequestInfo('http_code')], true, true, 200);
}
break;
default:
/** No valid action was specified **/
$vf->output(['success' => false, 'errors' => 'invalid action'], true, true, 200);
}

View File

@@ -0,0 +1,111 @@
<?php
require dirname(__DIR__, 3) . '/init.php';
use WHMCS\Module\Server\VirtFusionDirect\Module;
use WHMCS\Module\Server\VirtFusionDirect\ServerResource;
$vf = new Module();
$vf->isAuthenticated();
switch ($vf->validateAction(true)) {
/**
*
* 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);
}
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);
}
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);
}
break;
default:
/**
*
* No valid action was specified.
*
*/
$vf->output(['success' => false, 'errors' => 'invalid action'], true, true, 200);
}

View File

@@ -0,0 +1,19 @@
<?php
if (!defined("WHMCS")) {
die("This file cannot be accessed directly");
}
return [
'ipv4' => 'IPv4',
'packageId' => 'Package',
'hypervisorId' => 'Location',
'storage' => 'Storage',
'memory' => 'Memory',
'traffic' => 'Bandwidth',
'networkSpeedInbound' => 'Inbound Network Speed',
'networkSpeedOutbound' => 'Outbound Network Speed',
'cpuCores' => 'CPU Cores',
'networkProfile' => 'Network Type',
'storageProfile' => 'Storage Type',
];

View File

@@ -0,0 +1,232 @@
<?php
use WHMCS\User\User;
if (!defined("WHMCS")) {
die("This file cannot be accessed directly");
}
/**
* You'll need to configure the following constrants in your configuration.php file.
* This is only temporary and will be replaced with pulling the token from the database in the future.
*
* const VIRTFUSION_API_URL = "https://your-virtfusion-url.com/api/v1";
*
* You can create a token in the VirtFusion control panel under System > API.
*
* const VIRT_TOKEN = "your-virtfusion-token";
*/
/**
* If the constants are not defined, return null to prevent errors.
*/
if (!defined("VIRTFUSION_API_URL") || !defined("VIRT_TOKEN")) {
return null;
}
if (!function_exists('fetchPackageId')) {
/**
* @param string $packageName
* @return int|null
* @throws JsonException
*/
function fetchPackageId(string $packageName): ?int
{
$url = sprintf("%s/packages", VIRTFUSION_API_URL);
$packages = makeRequest($url);
foreach ($packages['data'] as $package) {
if ($package['name'] === $packageName && $package['enabled'] === true) {
return $package['id'];
}
}
return null;
}
}
if (!function_exists('fetchTemplates')) {
/**
* @param int $serverPackageId
* @return array|null
* @throws JsonException
*/
function fetchTemplates(int $serverPackageId): ?array
{
$url = sprintf("%s/media/templates/fromServerPackageSpec/%d", VIRTFUSION_API_URL, $serverPackageId);
return makeRequest($url);
}
}
if (!function_exists('custom_os_templates_hook')) {
/**
* @param array $vars
* @return array|null[]
*/
function custom_os_templates_hook(array $vars): array
{
try {
$serverPackageId = fetchPackageId($vars['productinfo']['name']); // Replace with the appropriate server package ID
if ($serverPackageId === null) {
return [
'templates' => null,
];
}
$templates = fetchTemplates($serverPackageId);
// Assign the generated dropdown menu to a Smarty template variable
return [
'templates' => $templates,
];
} catch (JsonException $e) {
return [
'templates' => null,
];
}
}
}
if (!function_exists('get_vf_user_details')) {
/**
* @param int $id
* @return array
* @throws JsonException
*/
function get_vf_user_details(int $id): ?array {
$url = sprintf("%s/users/%s/byExtRelation", VIRTFUSION_API_URL, $id);
$response = makeRequest($url);
if (isset($response['msg']) && $response['msg'] === "ext_relation_id not found") {
return null;
}
return $response['data'];
}
}
if (!function_exists('makeRequest')) {
/**
* @param string $url
* @return mixed
* @throws JsonException
* @throws Exception
*/
function makeRequest(string $url): array
{
$curl = curl_init();
curl_setopt_array($curl, array(
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => array(
sprintf("Authorization: Bearer %s", VIRT_TOKEN)
)
));
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
throw new Exception("cURL Error: " . $err);
}
return json_decode($response, true, 512, JSON_THROW_ON_ERROR);
}
}
if (!function_exists('get_users_ssh_keys')) {
/**
* @throws JsonException
*/
function get_users_ssh_keys(?User $user): ?array {
if (is_null($user)) {
return null;
}
$vfUser = get_vf_user_details($user['id']);
$url = sprintf("%s/ssh_keys/user/%s", VIRTFUSION_API_URL, $vfUser['id']);
$response = makeRequest($url);
return $response;
}
}
if (!function_exists('add_hook_os_templates')) {
/**
* @param array $vars
* @return array|null
* @throws JsonException
*/
function add_hook_os_templates(array $vars): ?array
{
if (!isset($vars['productinfo']['module']) || $vars['productinfo']['module'] !== 'VirtFusionDirect') {
return null;
}
$templates_data = custom_os_templates_hook($vars)['templates'];
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']);
});
$osTemplates = [
'id' => 'os_template',
'optionname' => 'Initial Operating System',
'optiontype' => 1,
'options' => $dropdownOptions,
'selectedvalue' => ''
];
$sshKeys = get_users_ssh_keys($vars['loggedinuser']);
$sshKeysOptions = [
'id' => 'ssh_key',
'optionname' => 'Initial SSH Key',
'optiontype' => 1,
'options' => array_map(function ($sshKey) {
if ($sshKey['enabled'] === false) {
return null;
}
return [
'id' => $sshKey['id'],
'name' => $sshKey['name']
];
}, $sshKeys['data'] ?? []),
'selectedvalue' => ''
];
$configurableoptions = $vars['configurableoptions'];
array_push(
$configurableoptions,
$osTemplates,
$sshKeysOptions
);
return [
'configurableoptions' => $configurableoptions,
];
}
add_hook('ClientAreaPageCart', 1, 'add_hook_os_templates');
}

View File

@@ -0,0 +1,114 @@
<?php
namespace WHMCS\Module\Server\VirtFusionDirect;
class AdminHTML
{
public static function options($systemUrl, $serviceId)
{
return <<<EOT
<button onclick="impersonateServerOwner('${serviceId}', '${systemUrl}')" type="button" class="btn btn-primary">Impersonate Server Owner</button>
<span class="text-info">&nbsp;&nbsp;A valid VirtFusion admin session in the same browser is required for this functionality to work.</span>
EOT;
}
public static function serverObject($serverObject)
{
return <<<EOT
<textarea class="form-control" name="modulefields[1]" rows="10" style="width: 100%" disabled>${serverObject}</textarea>
EOT;
}
public static function serverId($serverId)
{
return <<<EOT
<input type="text" class="form-control input-200 input-inline" name="modulefields[0]" size="20" value="${serverId}" />
<span class="text-info">&nbsp;&nbsp;Changing the Sever ID manually is not recommended. Alterations to this field are usually handled automatically.</span>
EOT;
}
public static function serverInfo($systemUrl, $serviceId)
{
return <<<EOT
<link href="${systemUrl}modules/servers/VirtFusionDirect/templates/css/module.css" rel="stylesheet">
<script src="${systemUrl}modules/servers/VirtFusionDirect/templates/js/module.js"></script>
<div id="vf-loader" class="vf-loader">
<div id="vf-loading"></div>
</div>
<div id="vf-server-info-error">
<div class="alert alert-warning mb-0">
<div id="vf-server-info-error-message"></div>
</div>
</div>
<div id="vf-server-info" class="row mb-2">
<div class="col-12">
<div class="row">
<div class="col-md-6">
<div class="row p-1">
<div class="col-xs-4 col-4 text-right vf-bold">
Name:
</div>
<div class="col-xs-8 col-8" id="vf-data-server-name">
</div>
</div>
<div class="row p-1">
<div class="col-xs-4 col-4 text-right vf-bold">
Hostname:
</div>
<div class="col-xs-8 col-8" id="vf-data-server-hostname">
</div>
</div>
<div class="row p-1">
<div class="col-xs-4 col-4 text-right vf-bold">
Memory:
</div>
<div class="col-xs-8 col-8" id="vf-data-server-memory">
</div>
</div>
<div class="row p-1">
<div class="col-xs-4 col-4 text-right vf-bold">
CPU:
</div>
<div class="col-xs-8 col-8" id="vf-data-server-cpu">
</div>
</div>
</div>
<div class="col-md-6">
<div class="row p-1">
<div class="col-xs-4 col-4 text-right vf-bold">
IPv4:
</div>
<div class="col-xs-8 col-8" id="vf-data-server-ipv4">
</div>
</div>
<div class="row p-1">
<div class="col-xs-4 col-4 text-right vf-bold">
IPv6:
</div>
<div class="col-xs-8 col-8" id="vf-data-server-ipv6">
</div>
</div>
<div class="row p-1">
<div class="col-xs-4 col-4 text-right vf-bold">
Storage:
</div>
<div class="col-xs-8 col-8" id="vf-data-server-storage">
</div>
</div>
<div class="row p-1">
<div class="col-xs-4 col-4 text-right vf-bold">
Traffic:
</div>
<div class="col-xs-8 col-8" id="vf-data-server-traffic">
</div>
</div>
</div>
</div>
</div>
</div>
<script>vfServerDataAdmin("${serviceId}","${systemUrl}");</script>
EOT;
}
}

View File

@@ -0,0 +1,200 @@
<?php
namespace WHMCS\Module\Server\VirtFusionDirect;
class Curl
{
private $ch;
private $data;
private $customOptions = [];
private $defaultOptions = [
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => false,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_USERAGENT => 'CURL',
CURLOPT_HEADER => false,
CURLOPT_NOBODY => false,
];
public function __construct()
{
$this->ch = curl_init();
}
public function useCookies()
{
$cookiesFile = tempnam('/tmp', 'virtfusion_cookies');
$this->defaultOptions[CURLOPT_COOKIEFILE] = $cookiesFile;
$this->defaultOptions[CURLOPT_COOKIEJAR] = $cookiesFile;
}
public function setLog()
{
$log = fopen(__DIR__ . '/CURL.log', 'a');
if ($log) {
fwrite($log, str_repeat('=', 80) . PHP_EOL);
$this->addOption(CURLOPT_STDERR, $log);
$this->addOption(CURLOPT_VERBOSE, true);
}
}
/**
* @param $name
* @param $value
*/
public function addOption($name, $value)
{
$this->customOptions[$name] = $value;
}
/**
* @param null $url
* @return bool|string|void
*/
public function put($url = null)
{
return $this->send('PUT', $url);
}
/**
* @param $method
* @param $url
* @return bool|string|void
*/
private function send($method, $url)
{
if ($url === null) {
if (!isset($this->customOptions[CURLOPT_URL]) || empty($this->customOptions[CURLOPT_URL])) {
exit('empty url');
}
}
$this->addOption(CURLOPT_CUSTOMREQUEST, $method);
$this->addOption(CURLOPT_URL, $url);
return $this->exec();
}
/**
* @return bool|string
*/
private function exec()
{
$this->setOptions();
$response = curl_exec($this->ch);
$this->data['info'] = curl_getinfo($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);
}
curl_close($this->ch);
return $response;
}
private function setOptions()
{
if (isset($this->customOptions[CURLOPT_HEADER]) && $this->customOptions[CURLOPT_HEADER]) {
$this->addOption(CURLINFO_HEADER_OUT, true);
}
$options = $this->customOptions + $this->defaultOptions;
curl_setopt_array($this->ch, $options);
}
/**
* @param $data
*/
private function processHeaders(&$data)
{
$tmp = explode("\r\n\r\n", $data, 2);
$this->data['info']['response_header'] = $tmp[0];
$this->data['info']['response_body'] = $data = trim($tmp[1]);
$tmp = explode("\r\n", $this->data['info']['response_header']);
$this->data['data']['Message'] = $tmp[0];
for ($i = 1, $size = count($tmp); $i < $size; ++$i) {
$string = explode(': ', $tmp[$i], 2);
$this->data['data'][$string[0]] = $string[1];
}
}
/**
* @param null $url
* @return bool|string|void
*/
public function get($url = null)
{
return $this->send('GET', $url);
}
/**
* @param null $url
* @return bool|string|void
*/
public function delete($url = null)
{
return $this->send('DELETE', $url);
}
/**
* @param null $url
* @return bool|string|void
*/
public function post($url = null)
{
return $this->send('POST', $url);
}
/**
* @param null $url
* @return bool|string|void
*/
public function head($url = null)
{
return $this->send('HEAD', $url);
}
/**
* @param false $param
* @return mixed|null
*/
public function getRequestInfo($param = false)
{
if ($param) {
return $this->getDataItem('info', $param);
} else {
return $this->data['info'];
}
}
/**
* @param $what
* @param $name
* @return mixed|null
*/
private function getDataItem($what, $name)
{
if (isset($this->data[$what][$name])) {
return $this->data[$what][$name];
} else {
return null;
}
}
/**
* @param false $param
* @return mixed|null
*/
public function getHeadersData($param = false)
{
if ($param) {
return $this->getDataItem('data', $param);
}
return $this->data['data'];
}
}

View File

@@ -0,0 +1,121 @@
<?php
namespace WHMCS\Module\Server\VirtFusionDirect;
use WHMCS\Database\Capsule as DB;
class Database
{
const SYSTEM_TABLE = 'mod_virtfusion_direct';
public static function schema()
{
if (!DB::schema()->hasTable(self::SYSTEM_TABLE)) {
try {
DB::schema()->create(self::SYSTEM_TABLE, function ($table) {
$table->unsignedBigInteger('service_id')->nullable()->default(null)->index();
$table->unsignedBigInteger('server_id')->nullable()->default(null);
$table->timestamps();
});
} catch (\Exception $e) {
Log::insert(__FUNCTION__, [], $e->getMessage());
}
}
if (!DB::schema()->hasColumn(self::SYSTEM_TABLE, 'server_object')) {
try {
DB::schema()->table(self::SYSTEM_TABLE, function ($table) {
$table->longText('server_object')->nullable()->default(null);
});
} catch (\Exception $e) {
Log::insert(__FUNCTION__, [], $e->getMessage());
}
}
}
public static function getWhmcsServer(int $server, $any = false)
{
if ($server) {
return DB::table('tblservers')->where('type', 'VirtFusionDirect')->where('id', $server)->first();
}
if ($any) {
return DB::table('tblservers')->where('type', 'VirtFusionDirect')->where('disabled', 0)->first();
}
return false;
}
public static function userWhmcsService(int $serviceId, int $userId)
{
return DB::table('tblhosting')->where('id', $serviceId)->where('userid', $userId)->exists();
}
public static function getSystemUrl()
{
$url = DB::table('tblconfiguration')->where('setting', '=', 'SystemURL')->first();
return $url->value;
}
public static function getUser(int $id)
{
return DB::table('tblclients')->where('id', $id)->first();
}
public static function getWhmcsService(int $serviceId)
{
return DB::table('tblhosting')->where('id', $serviceId)->first();
}
public static function updateSystemServiceServerId(int $serviceId, int $serverId)
{
DB::table(self::SYSTEM_TABLE)->updateOrInsert(
[
"service_id" => $serviceId
],
[
'server_id' => $serverId
]
);
}
public static function updateWhmcsServiceParams(int $serviceId, $data)
{
if (count($data)) {
foreach ($data as $key => $items) {
DB::table($key)->where('id', $serviceId)->update($items);
}
}
}
public static function checkSystemService(int $serviceId)
{
return DB::table(self::SYSTEM_TABLE)->where('service_id', $serviceId)->exists();
}
public static function deleteSystemService(int $serviceId)
{
DB::table(self::SYSTEM_TABLE)->where('service_id', $serviceId)->delete();
}
public static function updateSystemServiceServerObject(int $serviceId, $data)
{
DB::table(self::SYSTEM_TABLE)->where('service_id', $serviceId)->update(['server_object' => json_encode($data, JSON_PRETTY_PRINT)]);
}
public static function systemOnServerCreate(int $serviceId, $data)
{
if (DB::table(self::SYSTEM_TABLE)->where('service_id', $serviceId)->exists()) {
DB::table(self::SYSTEM_TABLE)->where('service_id', $serviceId)->update(['server_id' => $data->data->id, 'server_object' => json_encode($data, JSON_PRETTY_PRINT)]);
} else {
DB::table(self::SYSTEM_TABLE)->insert(['service_id' => $serviceId, 'server_id' => $data->data->id, 'server_object' => json_encode($data, JSON_PRETTY_PRINT)]);
}
}
public static function getSystemService(int $serviceId)
{
return DB::table(self::SYSTEM_TABLE)->where('service_id', $serviceId)->first();
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace WHMCS\Module\Server\VirtFusionDirect;
class Log
{
const LOG_MODULE = 'VirtFusionDirect';
public static function insert($action, $requestString, $responseData)
{
/**
* Log module call.
*
* @param string $module The name of the module
* @param string $action The name of the action being performed
* @param string|array $requestString The input parameters for the API call
* @param string|array $responseData The response data from the API call
* @param string|array $processedData The resulting data after any post processing (eg. json decode, xml decode, etc...)
* @param array $replaceVars An array of strings for replacement
*/
logModuleCall(self::LOG_MODULE, $action, $requestString, $responseData);
}
}

View File

@@ -0,0 +1,235 @@
<?php
namespace WHMCS\Module\Server\VirtFusionDirect;
class Module
{
public function __construct()
{
error_reporting(0);
Database::schema();
}
/**
* @param bool $exitOnError
* @return mixed
*/
public function validateAction($exitOnError = true)
{
if (!isset($_GET['action'])) {
$this->output(['errors' => 'no action specified'], true, $exitOnError, 200);
}
return $_GET['action'];
}
/**
* @param bool $exitOnError
* @return mixed
*/
public function validateServiceID($exitOnError = true)
{
if (!isset($_GET['serviceID'])) {
$this->output(['errors' => 'no serviceID specified'], true, $exitOnError, 200);
}
return $_GET['serviceID'];
}
/**
* @param $serviceID
* @param bool $exitOnError
* @return bool
*/
public function validateUserOwnsService($serviceID, $exitOnError = true)
{
$currentUser = new \WHMCS\Authentication\CurrentUser;
$client = $currentUser->client();
if (!$client) {
return false;
}
if (Database::userWhmcsService($serviceID, $client->id)) {
return $client->id;
}
return false;
}
/**
* @param $serviceID
* @return false|string
*/
public function fetchLoginTokens($serviceID)
{
$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'] . '/users/' . $whmcsService->userid . '/serverAuthenticationTokens/' . $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;
}
}
return false;
}
public function updateWhmcsServiceParamsOnServerObject($serviceId, $data)
{
$output = [];
$serverResource = (new ServerResource())->process($data);
$dedicatedIpv4 = null;
if (count($serverResource['primaryNetwork']['ipv4Unformatted'])) {
$dedicatedIpv4 = $serverResource['primaryNetwork']['ipv4Unformatted'][0];
}
if ($serverResource['hostname'] == '-') {
if ($serverResource['name'] == '-') {
$name = '';
} else {
$name = $serverResource['name'];
}
} else {
$name = $serverResource['hostname'];
}
$output['tblhosting'] = ["dedicatedip" => $dedicatedIpv4, "domain" => $name, "username" => $serverResource['username'], "password" => $serverResource['password']];
Database::updateWhmcsServiceParams($serviceId, $output);
}
public function updateWhmcsServiceParamsOnDestroy($serviceId)
{
$output['tblhosting'] = ["dedicatedip" => null];
Database::updateWhmcsServiceParams($serviceId, $output);
}
public function fetchServerData($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);
Log::insert(__FUNCTION__, $request->getRequestInfo(), $data);
if ($request->getRequestInfo('http_code') == '200') {
return json_decode($data);
}
}
return false;
}
public function resetUserPassword($serviceID, $clientID)
{
$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'] . '/users/' . $clientID . '/byExtRelation/resetPassword');
Log::insert(__FUNCTION__, $request->getRequestInfo(), $data);
if ($request->getRequestInfo('http_code') == '201') {
return json_decode($data);
}
}
return false;
}
/**
* @param $data
* @param bool $json
* @param bool $exit
* @param int $rspCode
*/
public function output($data, $json = true, $exit = true, $rspCode = 200)
{
http_response_code($rspCode);
if ($json) {
header('Content-Type: application/json; charset=utf-8');
echo json_encode($data);
} else {
echo $data;
}
if ($exit) {
exit();
}
}
/**
* @param $server
* @return array|false
*/
public function getCP($server, $any = false)
{
$cp = Database::getWhmcsServer($server, $any);
if ($cp) {
return [
'url' => 'https://' . $cp->hostname . '/api/v1',
'base_url' => 'https://' . $cp->hostname,
'token' => decrypt($cp->password)];
}
return false;
}
/**
* @return bool|void
*/
public function adminOnly()
{
if ((new \WHMCS\Authentication\CurrentUser)->isAuthenticatedAdmin()) {
return true;
}
$this->output(['errors' => 'unauthenticated'], true, true, 200);
}
/**
* @return bool|void
*/
public function isAuthenticated()
{
if ((new \WHMCS\Authentication\CurrentUser)->isAuthenticatedUser()) {
return true;
}
$this->output(['errors' => 'unauthenticated'], true, true, 200);
}
/**
* @param $token
* @return \WHMCS\Module\Server\VirtFusionDirect\Curl
*/
public function initCurl($token)
{
$curl = new Curl();
$curl->addOption(CURLOPT_HTTPHEADER, [
'Accept: application/json',
'Content-type: application/json; charset=utf-8',
'authorization: Bearer ' . $token
]);
return $curl;
}
}

View File

@@ -0,0 +1,434 @@
<?php
namespace WHMCS\Module\Server\VirtFusionDirect;
class ModuleFunctions extends Module
{
public function __construct()
{
parent::__construct();
}
/**
*
* CREATE SERVER
*
* Before creating a server, we check to see if a user exists in VirtFusion that matches
* the WHMCS user. If it matches, We move on to create the server, if not, we attempt to
* create a user to assign to the new server.
*
*/
public function createAccount($params)
{
try {
/**
*
* 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 ? false : true);
if (!$cp) {
return 'No Control server found.';
}
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');
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']);
$request = $this->initCurl($cp['token']);
$request->addOption(CURLOPT_POSTFIELDS, json_encode(
[
"name" => $user->firstname . ' ' . $user->lastname,
"email" => $user->email,
"extRelationId" => $user->id
]
));
$data = $request->post($cp['url'] . '/users');
Log::insert(__FUNCTION__, $request->getRequestInfo(), $data);
if ($request->getRequestInfo('http_code') !== 201) {
return 'Unable to create user.';
}
break;
default:
return 'Error processing user account.';
break;
}
$data = json_decode($data);
/**
*
* A user is available. We can now attempt to create a server.
*
*/
$configOptionDefaultNaming = [
'ipv4' => 'IPv4',
'packageId' => 'Package',
'hypervisorId' => 'Location',
'storage' => 'Storage',
'memory' => 'Memory',
'traffic' => 'Bandwidth',
'networkSpeedInbound' => 'Inbound Network Speed',
'networkSpeedOutbound' => 'Outbound Network Speed',
'cpuCores' => 'CPU Cores',
'networkProfile' => 'Network Type',
'storageProfile' => 'Storage Type',
];
$configOptionCustomNaming = [];
if (file_exists(ROOTDIR . '/modules/servers/VirtFusionDirect/config/ConfigOptionMapping.php')) {
$configOptionCustomNaming = require_once ROOTDIR . '/modules/servers/VirtFusionDirect/config/ConfigOptionMapping.php';
}
$options = [
"packageId" => $params['configoption2'],
"userId" => $data->data->id,
"hypervisorId" => $params['configoption1'],
"ipv4" => $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
// VirtFusion expects memory in MB.
if ($currentOption === 'Memory' && $params['configoptions'][$currentOption] < 1024) {
$options[$key] = $params['configoptions'][$currentOption] * 1024;
} else {
$options[$key] = $params['configoptions'][$currentOption];
}
}
}
}
$request = $this->initCurl($cp['token']);
$request->addOption(CURLOPT_POSTFIELDS, json_encode($options));
$data = $request->post($cp['url'] . '/servers');
$data = json_decode($data);
Log::insert(__FUNCTION__, $request->getRequestInfo(), $data);
if ($request->getRequestInfo('http_code') === 201) {
Database::systemOnServerCreate($params['serviceid'], $data);
$this->updateWhmcsServiceParamsOnServerObject($params['serviceid'], $data);
/**
*
* Server was created successfully.
*
*/
return 'success';
} else {
if ($data->errors[0]) {
return $data->errors[0];
}
return 'Unknown error.';
}
} catch (\Exception $e) {
Log::insert(__FUNCTION__, $params, $e->getMessage());
return $e->getMessage();
}
}
/**
*
* TERMINATE SERVER
*
* When requesting to terminate a server in VirtFusion, we leave it set to
* the default 5-minute delay allowing to un-terminate in VirtFusion if the
* request was done in error.
*
*/
public function terminateAccount($params)
{
$service = Database::getSystemService($params['serviceid']);
if ($service) {
$whmcsService = Database::getWhmcsService($params['serviceid']);
$cp = $this->getCP($whmcsService->server);
$request = $this->initCurl($cp['token']);
$data = $request->delete($cp['url'] . '/servers/' . $service->server_id);
$data = json_decode($data);
Log::insert(__FUNCTION__, $request->getRequestInfo(), $data);
switch ($request->getRequestInfo('http_code')) {
case 204:
Database::deleteSystemService($params['serviceid']);
$this->updateWhmcsServiceParamsOnDestroy($params['serviceid']);
return 'success';
break;
case 404:
if (property_exists($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.';
}
} else {
return '404 was returned from the web service without the msg property. The service may be currently unavailable.';
}
break;
default:
return 'Termination request failed. The web service reported HTTP code ' . $request->getRequestInfo('http_code');
break;
}
}
return 'Service not found. Termination routine has already been run?';
}
/**
*
* SUSPEND SERVER
*
* When requesting to suspend a server in VirtFusion it may be delayed if another action
* is being processed. This function will return success if the server is either suspended
* now or has been queued for suspension.
*
*/
public function suspendAccount($params)
{
$service = Database::getSystemService($params['serviceid']);
if ($service) {
$whmcsService = Database::getWhmcsService($params['serviceid']);
$cp = $this->getCP($whmcsService->server);
$request = $this->initCurl($cp['token']);
$data = $request->post($cp['url'] . '/servers/' . $service->server_id . '/suspend');
$data = json_decode($data);
Log::insert(__FUNCTION__, $request->getRequestInfo(), $data);
switch ($request->getRequestInfo('http_code')) {
case 204:
return 'success';
break;
case 404:
if (property_exists($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.';
}
} else {
return '404 was returned from the web service without the msg property. The service may be currently unavailable.';
}
break;
case 423:
if (property_exists($data, 'msg')) {
return $data->msg;
}
default:
return 'Suspend request failed. The web service reported HTTP code ' . $request->getRequestInfo('http_code');
break;
}
}
return 'Service not found.';
}
function updateServerObject($params)
{
$service = Database::getSystemService($params['serviceid']);
if ($service) {
$whmcsService = Database::getWhmcsService($params['serviceid']);
$cp = $this->getCP($whmcsService->server);
$request = $this->initCurl($cp['token']);
$data = $request->get($cp['url'] . '/servers/' . $service->server_id);
$data = json_decode($data);
Log::insert(__FUNCTION__, $request->getRequestInfo(), $data);
switch ($request->getRequestInfo('http_code')) {
case 200:
Database::updateSystemServiceServerObject($params['serviceid'], $data);
$this->updateWhmcsServiceParamsOnServerObject($params['serviceid'], $data);
return 'success';
break;
default:
return 'Request failed. The web service reported HTTP code ' . $request->getRequestInfo('http_code');
break;
}
}
return 'Service not found.';
}
public function unsuspendAccount($params)
{
$service = Database::getSystemService($params['serviceid']);
if ($service) {
$whmcsService = Database::getWhmcsService($params['serviceid']);
$cp = $this->getCP($whmcsService->server);
$request = $this->initCurl($cp['token']);
$data = $request->post($cp['url'] . '/servers/' . $service->server_id . '/unsuspend');
$data = json_decode($data);
Log::insert(__FUNCTION__, $request->getRequestInfo(), $data);
switch ($request->getRequestInfo('http_code')) {
case 204:
return 'success';
break;
case 404:
if (property_exists($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.';
}
} else {
return '404 was returned from the web service without the msg property. The service may be currently unavailable.';
}
break;
case 423:
if (property_exists($data, 'msg')) {
return $data->msg;
}
default:
return 'Unsuspend request failed. The web service reported HTTP code ' . $request->getRequestInfo('http_code');
break;
}
}
return 'Service not found';
}
public function adminServicesTabFields($params)
{
$serverId = '';
$serverObject = '';
$service = Database::getSystemService($params['serviceid']);
$systemUrl = Database::getSystemUrl();
if ($service) {
$serverId = $service->server_id;
$serverObject = $service->server_object;
}
$fields = [
'Server ID' => AdminHTML::serverId($serverId),
'Server Info' => AdminHTML::serverInfo($systemUrl, $params['serviceid']),
'Server Object' => AdminHTML::serverObject($serverObject),
];
if ($params['status'] != 'Terminated') {
$fields['Options'] = AdminHTML::options($systemUrl, $params['serviceid']);
}
return $fields;
}
public function adminServicesTabFieldsSave($params)
{
if ($_POST['modulefields'][0] == '') {
Database::deleteSystemService($params['serviceid']);
} else {
Database::updateSystemServiceServerId($params['serviceid'], $_POST['modulefields'][0]);
}
}
public function clientArea($params)
{
$serverHostname = null;
if (array_key_exists('serverhostname', $params)) {
$serverHostname = $params['serverhostname'];
}
try {
return [
'tabOverviewReplacementTemplate' => 'overview',
'templateVariables' => [
'systemURL' => Database::getSystemUrl(),
'serviceStatus' => $params['status'],
'serverHostname' => $serverHostname,
],
];
} catch (\Throwable $e) {
Log::insert(__FUNCTION__, $params, $e->getMessage());
return [
'tabOverviewReplacementTemplate' => 'error',
'templateVariables' => [],
];
}
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace WHMCS\Module\Server\VirtFusionDirect;
class ServerResource
{
public function process($data)
{
$server = json_decode(json_encode($data->data), true);
$traffic = '∞';
if ($server['settings']['resources']['traffic']) {
if ($server['settings']['resources']['traffic'] > 0) {
$traffic = $server['settings']['resources']['traffic'] . ' GB';
}
}
$data = [
'name' => $server['name'] ?: '-',
'hostname' => $server['hostname'] ?: '-',
'memory' => $server['settings']['resources']['memory'] . ' MB',
'traffic' => $traffic,
'storage' => $server['settings']['resources']['storage'] . ' GB',
'cpu' => $server['settings']['resources']['cpuCores'] . ' Core(s)',
'primaryNetwork' => [
'ipv4' => ['-'],
'ipv4Unformatted' => [],
'ipv6' => ['-'],
'ipv6Unformatted' => [],
]
];
if (array_key_exists('network', $server)) {
if (array_key_exists('interfaces', $server['network'])) {
if (count($server['network']['interfaces'])) {
if (count($server['network']['interfaces'][0]['ipv4'])) {
$data['primaryNetwork']['ipv4'] = [];
foreach ($server['network']['interfaces'][0]['ipv4'] as $ip) {
$data['primaryNetwork']['ipv4'][] = $ip['address'];
}
}
if (count($server['network']['interfaces'][0]['ipv6'])) {
$data['primaryNetwork']['ipv6'] = [];
foreach ($server['network']['interfaces'][0]['ipv6'] as $ip) {
$data['primaryNetwork']['ipv6'][] = $ip['subnet'] . '/' . $ip['cidr'];
}
}
}
}
}
$data['primaryNetwork']['ipv4Unformatted'] = $data['primaryNetwork']['ipv4'];
$data['primaryNetwork']['ipv6Unformatted'] = $data['primaryNetwork']['ipv6'];
$data['primaryNetwork']['ipv4'] = implode(', ', $data['primaryNetwork']['ipv4']);
$data['primaryNetwork']['ipv6'] = implode(', ', $data['primaryNetwork']['ipv6']);
return $data;
}
}

View File

@@ -0,0 +1 @@
.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}

View File

@@ -0,0 +1 @@
<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>

View File

@@ -0,0 +1 @@
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){})}

File diff suppressed because one or more lines are too long