15 Commits

Author SHA1 Message Date
7b87fdcc3f Updated github action 2025-10-01 13:06:43 -04:00
a2275f4444 Moved SQL to a file 2025-10-01 13:02:52 -04:00
798d3fcdb5 Small tweaks 2025-10-01 13:02:42 -04:00
Andrew
9aa8378599 Merge pull request #2 from EZSCALE/Prophet731-patch-1
Update hooks.php
2024-01-16 12:13:15 -05:00
Andrew
f0c28a4961 Update hooks.php
Hide the SSH key field if the user isn't logged in to see them or is a new user.
2024-01-16 12:11:26 -05:00
Prophet731
a46223e5ac Update issue templates 2023-09-11 00:31:03 -04:00
98250f2f4c Ahh typos 2023-09-10 23:58:17 -04:00
2691013cc4 Put the on in the wrong spot. 2023-09-10 23:57:00 -04:00
24e79eed31 Forgot to remove this 2023-09-10 23:50:54 -04:00
db3afee99e Deleted linter as its not really needed. 2023-09-10 23:50:36 -04:00
fb0cffc844 Changed to not required. 2023-09-10 23:49:53 -04:00
c6012fa63c Updated readme.me 2023-09-10 23:46:43 -04:00
b25530f063 Successful build of server. Ready for testing. 2023-09-10 23:32:59 -04:00
bb2e8ac538 Check the database first to see if the ID has been set, otherwise, fallback to querying the API based on product name. 2023-09-10 20:56:53 -04:00
07f3c69977 Added changes done by BlinkohHost.
See https://github.com/BlinkohHost/virtfusion-whmcs-module
2023-09-10 20:36:22 -04:00
10 changed files with 383 additions and 133 deletions

38
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,38 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

View File

@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -1,22 +0,0 @@
---
name: Lint Code Base
on: [ workflow_call ]
jobs:
build:
name: Lint Code Base
runs-on: ubuntu-latest
permissions:
contents: read
packages: read
statuses: write
steps:
- name: Checkout Code
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Lint Code Base
uses: super-linter/super-linter@v5
env:
VALIDATE_ALL_CODEBASE: false
DEFAULT_BRANCH: main
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}

View File

@@ -1,22 +1,38 @@
---
name: Publish Release
# .github/workflows/semantic-versioning-release.yml
name: Automated Semantic Versioning Release
on:
push:
branches:
- main
jobs:
linter:
uses: ./.github/workflows/linter.yml
publish-release:
release:
runs-on: ubuntu-latest
needs: linter
permissions:
contents: write # for creating tags and releases
issues: write # for commenting on issues
pull-requests: write # for commenting on PRs
steps:
- name: Publish Release
uses: ncipollo/release-action@v1
- name: Checkout code
uses: actions/checkout@v4
with:
token: ${{secrets.GITHUB_TOKEN}}
draft: false
prerelease: false
name: "0.0.${{ github.run_number }}"
tag: "0.0.${{ github.run_number }}"
body: "Release 0.0.${{ github.run_number }}"
# This is required to analyze the full commit history
fetch-depth: 0
- name: Automated Semantic Release
# This action wraps the popular semantic-release tool
uses: cycjimmy/semantic-release-action@v4
with:
# You can specify the branches to release from
branch: main
env:
# GITHUB_TOKEN is required for authentication
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# To make this work, you must follow the Conventional Commits specification.
# Examples:
# - fix: correct a typo in the documentation
# - feat: add a new user authentication endpoint
# - feat(api): add rate limiting
# BREAKING CHANGE: The API now returns 429 when rate limit is exceeded.

View File

@@ -5,19 +5,33 @@
![GitHub issues](https://img.shields.io/github/issues/EZSCALE/virtfusion-whmcs-module)
![GitHub pull requests](https://img.shields.io/github/issues-pr/EZSCALE/virtfusion-whmcs-module)
This module requires VirtFusion v1.7.3 or higher as this is what it's based on. Please refer to the official [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 [documentation](https://docs.virtfusion.com/integrations/whmcs).
## Installation
1. Download the latest release from the [releases](https://github.com/EZSCALE/virtfusion-whmcs-module/releases) page.
2. Extract the contents of the archive and upload the modules folder to your WHMCS installation directory.
## :heavy_exclamation_mark: Important Notes :heavy_exclamation_mark:
You must create two custom fields in WHMCS for this module to work. You need to configure the following custom fields on
each product you want to use this module with.
| Field Name | Field Type | Description | Validation | Select Options | Admin Only | Required Field | Show on Order Form | Show on Invoice |
|--------------------------|------------|--------------------------|-------------|----------------|------------|----------------|--------------------|-----------------|
| Initial Operating System | Text Box | Set to whatever you want | Leave Blank | Leave Blank | :x: | :x: | :white_check_mark: | :x: |
| Initial SSH Key | Text Box | Set to whatever you want | Leave Blank | Leave Blank | :x: | :x: | :white_check_mark: | :x: |
You can run this SQL query to create the custom fields. Run the SQL from this [file](modify.sql) or copy the contents
from it.
## What does this module change?
This module changes the following things:
- Adds configurable options to the product configuration page to allow the user to select the operating system and add
a ssh key to the initial deployment.
an ssh key to the initial deployment.
## TODO

49
modify.sql Normal file
View File

@@ -0,0 +1,49 @@
-- Insert records for Initial Operating System if they don't already exist
INSERT INTO tblcustomfields
(type, relid, fieldname, fieldtype, description, fieldoptions, regexpr, adminonly, required, showorder, showinvoice,
sortorder, created_at, updated_at)
SELECT 'product',
id,
'Initial Operating System',
'text',
'',
'',
'',
'',
'',
'on',
'',
0,
UTC_TIMESTAMP(),
UTC_TIMESTAMP()
FROM tblproducts
WHERE servertype = 'VirtFusionDirect'
AND NOT EXISTS (SELECT 1
FROM tblcustomfields
WHERE fieldname = 'Initial Operating System'
AND relid = tblproducts.id);
-- Insert records for Initial SSH Key if they don't already exist
INSERT INTO tblcustomfields
(type, relid, fieldname, fieldtype, description, fieldoptions, regexpr, adminonly, required, showorder, showinvoice,
sortorder, created_at, updated_at)
SELECT 'product',
id,
'Initial SSH Key',
'text',
'',
'',
'',
'',
'',
'on',
'',
0,
UTC_TIMESTAMP(),
UTC_TIMESTAMP()
FROM tblproducts
WHERE servertype = 'VirtFusionDirect'
AND NOT EXISTS (SELECT 1
FROM tblcustomfields
WHERE fieldname = 'Initial SSH Key'
AND relid = tblproducts.id);

View File

@@ -83,9 +83,16 @@ function VirtFusionDirect_updateServerObject(array $params)
return (new ModuleFunctions())->updateServerObject($params);
}
/**
* Allows changing of the package of a server
*
* @author https://github.com/BlinkohHost/virtfusion-whmcs-module
* @param array $params
* @return string
*/
function VirtFusionDirect_ChangePackage(array $params)
{
return 'success';
return (new ModuleFunctions())->changePackage($params);
}
function VirtFusionDirect_AdminServicesTabFields(array $params)

View File

@@ -7,82 +7,120 @@ if (!defined("WHMCS")) {
die("This file cannot be accessed directly");
}
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;
}
$cs = new ConfigureService();
$templates_data = $cs->fetchTemplates(
$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']);
});
$osTemplates = [
'id' => 'os_template',
'optionname' => 'Initial Operating System',
'optiontype' => 1,
'options' => $dropdownOptions,
'selectedvalue' => ''
];
$sshKeys = $cs->getUserSshKeys($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('ClientAreaFooterOutput', 1, function ($vars) {
if (!isset($vars['productinfo']['module']) || $vars['productinfo']['module'] !== 'VirtFusionDirect') {
return null;
}
add_hook('ClientAreaPageCart', 1, 'add_hook_os_templates');
}
$cs = new ConfigureService();
$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) {
return null;
}
return [
'id' => $sshKey['id'],
'name' => $sshKey['name']
];
}, $sshKeys['data'] ?? []);
$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'])));
// Construct the JavaScript code
return "
<script>
document.addEventListener('DOMContentLoaded', function() {
let osTemplates = ".json_encode($dropdownOptions, JSON_THROW_ON_ERROR).";
let sshKeys = ".json_encode($sshKeysOptions, JSON_THROW_ON_ERROR).";
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');
osSelect.className = 'form-control';
osTemplates.forEach(function(template) {
let 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';
}
});
</script>
";
});

View File

@@ -43,13 +43,33 @@ class ConfigureService extends Module
return null;
}
/**
* @param int $productId
* @return int|null
*/
public function fetchPackageByDbId(int $productId): ?int
{
$product = DB::table('tblproducts')->where('id', $productId)->first();
if (is_null($product)) {
return null;
}
return (int)$product->configoption2;
}
/**
* @param int $serverPackageId
* @return array|null
* @throws JsonException
*/
public function fetchTemplates(int $serverPackageId): ?array
public function fetchTemplates(?int $serverPackageId): ?array
{
if (is_null($serverPackageId)) {
return null;
}
$request = $this->initCurl($this->cp['token']);
$response = $request->get(
@@ -96,4 +116,38 @@ class ConfigureService extends Module
return isset($response['msg']) && $response['msg'] === "ext_relation_id not found" ? null : $response['data'];
}
/**
* @param int $id
* @param array $vars
* @return bool
*/
public function initServerBuild(int $id, array $vars): bool
{
$request = $this->initCurl($this->cp['token']);
// Generate a random 8 character hostname
$hostname = substr(str_shuffle('abcdefghijklmnopqrstuvwxyz'), 0, 8);
$inputData = [
"operatingSystemId" => $vars['customfields']['Initial Operating System'],
"name" => $hostname,
"sshKeys" => [
$vars['customfields']['Initial SSH Key']
],
'email' => true
];
if (empty($vars['customfields']['Initial SSH Key'])) {
unset($inputData['sshKeys']);
}
$request->addOption(CURLOPT_POSTFIELDS, json_encode($inputData));
$request->post(
sprintf("%s/servers/%d/build", $this->cp['url'], $id)
);
return true;
}
}

View File

@@ -39,7 +39,7 @@ class ModuleFunctions extends Module
*/
$server = $params['serverid'] ?: false;
$cp = $this->getCP($server, $server ? false : true);
$cp = $this->getCP($server, !$server);
if (!$cp) {
return 'No Control server found.';
@@ -82,7 +82,7 @@ class ModuleFunctions extends Module
[
"name" => $user->firstname . ' ' . $user->lastname,
"email" => $user->email,
"extRelationId" => $user->id
"extRelationId" => $user->id,
]
));
@@ -96,7 +96,6 @@ class ModuleFunctions extends Module
break;
default:
return 'Error processing user account.';
break;
}
$data = json_decode($data);
@@ -163,6 +162,10 @@ class ModuleFunctions extends Module
Database::systemOnServerCreate($params['serviceid'], $data);
$this->updateWhmcsServiceParamsOnServerObject($params['serviceid'], $data);
// If the server is created successfully, we can initialize the server build.
$cs = new ConfigureService();
$cs->initServerBuild($data->data->id, $params);
/**
*
* Server was created successfully.
@@ -181,6 +184,49 @@ class ModuleFunctions extends Module
}
}
// This function was implemented by Zander Scott / awildboop of Blinkoh, LLC
// Please read this function thoroughly before use to ensure security & integrity
/**
* Allows changing of the package of a server
*
* @author https://github.com/BlinkohHost/virtfusion-whmcs-module
*
* @param $params
*
* @return string
*/
public function changePackage($params)
{
$service = Database::getSystemService($params['serviceid']);
if ($service) {
$whmcsService = Database::getWhmcsService($params['serviceid']);
$cp = $this->getCP($whmcsService->server);
$request = $this->initCurl($cp['token']);
$data = $request->put($cp['url'] . '/servers/' . $service->server_id . '/package/' . $params['configoption2']);
$data = json_decode($data);
Log::insert(__FUNCTION__, $request->getRequestInfo(), $data);
switch ($request->getRequestInfo('http_code')) {
case 204:
return 'success';
case 404:
return '404 was returned from the web service without the msg property. The service may be currently unavailable.';
case 423:
if (property_exists($data, 'msg')) {
return $data->msg;
}
break;
default:
return 'Update package request failed. The web service reported HTTP code ' . $request->getRequestInfo('http_code');
}
}
return 'Service not found.';
}
/**
*
* TERMINATE SERVER
@@ -212,7 +258,6 @@ class ModuleFunctions extends Module
Database::deleteSystemService($params['serviceid']);
$this->updateWhmcsServiceParamsOnDestroy($params['serviceid']);
return 'success';
break;
case 404:
if (property_exists($data, 'msg')) {
@@ -225,11 +270,9 @@ class ModuleFunctions extends Module
} 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?';
@@ -263,7 +306,6 @@ class ModuleFunctions extends Module
case 204:
return 'success';
break;
case 404:
if (property_exists($data, 'msg')) {
@@ -277,7 +319,6 @@ class ModuleFunctions extends Module
} 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;
@@ -285,7 +326,6 @@ class ModuleFunctions extends Module
default:
return 'Suspend request failed. The web service reported HTTP code ' . $request->getRequestInfo('http_code');
break;
}
}
return 'Service not found.';
@@ -314,10 +354,8 @@ class ModuleFunctions extends Module
$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.';
@@ -342,7 +380,6 @@ class ModuleFunctions extends Module
case 204:
return 'success';
break;
case 404:
if (property_exists($data, 'msg')) {
@@ -355,15 +392,14 @@ class ModuleFunctions extends Module
} 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;
}
break;
default:
return 'Unsuspend request failed. The web service reported HTTP code ' . $request->getRequestInfo('http_code');
break;
}
}
return 'Service not found';