diff --git a/README.md b/README.md index df31ebc..952b8f1 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# VirtFusion Direct Provisioning Moudle for WHMCS +# 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 offical [documenataion](https://docs.virtfusion.com/integrations/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 -- Allow's using the configurable options quanity method for assigning Memory in Gigabytes instead of Megabyte. So you can use whole numbers to represent GB instead. +- 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. diff --git a/VirtFusionDirect.php b/VirtFusionDirect.php new file mode 100644 index 0000000..adfc955 --- /dev/null +++ b/VirtFusionDirect.php @@ -0,0 +1,104 @@ + '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); +} diff --git a/admin.php b/admin.php new file mode 100644 index 0000000..5083d41 --- /dev/null +++ b/admin.php @@ -0,0 +1,85 @@ +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); +} + diff --git a/client.php b/client.php new file mode 100644 index 0000000..a17cfd6 --- /dev/null +++ b/client.php @@ -0,0 +1,111 @@ +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); +} + + diff --git a/config/ConfigOptionMapping-example.php b/config/ConfigOptionMapping-example.php new file mode 100644 index 0000000..e024945 --- /dev/null +++ b/config/ConfigOptionMapping-example.php @@ -0,0 +1,19 @@ + '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', +]; \ No newline at end of file diff --git a/hooks.php b/hooks.php new file mode 100644 index 0000000..b3d9bbc --- /dev/null +++ b/hooks.php @@ -0,0 +1 @@ +Impersonate Server Owner +   A valid VirtFusion admin session in the same browser is required for this functionality to work. +EOT; + } + + public static function serverObject($serverObject) + { + return <<${serverObject} +EOT; + + } + + public static function serverId($serverId) + { + return << +   Changing the Sever ID manually is not recommended. Alterations to this field are usually handled automatically. +EOT; + } + + public static function serverInfo($systemUrl, $serviceId) + { + return << + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Name: +
+
+
+
+
+
+ Hostname: +
+
+
+
+
+
+ Memory: +
+
+
+
+
+
+ CPU: +
+
+
+
+
+
+
+
+ IPv4: +
+
+
+
+
+
+ IPv6: +
+
+
+
+
+
+ Storage: +
+
+
+
+
+
+ Traffic: +
+
+
+
+
+
+
+
+ +EOT; + } +} \ No newline at end of file diff --git a/lib/Curl.php b/lib/Curl.php new file mode 100644 index 0000000..adaa588 --- /dev/null +++ b/lib/Curl.php @@ -0,0 +1,200 @@ + 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']; + } +} \ No newline at end of file diff --git a/lib/Database.php b/lib/Database.php new file mode 100644 index 0000000..e711f1f --- /dev/null +++ b/lib/Database.php @@ -0,0 +1,121 @@ +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(); + } + +} \ No newline at end of file diff --git a/lib/Log.php b/lib/Log.php new file mode 100644 index 0000000..b84ae59 --- /dev/null +++ b/lib/Log.php @@ -0,0 +1,23 @@ +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; + } +} diff --git a/lib/ModuleFunctions.php b/lib/ModuleFunctions.php new file mode 100644 index 0000000..5d4f259 --- /dev/null +++ b/lib/ModuleFunctions.php @@ -0,0 +1,434 @@ +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' => [], + ]; + } + } +} diff --git a/lib/ServerResource.php b/lib/ServerResource.php new file mode 100644 index 0000000..7c9f2fa --- /dev/null +++ b/lib/ServerResource.php @@ -0,0 +1,62 @@ +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; + } +} \ No newline at end of file diff --git a/templates/css/module.css b/templates/css/module.css new file mode 100644 index 0000000..5972d3e --- /dev/null +++ b/templates/css/module.css @@ -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} \ No newline at end of file diff --git a/templates/error.tpl b/templates/error.tpl new file mode 100644 index 0000000..8898d2f --- /dev/null +++ b/templates/error.tpl @@ -0,0 +1 @@ +

Oops! Something went wrong.

Please go back and try again.

If the problem persists, please contact support.

\ No newline at end of file diff --git a/templates/js/module.js b/templates/js/module.js new file mode 100644 index 0000000..21b6c06 --- /dev/null +++ b/templates/js/module.js @@ -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){})} \ No newline at end of file diff --git a/templates/overview.tpl b/templates/overview.tpl new file mode 100644 index 0000000..bdd968d --- /dev/null +++ b/templates/overview.tpl @@ -0,0 +1 @@ +{if $serviceStatus eq 'Active'}

Server Overview

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

Manage

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

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

{if $serverHostname}

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

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

{/if}
{/if}

Billing Overview

Product:
{$groupname} - {$product}
{$LANG.recurringamount}:
{$recurringamount}
{$LANG.orderbillingcycle}:
{$billingcycle}
{$LANG.clientareahostingregdate}:
{$regdate}
{$LANG.clientareahostingnextduedate}:
{$nextduedate}
{$LANG.orderpaymentmethod}:
{$paymentmethod}
\ No newline at end of file