From d253bd44e6b165ca256dc1b0629864f171d311d4 Mon Sep 17 00:00:00 2001 From: Prophet731 Date: Thu, 19 Mar 2026 15:03:17 -0500 Subject: [PATCH] feat: auto-create custom fields, add try/catch coverage, PHPDoc, and Pint formatting - Auto-create 'Initial Operating System' and 'Initial SSH Key' custom fields via Database::ensureCustomFields() on module load, eliminating the manual modify.sql step - Delete modify.sql (no longer needed) - Add try/catch blocks around every DB operation and API call across all PHP files per CLAUDE.md error handling rules - Add comprehensive PHPDoc to all classes, methods, and properties - Set up Laravel Pint (laravel/pint) with Laravel-style preset for consistent code formatting across the codebase - Add git pre-commit hook (hooks/pre-commit) that runs Pint on staged PHP files, auto-installed via Composer post-install/post-update scripts - Simplify README installation to a single copy-paste command Co-Authored-By: Claude Opus 4.6 (1M context) --- .gitignore | 3 +- CLAUDE.md | 2 + README.md | 101 +- composer.json | 19 + composer.lock | 87 ++ hooks/pre-commit | 26 + .../VirtFusionDirect/VirtFusionDirect.php | 202 ++-- modules/servers/VirtFusionDirect/admin.php | 141 +-- modules/servers/VirtFusionDirect/client.php | 677 ++++++------- .../config/ConfigOptionMapping-example.php | 6 +- modules/servers/VirtFusionDirect/hooks.php | 103 +- .../VirtFusionDirect/lib/AdminHTML.php | 36 +- .../servers/VirtFusionDirect/lib/Cache.php | 33 +- .../VirtFusionDirect/lib/ConfigureService.php | 391 +++++--- modules/servers/VirtFusionDirect/lib/Curl.php | 87 +- .../servers/VirtFusionDirect/lib/Database.php | 298 +++++- modules/servers/VirtFusionDirect/lib/Log.php | 22 +- .../servers/VirtFusionDirect/lib/Module.php | 899 +++++++++++------- .../VirtFusionDirect/lib/ModuleFunctions.php | 643 ++++++++----- .../VirtFusionDirect/lib/ServerResource.php | 9 + modules/servers/VirtFusionDirect/modify.sql | 49 - pint.json | 24 + 22 files changed, 2384 insertions(+), 1474 deletions(-) create mode 100644 composer.json create mode 100644 composer.lock create mode 100755 hooks/pre-commit delete mode 100644 modules/servers/VirtFusionDirect/modify.sql create mode 100644 pint.json diff --git a/.gitignore b/.gitignore index 1320326..7ebbd2c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /.idea/ -/.superpowers/ \ No newline at end of file +/.superpowers/ +/vendor/ \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index 3560560..33b17d4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -91,6 +91,8 @@ The `publish-release.yml` workflow creates a GitHub/Gitea release with auto-gene 6. Updates WHMCS hosting record (IP, username, password, domain) 7. Calls `ConfigureService::initServerBuild()` with selected OS + SSH key +Custom fields (`Initial Operating System`, `Initial SSH Key`) are auto-created by `Database::ensureCustomFields()` on module load for all products using this module. No manual SQL setup required. + ### Configurable Option Mapping Custom option names can be mapped in `config/ConfigOptionMapping.php` (copy from `-example.php`). Default mapping keys: `packageId`, `hypervisorId`, `ipv4`, `storage`, `memory`, `traffic`, `cpuCores`, `networkSpeedInbound`, `networkSpeedOutbound`, `networkProfile`, `storageProfile`. diff --git a/README.md b/README.md index 39a960d..acf349f 100644 --- a/README.md +++ b/README.md @@ -108,95 +108,28 @@ You also need a VirtFusion API token with the following permissions: ## Installation -### Step 1: Download & Install - -Download the latest release from the [releases](https://github.com/EZSCALE/virtfusion-whmcs-module/releases) page, or install directly via the command line: - ```bash -cd /tmp -git clone https://github.com/EZSCALE/virtfusion-whmcs-module.git -rsync -ahP --delete /tmp/virtfusion-whmcs-module/modules/servers/VirtFusionDirect/ /path/to/whmcs/modules/servers/VirtFusionDirect/ -rm -rf /tmp/virtfusion-whmcs-module +git clone https://github.com/EZSCALE/virtfusion-whmcs-module.git /tmp/vf && rsync -ahP --delete /tmp/vf/modules/servers/VirtFusionDirect/ /path/to/whmcs/modules/servers/VirtFusionDirect/ && rm -rf /tmp/vf ``` -Replace `/path/to/whmcs` with your actual WHMCS installation root. +Replace `/path/to/whmcs` with your actual WHMCS installation root. The database table, schema migrations, and custom fields are all created automatically on first load. -The resulting file structure should be: +Then configure in WHMCS Admin: -``` -modules/servers/VirtFusionDirect/ - VirtFusionDirect.php # Main module file - client.php # Client AJAX API - admin.php # Admin AJAX API - hooks.php # WHMCS hooks - modify.sql # Custom field setup SQL - lib/ - Module.php # Core module class - ModuleFunctions.php # Provisioning functions - ConfigureService.php # OS/SSH config service - Database.php # Database operations - Curl.php # HTTP client - ServerResource.php # Data transformer - AdminHTML.php # Admin interface HTML - Log.php # Logging - templates/ - overview.tpl # Client area template - error.tpl # Error template - css/module.css # Styles - js/module.js # Client JavaScript - js/keygen.js # SSH Ed25519 key generator - config/ - ConfigOptionMapping-example.php # Config mapping example -``` +1. **Add Server** — Configuration > System Settings > Servers > Add New Server. Set hostname to your VirtFusion panel (e.g. `cp.example.com`), type to "VirtFusion Direct Provisioning", and paste your API token in the Password field. Click **Test Connection** to verify. +2. **Create Product** — Configuration > System Settings > Products/Services. On the Module Settings tab, select "VirtFusion Direct Provisioning", choose your server, and set the Hypervisor Group ID, Package ID, and Default IPv4 count. -### Step 2: Set Up Server in WHMCS - -1. Go to **Configuration > System Settings > Servers** -2. Click **Add New Server** -3. Fill in: - - **Name**: Anything descriptive (e.g., "VirtFusion Production") - - **Hostname**: Your VirtFusion panel hostname (e.g., `cp.example.com`) - - **Type**: VirtFusion Direct Provisioning - - **Password/Access Hash**: Your VirtFusion API token -4. Click **Test Connection** to verify -5. Click **Save Changes** - -### Step 3: Create Product - -1. Go to **Configuration > System Settings > Products/Services** -2. Create a new product or edit an existing one -3. On the **Module Settings** tab: - - Set **Module Name** to "VirtFusion Direct Provisioning" - - Select your VirtFusion server - - Set **Hypervisor Group ID**, **Package ID**, and **Default IPv4** count -4. Save the product - -### Step 4: Set Up Custom Fields - -See [Custom Fields](#custom-fields) section below. - -### Step 5: Activate Hooks - -The hooks file (`hooks.php`) is automatically detected by WHMCS when the module is active. If you add the module files to an existing installation, you may need to re-save the product settings or clear the WHMCS template cache for hooks to take effect. +That's it. Hooks activate automatically and custom fields are created on module load. ## Upgrading -1. Back up your existing `modules/servers/VirtFusionDirect/` directory -2. Back up `config/ConfigOptionMapping.php` if you have a custom mapping -3. Download and deploy the new version: - ```bash -cd /tmp -git clone https://github.com/EZSCALE/virtfusion-whmcs-module.git -rsync -ahP --delete /tmp/virtfusion-whmcs-module/modules/servers/VirtFusionDirect/ /path/to/whmcs/modules/servers/VirtFusionDirect/ -rm -rf /tmp/virtfusion-whmcs-module +git clone https://github.com/EZSCALE/virtfusion-whmcs-module.git /tmp/vf && rsync -ahP --delete /tmp/vf/modules/servers/VirtFusionDirect/ /path/to/whmcs/modules/servers/VirtFusionDirect/ && rm -rf /tmp/vf ``` -4. Restore your custom `config/ConfigOptionMapping.php` if applicable -5. If you have theme-overridden templates, review them for any new template variables -6. Clear the WHMCS template cache: **Configuration > System Settings > General Settings > clear template cache** +> **Note:** If you have a custom `config/ConfigOptionMapping.php`, back it up first — `--delete` will remove it. Restore it after upgrading. -The module database table (`mod_virtfusion_direct`) is automatically migrated on first load. +If you use theme-overridden templates, review them for any new template variables. Clear the WHMCS template cache after upgrading: **Configuration > System Settings > General Settings > clear template cache**. ## Configuration @@ -222,20 +155,9 @@ Each WHMCS product using this module needs: ### Custom Fields -You **must** create two custom fields on each product that uses this module: +The module requires two custom fields per product: **Initial Operating System** and **Initial SSH Key**. These are **automatically created** when the module loads — no manual setup required. -| Field Name | Field Type | Show on Order Form | Admin Only | Required | -|---|---|---|---|---| -| Initial Operating System | Text Box | Yes | No | No | -| Initial SSH Key | Text Box | Yes | No | No | - -These fields are hidden text boxes that are dynamically replaced by dropdown selects via JavaScript hooks on the order form. - -**Automated setup**: Run the SQL from [modify.sql](modify.sql) to auto-create these fields for all VirtFusion products: - -```bash -mysql -u whmcs_user -p whmcs_database < modules/servers/VirtFusionDirect/modify.sql -``` +The fields are hidden text boxes that are dynamically replaced by dropdown selects via JavaScript hooks on the order form. They are created for every product with the module type set to "VirtFusion Direct Provisioning". ### Module Configuration Options @@ -559,7 +481,6 @@ modules/servers/VirtFusionDirect/ client.php # Client-facing AJAX API (authenticated, ownership-validated) admin.php # Admin-facing AJAX API (admin authentication required) hooks.php # WHMCS hooks (order form OS/SSH dropdowns, checkout validation) - modify.sql # SQL for creating custom fields lib/ Module.php # Base class: API communication, power, network, VNC, rebuild ModuleFunctions.php # Provisioning: create, suspend, unsuspend, terminate, change package diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..9898816 --- /dev/null +++ b/composer.json @@ -0,0 +1,19 @@ +{ + "name": "ezscale/virtfusion-whmcs-module", + "description": "VirtFusion Direct Provisioning Module for WHMCS", + "type": "whmcs-module", + "license": "GPL-3.0-or-later", + "require-dev": { + "laravel/pint": "^1.0" + }, + "scripts": { + "post-install-cmd": [ + "cp hooks/pre-commit .git/hooks/pre-commit && chmod +x .git/hooks/pre-commit" + ], + "post-update-cmd": [ + "cp hooks/pre-commit .git/hooks/pre-commit && chmod +x .git/hooks/pre-commit" + ], + "lint": "pint", + "lint-test": "pint --test" + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..e0c1fa7 --- /dev/null +++ b/composer.lock @@ -0,0 +1,87 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "f6be98eb2bded4b127a92bc0f1e19d93", + "packages": [], + "packages-dev": [ + { + "name": "laravel/pint", + "version": "v1.29.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/pint.git", + "reference": "bdec963f53172c5e36330f3a400604c69bf02d39" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/pint/zipball/bdec963f53172c5e36330f3a400604c69bf02d39", + "reference": "bdec963f53172c5e36330f3a400604c69bf02d39", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-mbstring": "*", + "ext-tokenizer": "*", + "ext-xml": "*", + "php": "^8.2.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.94.2", + "illuminate/view": "^12.54.1", + "larastan/larastan": "^3.9.3", + "laravel-zero/framework": "^12.0.5", + "mockery/mockery": "^1.6.12", + "nunomaduro/termwind": "^2.4.0", + "pestphp/pest": "^3.8.6", + "shipfastlabs/agent-detector": "^1.1.0" + }, + "bin": [ + "builds/pint" + ], + "type": "project", + "autoload": { + "psr-4": { + "App\\": "app/", + "Database\\Seeders\\": "database/seeders/", + "Database\\Factories\\": "database/factories/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "An opinionated code formatter for PHP.", + "homepage": "https://laravel.com", + "keywords": [ + "dev", + "format", + "formatter", + "lint", + "linter", + "php" + ], + "support": { + "issues": "https://github.com/laravel/pint/issues", + "source": "https://github.com/laravel/pint" + }, + "time": "2026-03-12T15:51:39+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": {}, + "platform-dev": {}, + "plugin-api-version": "2.9.0" +} diff --git a/hooks/pre-commit b/hooks/pre-commit new file mode 100755 index 0000000..6bdd7c1 --- /dev/null +++ b/hooks/pre-commit @@ -0,0 +1,26 @@ +#!/bin/bash + +# Run Pint on staged PHP files before committing. +# Fixes formatting in-place and re-stages the corrected files. + +STAGED_PHP=$(git diff --cached --name-only --diff-filter=ACM | grep '\.php$') + +if [ -z "$STAGED_PHP" ]; then + exit 0 +fi + +# Check that Pint is installed +if [ ! -x "./vendor/bin/pint" ]; then + echo "Error: laravel/pint is not installed. Run 'composer install' first." + exit 1 +fi + +echo "Running Pint on staged PHP files..." +./vendor/bin/pint $STAGED_PHP + +# Re-stage any files that Pint modified +for FILE in $STAGED_PHP; do + if [ -f "$FILE" ]; then + git add "$FILE" + fi +done diff --git a/modules/servers/VirtFusionDirect/VirtFusionDirect.php b/modules/servers/VirtFusionDirect/VirtFusionDirect.php index dd86c9d..2a3247c 100644 --- a/modules/servers/VirtFusionDirect/VirtFusionDirect.php +++ b/modules/servers/VirtFusionDirect/VirtFusionDirect.php @@ -1,13 +1,20 @@ [ - "FriendlyName" => "Hypervisor Group ID", - "Type" => "text", - "Size" => "20", - "Description" => "The default hypervisor group ID for server placement.", - "Default" => "1", + 'defaultHypervisorGroupId' => [ + 'FriendlyName' => 'Hypervisor Group ID', + 'Type' => 'text', + 'Size' => '20', + 'Description' => 'The default hypervisor group ID for server placement.', + 'Default' => '1', ], - "packageID" => [ - "FriendlyName" => "Package ID", - "Type" => "text", - "Size" => "20", - "Description" => "The VirtFusion package ID that defines server resources.", - "Default" => "1", + 'packageID' => [ + 'FriendlyName' => 'Package ID', + 'Type' => 'text', + 'Size' => '20', + 'Description' => 'The VirtFusion package ID that defines server resources.', + 'Default' => '1', ], - "defaultIPv4" => [ - "FriendlyName" => "Default IPv4", - "Type" => "dropdown", - "Options" => "0,1,2,3,4,5,6,7,8,9,10", - "Description" => "The default number of IPv4 addresses to assign to each server.", - "Default" => "1", + 'defaultIPv4' => [ + 'FriendlyName' => 'Default IPv4', + 'Type' => 'dropdown', + 'Options' => '0,1,2,3,4,5,6,7,8,9,10', + 'Description' => 'The default number of IPv4 addresses to assign to each server.', + 'Default' => '1', ], - "selfServiceMode" => [ - "FriendlyName" => "Self-Service Mode", - "Type" => "dropdown", - "Options" => "0|Disabled,1|Hourly,2|Resource Packs,3|Both", - "Description" => "Enable VirtFusion self-service billing for users created by this product.", - "Default" => "0", + 'selfServiceMode' => [ + 'FriendlyName' => 'Self-Service Mode', + 'Type' => 'dropdown', + 'Options' => '0|Disabled,1|Hourly,2|Resource Packs,3|Both', + 'Description' => 'Enable VirtFusion self-service billing for users created by this product.', + 'Default' => '0', ], - "autoTopOffThreshold" => [ - "FriendlyName" => "Auto Top-Off Threshold", - "Type" => "text", - "Size" => "10", - "Description" => "Credit balance below which auto top-off triggers during cron. 0 = disabled.", - "Default" => "0", + 'autoTopOffThreshold' => [ + 'FriendlyName' => 'Auto Top-Off Threshold', + 'Type' => 'text', + 'Size' => '10', + 'Description' => 'Credit balance below which auto top-off triggers during cron. 0 = disabled.', + 'Default' => '0', ], - "autoTopOffAmount" => [ - "FriendlyName" => "Auto Top-Off Amount", - "Type" => "text", - "Size" => "10", - "Description" => "Credit amount to add when auto top-off triggers.", - "Default" => "100", + 'autoTopOffAmount' => [ + 'FriendlyName' => 'Auto Top-Off Amount', + 'Type' => 'text', + 'Size' => '10', + 'Description' => 'Credit amount to add when auto top-off triggers.', + 'Default' => '100', ], ]; } @@ -78,7 +90,7 @@ function VirtFusionDirect_TestConnection(array $params) } $url = 'https://' . $hostname . '/api/v1'; - $module = new Module(); + $module = new Module; $request = $module->initCurl($password); $data = $request->get($url . '/connect'); @@ -94,27 +106,33 @@ function VirtFusionDirect_TestConnection(array $params) if ($httpCode == 0) { $curlError = $request->getRequestInfo('curl_error'); + return ['success' => false, 'error' => 'Connection failed: ' . ($curlError ?: 'Unable to reach the VirtFusion server. Verify the hostname and that SSL certificates are valid.')]; } return ['success' => false, 'error' => 'Unexpected response from VirtFusion API (HTTP ' . $httpCode . '). Please check the server configuration.']; - } catch (\Throwable $e) { + } catch (Throwable $e) { return ['success' => false, 'error' => 'Connection test failed: ' . $e->getMessage()]; } } +/** + * Returns custom admin action buttons shown on the service management page. + * + * @return array Button label => function suffix pairs + */ function VirtFusionDirect_AdminCustomButtonArray() { return [ - "Update Server Object" => "updateServerObject", - "Validate Server Config" => "validateServerConfig", + 'Update Server Object' => 'updateServerObject', + 'Validate Server Config' => 'validateServerConfig', ]; } function VirtFusionDirect_ServiceSingleSignOn(array $params) { try { - $module = new Module(); + $module = new Module; $token = $module->fetchLoginTokens($params['serviceid']); if ($token) { @@ -122,7 +140,7 @@ function VirtFusionDirect_ServiceSingleSignOn(array $params) } return ['success' => false, 'errorMsg' => 'Unable to generate a login token. The server may not be active or the VirtFusion API may be unreachable.']; - } catch (\Exception $e) { + } catch (Exception $e) { return ['success' => false, 'errorMsg' => $e->getMessage()]; } } @@ -132,64 +150,104 @@ function VirtFusionDirect_ServiceSingleSignOn(array $params) */ function VirtFusionDirect_CreateAccount(array $params) { - return (new ModuleFunctions())->createAccount($params); + return (new ModuleFunctions)->createAccount($params); } +/** + * Suspends the VirtFusion server associated with a WHMCS service. + * + * @param array $params WHMCS module parameters + * @return string 'success' or error message + */ function VirtFusionDirect_SuspendAccount(array $params) { - return (new ModuleFunctions())->suspendAccount($params); + return (new ModuleFunctions)->suspendAccount($params); } +/** + * Unsuspends the VirtFusion server associated with a WHMCS service. + * + * @param array $params WHMCS module parameters + * @return string 'success' or error message + */ function VirtFusionDirect_UnsuspendAccount(array $params) { - return (new ModuleFunctions())->unsuspendAccount($params); + return (new ModuleFunctions)->unsuspendAccount($params); } +/** + * Terminates (deletes) the VirtFusion server associated with a WHMCS service. + * + * @param array $params WHMCS module parameters + * @return string 'success' or error message + */ function VirtFusionDirect_TerminateAccount(array $params) { - return (new ModuleFunctions())->terminateAccount($params); + return (new ModuleFunctions)->terminateAccount($params); } +/** + * Admin custom action: refreshes the local server object from the VirtFusion API. + * + * @param array $params WHMCS module parameters + * @return string 'success' or error message + */ function VirtFusionDirect_updateServerObject(array $params) { - return (new ModuleFunctions())->updateServerObject($params); + return (new ModuleFunctions)->updateServerObject($params); } /** * Allows changing of the package of a server * - * @param array $params * @return string */ function VirtFusionDirect_ChangePackage(array $params) { - return (new ModuleFunctions())->changePackage($params); + return (new ModuleFunctions)->changePackage($params); } +/** + * Returns HTML fields rendered in the custom admin services tab. + * + * @param array $params WHMCS module parameters + * @return array Field name => HTML value pairs + */ function VirtFusionDirect_AdminServicesTabFields(array $params) { - return (new ModuleFunctions())->adminServicesTabFields($params); + return (new ModuleFunctions)->adminServicesTabFields($params); } +/** + * Handles saving of custom admin services tab field values. + * + * @param array $params WHMCS module parameters + * @return void + */ function VirtFusionDirect_AdminServicesTabFieldsSave(array $params) { - (new ModuleFunctions())->adminServicesTabFieldsSave($params); + (new ModuleFunctions)->adminServicesTabFieldsSave($params); } +/** + * Returns the client area template variables and template name for the service overview page. + * + * @param array $params WHMCS module parameters + * @return array Smarty template variables and 'templatefile' key + */ function VirtFusionDirect_ClientArea(array $params) { - return (new ModuleFunctions())->clientArea($params); + return (new ModuleFunctions)->clientArea($params); } /** * Validates server configuration via dry run without creating the server. * - * @param array $params * @return string 'success' or error message */ function VirtFusionDirect_validateServerConfig(array $params) { - return (new ModuleFunctions())->validateServerConfig($params); + return (new ModuleFunctions)->validateServerConfig($params); } /** @@ -198,20 +256,20 @@ function VirtFusionDirect_validateServerConfig(array $params) * Updates tblhosting with disk and bandwidth usage data from VirtFusion. * Fields updated: diskused, disklimit, bwused, bwlimit, lastupdate * - * @param array $params Server access credentials + * @param array $params Server access credentials * @return string 'success' or error message */ function VirtFusionDirect_UsageUpdate(array $params) { try { - $module = new Module(); + $module = new Module; $cp = $module->getCP($params['serverid']); - if (!$cp) { + if (! $cp) { return 'No control server found for usage update.'; } - $services = \WHMCS\Database\Capsule::table('tblhosting') + $services = Capsule::table('tblhosting') ->where('server', $params['serverid']) ->where('domainstatus', 'Active') ->get(); @@ -219,7 +277,7 @@ function VirtFusionDirect_UsageUpdate(array $params) foreach ($services as $service) { try { $systemService = Database::getSystemService($service->id); - if (!$systemService) { + if (! $systemService) { continue; } @@ -231,7 +289,7 @@ function VirtFusionDirect_UsageUpdate(array $params) } $serverData = json_decode($data, true); - if (!isset($serverData['data'])) { + if (! isset($serverData['data'])) { continue; } @@ -255,15 +313,15 @@ function VirtFusionDirect_UsageUpdate(array $params) $update['bwlimit'] = $trafficGB > 0 ? $trafficGB * 1024 : 0; } - if (!empty($update)) { + if (! empty($update)) { $update['lastupdate'] = date('Y-m-d H:i:s'); - \WHMCS\Database\Capsule::table('tblhosting') + Capsule::table('tblhosting') ->where('id', $service->id) ->update($update); } // Self-service auto top-off - $product = \WHMCS\Database\Capsule::table('tblproducts') + $product = Capsule::table('tblproducts') ->where('id', $service->packageid) ->first(); @@ -278,24 +336,24 @@ function VirtFusionDirect_UsageUpdate(array $params) $credit = $usageInner['credit'] ?? $usageInner['balance'] ?? null; if ($credit !== null && (float) $credit < $threshold) { $module->addSelfServiceCredit($service->id, $topOffAmount, 'Auto top-off'); - \WHMCS\Module\Server\VirtFusionDirect\Log::insert( + Log::insert( 'UsageUpdate:autoTopOff', ['serviceId' => $service->id, 'credit' => $credit, 'threshold' => $threshold], - ['amount' => $topOffAmount] + ['amount' => $topOffAmount], ); } } } } - } catch (\Exception $e) { + } catch (Exception $e) { // Log but continue processing other services - \WHMCS\Module\Server\VirtFusionDirect\Log::insert('UsageUpdate:service:' . $service->id, [], $e->getMessage()); + Log::insert('UsageUpdate:service:' . $service->id, [], $e->getMessage()); continue; } } return 'success'; - } catch (\Exception $e) { + } catch (Exception $e) { return 'Usage update failed: ' . $e->getMessage(); } } diff --git a/modules/servers/VirtFusionDirect/admin.php b/modules/servers/VirtFusionDirect/admin.php index e97e498..566c675 100644 --- a/modules/servers/VirtFusionDirect/admin.php +++ b/modules/servers/VirtFusionDirect/admin.php @@ -2,82 +2,97 @@ require dirname(__DIR__, 3) . '/init.php'; +/** + * Admin-facing AJAX API endpoint. + * + * Requires WHMCS admin authentication. Provides server data lookup + * and user impersonation for the admin services tab. + */ + use WHMCS\Module\Server\VirtFusionDirect\Database; +use WHMCS\Module\Server\VirtFusionDirect\Log; use WHMCS\Module\Server\VirtFusionDirect\Module; use WHMCS\Module\Server\VirtFusionDirect\ServerResource; -$vf = new Module(); +$vf = new Module; -$vf->adminOnly(); +try { -switch ($vf->validateAction(true)) { + $vf->adminOnly(); - /** - * Get server information. - */ - case 'serverData': + switch ($vf->validateAction(true)) { - $serviceID = $vf->validateServiceID(true); + /** + * Get server information. + */ + case 'serverData': - $whmcsService = Database::getWhmcsService($serviceID); + $serviceID = $vf->validateServiceID(true); - if (!$whmcsService) { - $vf->output(['success' => false, 'errors' => 'Service not found.'], true, true, 404); + $whmcsService = Database::getWhmcsService($serviceID); + + if (! $whmcsService) { + $vf->output(['success' => false, 'errors' => 'Service not found.'], true, true, 404); + break; + } + + if (in_array($whmcsService->domainstatus, ['Pending', 'Terminated', 'Cancelled', 'Fraud'], true)) { + $vf->output(['success' => false, 'errors' => 'Server is not Active, Suspended or Completed. Not fetching remote data.'], true, true, 400); + break; + } + + $data = $vf->fetchServerData($serviceID); + + if (! $data) { + $vf->output(['success' => false, 'errors' => 'No data returned from VirtFusion.'], true, true, 502); + break; + } + + $vf->updateWhmcsServiceParamsOnServerObject($serviceID, $data); + $vf->output(['success' => true, 'data' => (new ServerResource)->process($data)], true, true, 200); break; - } - if (in_array($whmcsService->domainstatus, ['Pending', 'Terminated', 'Cancelled', 'Fraud'], true)) { - $vf->output(['success' => false, 'errors' => 'Server is not Active, Suspended or Completed. Not fetching remote data.'], true, true, 400); + /** + * Impersonate server owner. + */ + case 'impersonateServerOwner': + + $serviceID = $vf->validateServiceID(true); + + $service = Database::getSystemService($serviceID); + if (! $service) { + $vf->output(['success' => false, 'errors' => 'Service not found'], true, true, 404); + break; + } + + $whmcsService = Database::getWhmcsService($serviceID); + if (! $whmcsService) { + $vf->output(['success' => false, 'errors' => 'WHMCS service not found'], true, true, 404); + break; + } + + $cp = $vf->getCP($whmcsService->server); + if (! $cp) { + $vf->output(['success' => false, 'errors' => 'Control server not found'], true, true, 500); + break; + } + + $request = $vf->initCurl($cp['token']); + $data = $request->get($cp['url'] . '/users/' . (int) $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); + break; + } + + $vf->output(['success' => false, 'errors' => 'Unable to fetch user data'], true, true, 502); break; - } - $data = $vf->fetchServerData($serviceID); + default: + $vf->output(['success' => false, 'errors' => 'invalid action'], true, true, 400); + } - if (!$data) { - $vf->output(['success' => false, 'errors' => 'No data returned from VirtFusion.'], true, true, 502); - break; - } - - $vf->updateWhmcsServiceParamsOnServerObject($serviceID, $data); - $vf->output(['success' => true, 'data' => (new ServerResource())->process($data)], true, true, 200); - break; - - /** - * Impersonate server owner. - */ - case 'impersonateServerOwner': - - $serviceID = $vf->validateServiceID(true); - - $service = Database::getSystemService($serviceID); - if (!$service) { - $vf->output(['success' => false, 'errors' => 'Service not found'], true, true, 404); - break; - } - - $whmcsService = Database::getWhmcsService($serviceID); - if (!$whmcsService) { - $vf->output(['success' => false, 'errors' => 'WHMCS service not found'], true, true, 404); - break; - } - - $cp = $vf->getCP($whmcsService->server); - if (!$cp) { - $vf->output(['success' => false, 'errors' => 'Control server not found'], true, true, 500); - break; - } - - $request = $vf->initCurl($cp['token']); - $data = $request->get($cp['url'] . '/users/' . (int) $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); - break; - } - - $vf->output(['success' => false, 'errors' => 'Unable to fetch user data'], true, true, 502); - break; - - default: - $vf->output(['success' => false, 'errors' => 'invalid action'], true, true, 400); +} catch (Exception $e) { + Log::insert('admin.php', [], $e->getMessage()); + $vf->output(['success' => false, 'errors' => 'An unexpected error occurred'], true, true, 500); } diff --git a/modules/servers/VirtFusionDirect/client.php b/modules/servers/VirtFusionDirect/client.php index 6e60383..1d3ee9a 100644 --- a/modules/servers/VirtFusionDirect/client.php +++ b/modules/servers/VirtFusionDirect/client.php @@ -2,399 +2,414 @@ require dirname(__DIR__, 3) . '/init.php'; +/** + * Client-facing AJAX API endpoint. + * + * Authenticated by WHMCS session + service ownership validation. + * POST for mutations (power, rebuild, rename, credit), GET for reads (serverData, templates, backups). + */ + +use WHMCS\Module\Server\VirtFusionDirect\Log; use WHMCS\Module\Server\VirtFusionDirect\Module; use WHMCS\Module\Server\VirtFusionDirect\ServerResource; -$vf = new Module(); +$vf = new Module; -$vf->isAuthenticated(); +try { -$action = $vf->validateAction(true); + $vf->isAuthenticated(); -switch ($action) { + $action = $vf->validateAction(true); - /** - * Reset Password. - */ - case 'resetPassword': + switch ($action) { - $serviceID = $vf->validateServiceID(true); - $client = $vf->validateUserOwnsService($serviceID); + /** + * Reset Password. + */ + case 'resetPassword': - if (!$client) { - $vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403); + $serviceID = $vf->validateServiceID(true); + $client = $vf->validateUserOwnsService($serviceID); + + if (! $client) { + $vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403); + break; + } + + $data = $vf->resetUserPassword($serviceID, $client); + + if ($data) { + $vf->output(['success' => true, 'data' => $data->data], true, true, 200); + break; + } + + $vf->output(['success' => false, 'errors' => 'Password reset failed'], true, true, 500); break; - } - $data = $vf->resetUserPassword($serviceID, $client); + /** + * Get server information. + */ + case 'serverData': - if ($data) { - $vf->output(['success' => true, 'data' => $data->data], true, true, 200); + $serviceID = $vf->validateServiceID(true); + + if (! $vf->validateUserOwnsService($serviceID)) { + $vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403); + break; + } + + $data = $vf->fetchServerData($serviceID); + + if ($data) { + $vf->updateWhmcsServiceParamsOnServerObject($serviceID, $data); + $vf->output(['success' => true, 'data' => (new ServerResource)->process($data)], true, true, 200); + break; + } + + $vf->output(['success' => false, 'errors' => 'Unable to retrieve server data'], true, true, 500); break; - } - $vf->output(['success' => false, 'errors' => 'Password reset failed'], true, true, 500); - break; + /** + * Login as server owner. + */ + case 'loginAsServerOwner': - /** - * Get server information. - */ - case 'serverData': + $serviceID = $vf->validateServiceID(true); - $serviceID = $vf->validateServiceID(true); + if (! $vf->validateUserOwnsService($serviceID)) { + $vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403); + break; + } - if (!$vf->validateUserOwnsService($serviceID)) { - $vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403); + $token = $vf->fetchLoginTokens($serviceID); + + if ($token) { + $vf->output(['success' => true, 'token_url' => $token], true, true, 200); + break; + } + + $vf->output(['success' => false, 'errors' => 'Unable to generate login token'], true, true, 500); break; - } - $data = $vf->fetchServerData($serviceID); + /** + * Power management actions: boot, shutdown, restart, poweroff + */ + case 'powerAction': - if ($data) { - $vf->updateWhmcsServiceParamsOnServerObject($serviceID, $data); - $vf->output(['success' => true, 'data' => (new ServerResource())->process($data)], true, true, 200); + $serviceID = $vf->validateServiceID(true); + + if (! $vf->validateUserOwnsService($serviceID)) { + $vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403); + break; + } + + $powerAction = isset($_POST['powerAction']) ? preg_replace('/[^a-zA-Z]/', '', $_POST['powerAction']) : ''; + $allowedActions = ['boot', 'shutdown', 'restart', 'poweroff']; + + if (! in_array($powerAction, $allowedActions, true)) { + $vf->output(['success' => false, 'errors' => 'Invalid power action'], true, true, 400); + break; + } + + $result = $vf->serverPowerAction($serviceID, $powerAction); + + if ($result) { + $vf->output(['success' => true, 'data' => ['action' => $powerAction, 'message' => 'Power action queued successfully']], true, true, 200); + break; + } + + $vf->output(['success' => false, 'errors' => 'Power action failed. The server may be locked or unavailable.'], true, true, 500); break; - } - $vf->output(['success' => false, 'errors' => 'Unable to retrieve server data'], true, true, 500); - break; + /** + * Rebuild/reinstall server with new OS. + */ + case 'rebuild': - /** - * Login as server owner. - */ - case 'loginAsServerOwner': + $serviceID = $vf->validateServiceID(true); - $serviceID = $vf->validateServiceID(true); + if (! $vf->validateUserOwnsService($serviceID)) { + $vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403); + break; + } - if (!$vf->validateUserOwnsService($serviceID)) { - $vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403); + $osId = isset($_POST['osId']) ? (int) $_POST['osId'] : 0; + $hostname = isset($_POST['hostname']) ? preg_replace('/[^a-zA-Z0-9.\-]/', '', $_POST['hostname']) : null; + + if ($osId <= 0) { + $vf->output(['success' => false, 'errors' => 'Invalid operating system ID'], true, true, 400); + break; + } + + $result = $vf->rebuildServer($serviceID, $osId, $hostname); + + if ($result) { + $vf->output(['success' => true, 'data' => ['message' => 'Server rebuild initiated successfully']], true, true, 200); + break; + } + + $vf->output(['success' => false, 'errors' => 'Server rebuild failed. The server may be locked or unavailable.'], true, true, 500); break; - } - $token = $vf->fetchLoginTokens($serviceID); + /** + * Rename server. + */ + case 'rename': - if ($token) { - $vf->output(['success' => true, 'token_url' => $token], true, true, 200); + $serviceID = $vf->validateServiceID(true); + + if (! $vf->validateUserOwnsService($serviceID)) { + $vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403); + break; + } + + $newName = isset($_POST['name']) ? trim($_POST['name']) : ''; + + if (empty($newName) || strlen($newName) > 63 || ! preg_match('/^[a-z0-9]([a-z0-9\-]{0,61}[a-z0-9])?$/', $newName)) { + $vf->output(['success' => false, 'errors' => 'Invalid server name'], true, true, 400); + break; + } + + $result = $vf->renameServer($serviceID, $newName); + + if ($result) { + $vf->output(['success' => true, 'data' => ['message' => 'Server renamed successfully']], true, true, 200); + break; + } + + $vf->output(['success' => false, 'errors' => 'Server rename failed'], true, true, 500); break; - } - $vf->output(['success' => false, 'errors' => 'Unable to generate login token'], true, true, 500); - break; + /** + * Get available OS templates for rebuild. + */ + case 'osTemplates': - /** - * Power management actions: boot, shutdown, restart, poweroff - */ - case 'powerAction': + $serviceID = $vf->validateServiceID(true); - $serviceID = $vf->validateServiceID(true); + if (! $vf->validateUserOwnsService($serviceID)) { + $vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403); + break; + } - if (!$vf->validateUserOwnsService($serviceID)) { - $vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403); + $templates = $vf->fetchOsTemplates($serviceID); + + if ($templates !== false) { + $vf->output(['success' => true, 'data' => $templates], true, true, 200); + break; + } + + $vf->output(['success' => false, 'errors' => 'Unable to fetch OS templates'], true, true, 500); break; - } - $powerAction = isset($_POST['powerAction']) ? preg_replace('/[^a-zA-Z]/', '', $_POST['powerAction']) : ''; - $allowedActions = ['boot', 'shutdown', 'restart', 'poweroff']; + // ================================================================= + // Server Password Reset + // ================================================================= - if (!in_array($powerAction, $allowedActions, true)) { - $vf->output(['success' => false, 'errors' => 'Invalid power action'], true, true, 400); + /** + * Reset server root password. + */ + case 'resetServerPassword': + + $serviceID = $vf->validateServiceID(true); + + if (! $vf->validateUserOwnsService($serviceID)) { + $vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403); + break; + } + + $result = $vf->resetServerPassword($serviceID); + + if ($result !== false) { + $vf->output(['success' => true, 'data' => $result], true, true, 200); + break; + } + + $vf->output(['success' => false, 'errors' => 'Password reset failed'], true, true, 500); break; - } - $result = $vf->serverPowerAction($serviceID, $powerAction); + // ================================================================= + // Backup Listing + // ================================================================= - if ($result) { - $vf->output(['success' => true, 'data' => ['action' => $powerAction, 'message' => 'Power action queued successfully']], true, true, 200); + /** + * Get server backups. + */ + case 'backups': + + $serviceID = $vf->validateServiceID(true); + + if (! $vf->validateUserOwnsService($serviceID)) { + $vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403); + break; + } + + $result = $vf->getServerBackups($serviceID); + + if ($result !== false) { + $vf->output(['success' => true, 'data' => $result], true, true, 200); + break; + } + + $vf->output(['success' => false, 'errors' => 'Unable to retrieve backups'], true, true, 500); break; - } - $vf->output(['success' => false, 'errors' => 'Power action failed. The server may be locked or unavailable.'], true, true, 500); - break; + // ================================================================= + // Traffic Statistics + // ================================================================= - /** - * Rebuild/reinstall server with new OS. - */ - case 'rebuild': + /** + * Get traffic statistics for a server. + */ + case 'trafficStats': - $serviceID = $vf->validateServiceID(true); + $serviceID = $vf->validateServiceID(true); - if (!$vf->validateUserOwnsService($serviceID)) { - $vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403); + if (! $vf->validateUserOwnsService($serviceID)) { + $vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403); + break; + } + + $result = $vf->getTrafficStats($serviceID); + + if ($result !== false) { + $vf->output(['success' => true, 'data' => $result], true, true, 200); + break; + } + + $vf->output(['success' => false, 'errors' => 'Unable to retrieve traffic statistics'], true, true, 500); break; - } - $osId = isset($_POST['osId']) ? (int) $_POST['osId'] : 0; - $hostname = isset($_POST['hostname']) ? preg_replace('/[^a-zA-Z0-9.\-]/', '', $_POST['hostname']) : null; + // ================================================================= + // VNC Console + // ================================================================= - if ($osId <= 0) { - $vf->output(['success' => false, 'errors' => 'Invalid operating system ID'], true, true, 400); + /** + * Get VNC console URL. + */ + case 'vnc': + + $serviceID = $vf->validateServiceID(true); + + if (! $vf->validateUserOwnsService($serviceID)) { + $vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403); + break; + } + + $result = $vf->getVncConsole($serviceID); + + if ($result !== false) { + $vf->output(['success' => true, 'data' => $result], true, true, 200); + break; + } + + $vf->output(['success' => false, 'errors' => 'VNC console unavailable. The server may be powered off or VNC is not supported.'], true, true, 500); break; - } - $result = $vf->rebuildServer($serviceID, $osId, $hostname); + /** + * Toggle VNC on/off. + */ + case 'toggleVnc': - if ($result) { - $vf->output(['success' => true, 'data' => ['message' => 'Server rebuild initiated successfully']], true, true, 200); + $serviceID = $vf->validateServiceID(true); + + if (! $vf->validateUserOwnsService($serviceID)) { + $vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403); + break; + } + + $enabled = isset($_POST['enabled']) && $_POST['enabled'] === '1'; + $result = $vf->toggleVnc($serviceID, $enabled); + + if ($result !== false) { + $vf->output(['success' => true, 'data' => $result], true, true, 200); + break; + } + + $vf->output(['success' => false, 'errors' => 'Failed to toggle VNC'], true, true, 500); break; - } - $vf->output(['success' => false, 'errors' => 'Server rebuild failed. The server may be locked or unavailable.'], true, true, 500); - break; + // ================================================================= + // Self Service — Credit & Usage + // ================================================================= - /** - * Rename server. - */ - case 'rename': + /** + * Get self-service usage data. + */ + case 'selfServiceUsage': - $serviceID = $vf->validateServiceID(true); + $serviceID = $vf->validateServiceID(true); - if (!$vf->validateUserOwnsService($serviceID)) { - $vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403); + if (! $vf->validateUserOwnsService($serviceID)) { + $vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403); + break; + } + + $result = $vf->getSelfServiceUsage($serviceID); + + if ($result !== false) { + $vf->output(['success' => true, 'data' => $result], true, true, 200); + break; + } + + $vf->output(['success' => false, 'errors' => 'Unable to retrieve self-service usage data'], true, true, 500); break; - } - $newName = isset($_POST['name']) ? trim($_POST['name']) : ''; + /** + * Get self-service billing report. + */ + case 'selfServiceReport': - if (empty($newName) || strlen($newName) > 63 || !preg_match('/^[a-z0-9]([a-z0-9\-]{0,61}[a-z0-9])?$/', $newName)) { - $vf->output(['success' => false, 'errors' => 'Invalid server name'], true, true, 400); + $serviceID = $vf->validateServiceID(true); + + if (! $vf->validateUserOwnsService($serviceID)) { + $vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403); + break; + } + + $result = $vf->getSelfServiceReport($serviceID); + + if ($result !== false) { + $vf->output(['success' => true, 'data' => $result], true, true, 200); + break; + } + + $vf->output(['success' => false, 'errors' => 'Unable to retrieve self-service report'], true, true, 500); break; - } - $result = $vf->renameServer($serviceID, $newName); + /** + * Add self-service credit. + */ + case 'selfServiceAddCredit': - if ($result) { - $vf->output(['success' => true, 'data' => ['message' => 'Server renamed successfully']], true, true, 200); + $serviceID = $vf->validateServiceID(true); + + if (! $vf->validateUserOwnsService($serviceID)) { + $vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403); + break; + } + + $tokens = isset($_POST['tokens']) ? (float) $_POST['tokens'] : 0; + if ($tokens <= 0) { + $vf->output(['success' => false, 'errors' => 'Invalid credit amount. Must be a positive number.'], true, true, 400); + break; + } + + $result = $vf->addSelfServiceCredit($serviceID, $tokens); + + if ($result !== false) { + $vf->output(['success' => true, 'data' => $result], true, true, 200); + break; + } + + $vf->output(['success' => false, 'errors' => 'Failed to add credit'], true, true, 500); break; - } - $vf->output(['success' => false, 'errors' => 'Server rename failed'], true, true, 500); - break; + default: + $vf->output(['success' => false, 'errors' => 'invalid action'], true, true, 400); + } - /** - * Get available OS templates for rebuild. - */ - case 'osTemplates': - - $serviceID = $vf->validateServiceID(true); - - if (!$vf->validateUserOwnsService($serviceID)) { - $vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403); - break; - } - - $templates = $vf->fetchOsTemplates($serviceID); - - if ($templates !== false) { - $vf->output(['success' => true, 'data' => $templates], true, true, 200); - break; - } - - $vf->output(['success' => false, 'errors' => 'Unable to fetch OS templates'], true, true, 500); - break; - - // ================================================================= - // Server Password Reset - // ================================================================= - - /** - * Reset server root password. - */ - case 'resetServerPassword': - - $serviceID = $vf->validateServiceID(true); - - if (!$vf->validateUserOwnsService($serviceID)) { - $vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403); - break; - } - - $result = $vf->resetServerPassword($serviceID); - - if ($result !== false) { - $vf->output(['success' => true, 'data' => $result], true, true, 200); - break; - } - - $vf->output(['success' => false, 'errors' => 'Password reset failed'], true, true, 500); - break; - - // ================================================================= - // Backup Listing - // ================================================================= - - /** - * Get server backups. - */ - case 'backups': - - $serviceID = $vf->validateServiceID(true); - - if (!$vf->validateUserOwnsService($serviceID)) { - $vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403); - break; - } - - $result = $vf->getServerBackups($serviceID); - - if ($result !== false) { - $vf->output(['success' => true, 'data' => $result], true, true, 200); - break; - } - - $vf->output(['success' => false, 'errors' => 'Unable to retrieve backups'], true, true, 500); - break; - - // ================================================================= - // Traffic Statistics - // ================================================================= - - /** - * Get traffic statistics for a server. - */ - case 'trafficStats': - - $serviceID = $vf->validateServiceID(true); - - if (!$vf->validateUserOwnsService($serviceID)) { - $vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403); - break; - } - - $result = $vf->getTrafficStats($serviceID); - - if ($result !== false) { - $vf->output(['success' => true, 'data' => $result], true, true, 200); - break; - } - - $vf->output(['success' => false, 'errors' => 'Unable to retrieve traffic statistics'], true, true, 500); - break; - - // ================================================================= - // VNC Console - // ================================================================= - - /** - * Get VNC console URL. - */ - case 'vnc': - - $serviceID = $vf->validateServiceID(true); - - if (!$vf->validateUserOwnsService($serviceID)) { - $vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403); - break; - } - - $result = $vf->getVncConsole($serviceID); - - if ($result !== false) { - $vf->output(['success' => true, 'data' => $result], true, true, 200); - break; - } - - $vf->output(['success' => false, 'errors' => 'VNC console unavailable. The server may be powered off or VNC is not supported.'], true, true, 500); - break; - - /** - * Toggle VNC on/off. - */ - case 'toggleVnc': - - $serviceID = $vf->validateServiceID(true); - - if (!$vf->validateUserOwnsService($serviceID)) { - $vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403); - break; - } - - $enabled = isset($_POST['enabled']) && $_POST['enabled'] === '1'; - $result = $vf->toggleVnc($serviceID, $enabled); - - if ($result !== false) { - $vf->output(['success' => true, 'data' => $result], true, true, 200); - break; - } - - $vf->output(['success' => false, 'errors' => 'Failed to toggle VNC'], true, true, 500); - break; - - // ================================================================= - // Self Service — Credit & Usage - // ================================================================= - - /** - * Get self-service usage data. - */ - case 'selfServiceUsage': - - $serviceID = $vf->validateServiceID(true); - - if (!$vf->validateUserOwnsService($serviceID)) { - $vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403); - break; - } - - $result = $vf->getSelfServiceUsage($serviceID); - - if ($result !== false) { - $vf->output(['success' => true, 'data' => $result], true, true, 200); - break; - } - - $vf->output(['success' => false, 'errors' => 'Unable to retrieve self-service usage data'], true, true, 500); - break; - - /** - * Get self-service billing report. - */ - case 'selfServiceReport': - - $serviceID = $vf->validateServiceID(true); - - if (!$vf->validateUserOwnsService($serviceID)) { - $vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403); - break; - } - - $result = $vf->getSelfServiceReport($serviceID); - - if ($result !== false) { - $vf->output(['success' => true, 'data' => $result], true, true, 200); - break; - } - - $vf->output(['success' => false, 'errors' => 'Unable to retrieve self-service report'], true, true, 500); - break; - - /** - * Add self-service credit. - */ - case 'selfServiceAddCredit': - - $serviceID = $vf->validateServiceID(true); - - if (!$vf->validateUserOwnsService($serviceID)) { - $vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403); - break; - } - - $tokens = isset($_POST['tokens']) ? (float) $_POST['tokens'] : 0; - if ($tokens <= 0) { - $vf->output(['success' => false, 'errors' => 'Invalid credit amount. Must be a positive number.'], true, true, 400); - break; - } - - $result = $vf->addSelfServiceCredit($serviceID, $tokens); - - if ($result !== false) { - $vf->output(['success' => true, 'data' => $result], true, true, 200); - break; - } - - $vf->output(['success' => false, 'errors' => 'Failed to add credit'], true, true, 500); - break; - - default: - $vf->output(['success' => false, 'errors' => 'invalid action'], true, true, 400); +} catch (Exception $e) { + Log::insert('client.php', [], $e->getMessage()); + $vf->output(['success' => false, 'errors' => 'An unexpected error occurred'], true, true, 500); } diff --git a/modules/servers/VirtFusionDirect/config/ConfigOptionMapping-example.php b/modules/servers/VirtFusionDirect/config/ConfigOptionMapping-example.php index e024945..7cd39b4 100644 --- a/modules/servers/VirtFusionDirect/config/ConfigOptionMapping-example.php +++ b/modules/servers/VirtFusionDirect/config/ConfigOptionMapping-example.php @@ -1,7 +1,7 @@ 'CPU Cores', 'networkProfile' => 'Network Type', 'storageProfile' => 'Storage Type', -]; \ No newline at end of file +]; diff --git a/modules/servers/VirtFusionDirect/hooks.php b/modules/servers/VirtFusionDirect/hooks.php index 933be2a..f4be8fc 100644 --- a/modules/servers/VirtFusionDirect/hooks.php +++ b/modules/servers/VirtFusionDirect/hooks.php @@ -1,10 +1,12 @@ $product) { - $pid = $product['pid'] ?? null; - if (!$pid) { - continue; + try { + if (! isset($_SESSION['cart']['products']) || ! is_array($_SESSION['cart']['products'])) { + return $errors; } - $dbProduct = \WHMCS\Database\Capsule::table('tblproducts') - ->where('id', $pid) - ->where('servertype', 'VirtFusionDirect') - ->first(); + foreach ($_SESSION['cart']['products'] as $key => $product) { + $pid = $product['pid'] ?? null; + if (! $pid) { + continue; + } - if (!$dbProduct) { - continue; - } + $dbProduct = Capsule::table('tblproducts') + ->where('id', $pid) + ->where('servertype', 'VirtFusionDirect') + ->first(); - // Check if Initial Operating System custom field has a value - if (isset($product['customfields']) && is_array($product['customfields'])) { - $osSelected = false; - $customFields = \WHMCS\Database\Capsule::table('tblcustomfields') - ->where('relid', $pid) - ->where('type', 'product') - ->get(); + if (! $dbProduct) { + continue; + } - foreach ($customFields as $field) { - if (strtolower(str_replace(' ', '', $field->fieldname)) === 'initialoperatingsystem') { - $fieldValue = $product['customfields'][$field->id] ?? ''; - if (!empty($fieldValue) && is_numeric($fieldValue)) { - $osSelected = true; + // Check if Initial Operating System custom field has a value + if (isset($product['customfields']) && is_array($product['customfields'])) { + $osSelected = false; + $customFields = Capsule::table('tblcustomfields') + ->where('relid', $pid) + ->where('type', 'product') + ->get(); + + foreach ($customFields as $field) { + if (strtolower(str_replace(' ', '', $field->fieldname)) === 'initialoperatingsystem') { + $fieldValue = $product['customfields'][$field->id] ?? ''; + if (! empty($fieldValue) && is_numeric($fieldValue)) { + $osSelected = true; + } + break; } - break; + } + + if (! $osSelected) { + $errors[] = 'Please select an Operating System for your VPS order.'; } } - - if (!$osSelected) { - $errors[] = 'Please select an Operating System for your VPS order.'; - } } + } catch (Exception $e) { + // Don't block checkout on internal errors } return $errors; @@ -70,22 +76,22 @@ add_hook('ShoppingCartValidateCheckout', 1, function ($vars) { * Works with all WHMCS themes by using vanilla JavaScript and standard form-control classes. */ add_hook('ClientAreaFooterOutput', 1, function ($vars) { - if (!isset($vars['productinfo']['module']) || $vars['productinfo']['module'] !== 'VirtFusionDirect') { + if (! isset($vars['productinfo']['module']) || $vars['productinfo']['module'] !== 'VirtFusionDirect') { return null; } try { - $cs = new ConfigureService(); + $cs = new ConfigureService; $templates_data = $cs->fetchTemplates( - $cs->fetchPackageByDbId($vars['productinfo']['pid']) ?? $cs->fetchPackageId($vars['productinfo']['name']) + $cs->fetchPackageByDbId($vars['productinfo']['pid']) ?? $cs->fetchPackageId($vars['productinfo']['name']), ); if (empty($templates_data)) { return null; } - $vfServer = \WHMCS\Database\Capsule::table('tblservers') + $vfServer = Capsule::table('tblservers') ->where('type', 'VirtFusionDirect') ->where('disabled', 0) ->first(); @@ -93,7 +99,7 @@ add_hook('ClientAreaFooterOutput', 1, function ($vars) { $galleryData = [ 'baseUrl' => $baseUrl, - 'categories' => \WHMCS\Module\Server\VirtFusionDirect\Module::groupOsTemplates($templates_data['data'] ?? [], true), + 'categories' => Module::groupOsTemplates($templates_data['data'] ?? [], true), ]; $sshKeys = []; @@ -105,9 +111,10 @@ add_hook('ClientAreaFooterOutput', 1, function ($vars) { if ($sshKey['enabled'] === false) { return null; } + return [ 'id' => $sshKey['id'], - 'name' => htmlspecialchars($sshKey['name'], ENT_QUOTES, 'UTF-8') + 'name' => htmlspecialchars($sshKey['name'], ENT_QUOTES, 'UTF-8'), ]; }, $sshKeysData['data']))); } @@ -134,17 +141,17 @@ add_hook('ClientAreaFooterOutput', 1, function ($vars) { $systemUrl = Database::getSystemUrl(); - return " - - + return ' + + "; - } catch (\Throwable $e) { + } catch (Throwable $e) { // Silently fail - don't break the checkout page return null; } diff --git a/modules/servers/VirtFusionDirect/lib/AdminHTML.php b/modules/servers/VirtFusionDirect/lib/AdminHTML.php index 6f8f4a2..3a5c321 100644 --- a/modules/servers/VirtFusionDirect/lib/AdminHTML.php +++ b/modules/servers/VirtFusionDirect/lib/AdminHTML.php @@ -2,41 +2,73 @@ namespace WHMCS\Module\Server\VirtFusionDirect; +/** + * Static methods that generate HTML fragments for the WHMCS admin services tab. + */ class AdminHTML { - + /** + * Render the "Impersonate Server Owner" button for the admin services tab. + * + * @param string $systemUrl WHMCS system URL + * @param int $serviceId VirtFusion server ID + * @return string HTML button markup + */ public static function options($systemUrl, $serviceId) { $systemUrl = htmlspecialchars($systemUrl, ENT_QUOTES, 'UTF-8'); + return <<Impersonate Server Owner   A valid VirtFusion admin session in the same browser is required for this functionality to work. EOT; } + /** + * Render a read-only textarea containing the raw VirtFusion server JSON object. + * + * @param string $serverObject JSON-encoded server object from the VirtFusion API + * @return string HTML textarea markup + */ public static function serverObject($serverObject) { $serverObject = htmlspecialchars($serverObject, ENT_QUOTES, 'UTF-8'); + return <<${serverObject} EOT; } + /** + * Render an editable text input for the VirtFusion server ID field. + * + * @param int $serverId Current VirtFusion server ID + * @return string HTML input markup with a warning note + */ public static function serverId($serverId) { $serverId = (int) $serverId; + return <<   Changing the Sever ID manually is not recommended. Alterations to this field are usually handled automatically. EOT; } + /** + * Render the inline server info panel for the admin services tab, including CSS/JS assets. + * + * @param string $systemUrl WHMCS system URL (used to build asset and AJAX URLs) + * @param int $serviceId VirtFusion server ID passed to the JS data-loader + * @return string HTML panel markup with embedded script and asset tags + */ public static function serverInfo($systemUrl, $serviceId) { $systemUrl = htmlspecialchars($systemUrl, ENT_QUOTES, 'UTF-8'); $serviceId = (int) $serviceId; $cacheV = time(); + return << @@ -117,4 +149,4 @@ EOT; EOT; } -} \ No newline at end of file +} diff --git a/modules/servers/VirtFusionDirect/lib/Cache.php b/modules/servers/VirtFusionDirect/lib/Cache.php index 7a1c3eb..7cc70c1 100644 --- a/modules/servers/VirtFusionDirect/lib/Cache.php +++ b/modules/servers/VirtFusionDirect/lib/Cache.php @@ -2,6 +2,10 @@ namespace WHMCS\Module\Server\VirtFusionDirect; +/** + * Two-tier cache: uses Redis when the ext-redis extension is available, with an atomic + * filesystem fallback stored in the system temp directory. + */ class Cache { const PREFIX = 'vfd:'; @@ -28,19 +32,22 @@ class Cache return self::$redis; } - if (!extension_loaded('redis')) { + if (! extension_loaded('redis')) { self::$redisAvailable = false; + return null; } try { - $redis = new \Redis(); + $redis = new \Redis; $redis->connect('127.0.0.1', 6379, 1.0); self::$redis = $redis; self::$redisAvailable = true; + return $redis; } catch (\Exception $e) { self::$redisAvailable = false; + return null; } } @@ -55,11 +62,12 @@ class Cache } $dir = sys_get_temp_dir() . '/vfd_cache'; - if (!is_dir($dir)) { + if (! is_dir($dir)) { @mkdir($dir, 0700, true); } self::$fileDir = $dir; + return $dir; } @@ -74,7 +82,7 @@ class Cache /** * Get a cached value. * - * @param string $key + * @param string $key * @return mixed|null Returns null on miss */ public static function get($key) @@ -87,6 +95,7 @@ class Cache if ($data !== false) { return json_decode($data, true); } + return null; } catch (\Exception $e) { // Fall through to file cache @@ -95,7 +104,7 @@ class Cache // File cache fallback $path = self::filePath($key); - if (!file_exists($path)) { + if (! file_exists($path)) { return null; } @@ -105,13 +114,15 @@ class Cache } $entry = json_decode($raw, true); - if (!$entry || !isset($entry['expires']) || !isset($entry['data'])) { + if (! $entry || ! isset($entry['expires']) || ! isset($entry['data'])) { @unlink($path); + return null; } if ($entry['expires'] < time()) { @unlink($path); + return null; } @@ -121,9 +132,9 @@ class Cache /** * Store a value in cache. * - * @param string $key - * @param mixed $value - * @param int $ttl Time-to-live in seconds + * @param string $key + * @param mixed $value + * @param int $ttl Time-to-live in seconds */ public static function set($key, $value, $ttl = 300) { @@ -132,6 +143,7 @@ class Cache if ($redis) { try { $redis->setex(self::PREFIX . $key, $ttl, json_encode($value)); + return; } catch (\Exception $e) { // Fall through to file cache @@ -151,7 +163,7 @@ class Cache /** * Delete a cached value. * - * @param string $key + * @param string $key */ public static function forget($key) { @@ -169,5 +181,4 @@ class Cache @unlink($path); } } - } diff --git a/modules/servers/VirtFusionDirect/lib/ConfigureService.php b/modules/servers/VirtFusionDirect/lib/ConfigureService.php index a02d804..e0d81af 100644 --- a/modules/servers/VirtFusionDirect/lib/ConfigureService.php +++ b/modules/servers/VirtFusionDirect/lib/ConfigureService.php @@ -5,13 +5,30 @@ namespace WHMCS\Module\Server\VirtFusionDirect; use WHMCS\Database\Capsule as DB; use WHMCS\User\User; +/** + * Handles order-time and provisioning-time operations for VirtFusion servers. + * + * Extends Module to provide package discovery, OS template fetching, server build + * initialization, and SSH key retrieval/creation. Used during WHMCS checkout and + * account creation flows rather than ongoing service management. + */ class ConfigureService extends Module { /** - * @var array|false $cp + * The first available VirtFusion control panel connection, as returned by + * getCP(). Holds server URL and API token used for all API calls in this + * class. False if no active VirtFusion server is configured in WHMCS. + * + * @var array|false */ private array|bool $cp; + /** + * Initialize the service configurator with the first available VirtFusion server. + * + * Calls the parent Module constructor then resolves the control panel connection + * so all methods in this class have a ready API endpoint. + */ public function __construct() { parent::__construct(); @@ -19,208 +36,298 @@ class ConfigureService extends Module } /** - * @param string $packageName - * @return int|null - * @throws JsonException + * Find a VirtFusion package ID by its name via the API. + * + * Searches the packages list for an enabled package whose name matches + * exactly. Result is cached for 10 minutes. Returns null if not found + * or if no control panel is available. + * + * @param string $packageName Exact package name as configured in VirtFusion. + * @return int|null Package ID, or null if not found. */ public function fetchPackageId(string $packageName): ?int { - $cacheKey = 'pkg_name:' . md5($packageName); - $cached = Cache::get($cacheKey); - if ($cached !== null) { - return $cached; - } - - if (!$this->cp) return null; - - $request = $this->initCurl($this->cp['token']); - - $response = $request->get( - sprintf("%s/packages", $this->cp['url']) - ); - - $packages = $this->decodeResponseFromJson($response); - - foreach ($packages['data'] as $package) { - if ($package['name'] === $packageName && $package['enabled'] === true) { - Cache::set($cacheKey, $package['id'], 600); - return $package['id']; + try { + $cacheKey = 'pkg_name:' . md5($packageName); + $cached = Cache::get($cacheKey); + if ($cached !== null) { + return $cached; } - } - return null; + if (! $this->cp) { + return null; + } + + $request = $this->initCurl($this->cp['token']); + + $response = $request->get( + sprintf('%s/packages', $this->cp['url']), + ); + + $packages = $this->decodeResponseFromJson($response); + + foreach ($packages['data'] as $package) { + if ($package['name'] === $packageName && $package['enabled'] === true) { + Cache::set($cacheKey, $package['id'], 600); + + return $package['id']; + } + } + + return null; + } catch (\Exception $e) { + Log::insert(__FUNCTION__, [], $e->getMessage()); + + return null; + } } - /** - * @param int $productId - * @return int|null + * Get the VirtFusion package ID from a WHMCS product's config option. + * + * Reads configoption2 directly from the tblproducts database record for + * the given WHMCS product ID. Returns null if the product does not exist. + * + * @param int $productId WHMCS product (tblproducts) ID. + * @return int|null VirtFusion package ID, or null if the product is not found. */ public function fetchPackageByDbId(int $productId): ?int { - $product = DB::table('tblproducts')->where('id', $productId)->first(); + try { + $product = DB::table('tblproducts')->where('id', $productId)->first(); + + if (is_null($product)) { + return null; + } + + return (int) $product->configoption2; + } catch (\Exception $e) { + Log::insert(__FUNCTION__, [], $e->getMessage()); - if (is_null($product)) { return null; } - - return (int)$product->configoption2; } /** - * @param int $serverPackageId - * @return array|null - * @throws JsonException + * Fetch the available OS templates for a given VirtFusion server package. + * + * Queries the VirtFusion API for templates compatible with the specified + * package spec ID. Result is cached for 10 minutes. Returns null if no + * package ID is provided or no control panel is available. + * + * @param int|null $serverPackageId VirtFusion server package spec ID. + * @return array|null Template list from the API, or null on failure. */ public function fetchTemplates(?int $serverPackageId): ?array { - if (is_null($serverPackageId)) { + try { + if (is_null($serverPackageId)) { + return null; + } + + $cacheKey = 'tpl:' . $serverPackageId; + $cached = Cache::get($cacheKey); + if ($cached !== null) { + return $cached; + } + + if (! $this->cp) { + return null; + } + + $request = $this->initCurl($this->cp['token']); + + $response = $request->get( + sprintf('%s/media/templates/fromServerPackageSpec/%d', $this->cp['url'], $serverPackageId), + ); + + $result = $this->decodeResponseFromJson($response); + Cache::set($cacheKey, $result, 600); + + return $result; + } catch (\Exception $e) { + Log::insert(__FUNCTION__, [], $e->getMessage()); + return null; } - - $cacheKey = 'tpl:' . $serverPackageId; - $cached = Cache::get($cacheKey); - if ($cached !== null) { - return $cached; - } - - if (!$this->cp) return null; - - $request = $this->initCurl($this->cp['token']); - - $response = $request->get( - sprintf("%s/media/templates/fromServerPackageSpec/%d", $this->cp['url'], $serverPackageId) - ); - - $result = $this->decodeResponseFromJson($response); - Cache::set($cacheKey, $result, 600); - return $result; } /** - * @param User|null $user - * @return array|null - * @throws JsonException + * Get the SSH keys registered for a VirtFusion user. + * + * Looks up the VirtFusion account for the given WHMCS user via external + * relation ID, then fetches their SSH key list from the API. Returns null + * if the user is not found in VirtFusion or no control panel is available. + * + * @param User|null $user WHMCS User object. + * @return array|null SSH key list from the API, or null on failure. */ public function getUserSshKeys(?User $user): ?array { - if (is_null($user)) { + try { + if (is_null($user)) { + return null; + } + + if (! $this->cp) { + return null; + } + + $request = $this->initCurl($this->cp['token']); + + $vfUser = $this->getVFUserDetails($user['id']); + + if (! $vfUser) { + return null; + } + + $response = $request->get( + sprintf('%s/ssh_keys/user/%d', $this->cp['url'], $vfUser['id']), + ); + + return $this->decodeResponseFromJson($response); + } catch (\Exception $e) { + Log::insert(__FUNCTION__, [], $e->getMessage()); + return null; } - - if (!$this->cp) return null; - - $request = $this->initCurl($this->cp['token']); - - $vfUser = $this->getVFUserDetails($user['id']); - - if (!$vfUser) return null; - - $response = $request->get( - sprintf("%s/ssh_keys/user/%d", $this->cp['url'], $vfUser['id']) - ); - - return $this->decodeResponseFromJson($response); } /** - * @param int $id - * @return array|null - * @throws JsonException + * Look up a VirtFusion user by WHMCS external relation ID. + * + * Calls the VirtFusion API's byExtRelation endpoint using the WHMCS client + * ID. Returns null if the user does not exist in VirtFusion or no control + * panel is available. + * + * @param int $id WHMCS client ID used as the VirtFusion external relation ID. + * @return array|null VirtFusion user data array, or null if not found. */ public function getVFUserDetails(int $id): ?array { - if (!$this->cp) return null; + try { + if (! $this->cp) { + return null; + } - $request = $this->initCurl($this->cp['token']); + $request = $this->initCurl($this->cp['token']); - $response = $this->decodeResponseFromJson($request->get( - sprintf("%s/users/%d/byExtRelation", $this->cp['url'], $id) - )); + $response = $this->decodeResponseFromJson($request->get( + sprintf('%s/users/%d/byExtRelation', $this->cp['url'], $id), + )); - return isset($response['msg']) && $response['msg'] === "ext_relation_id not found" ? null : $response['data']; + return isset($response['msg']) && $response['msg'] === 'ext_relation_id not found' ? null : $response['data']; + } catch (\Exception $e) { + Log::insert(__FUNCTION__, [], $e->getMessage()); + + return null; + } } /** - * @param int $id - * @param array $vars - * @param int|null $vfUserId VirtFusion user ID (for creating SSH keys from raw public key) - * @return bool + * Trigger OS installation on a newly created VirtFusion server. + * + * Posts a build request to the VirtFusion API with the selected OS template + * and optionally an SSH key. If the custom field contains a numeric value it + * is treated as an existing key ID; if it is a raw public key string, the key + * is created first via createUserSshKey(). Returns true on HTTP 200/201. + * + * @param int $id VirtFusion server ID to build. + * @param array $vars WHMCS order vars, including customfields for OS and SSH key. + * @param int|null $vfUserId VirtFusion user ID, required when creating a new SSH key from a raw public key. + * @return bool True if the build request was accepted, false otherwise. */ public function initServerBuild(int $id, array $vars, ?int $vfUserId = null): bool { - if (!$this->cp) return false; - - $request = $this->initCurl($this->cp['token']); - - // Generate a hostname with sufficient entropy to avoid collisions - $hostname = 'vps-' . bin2hex(random_bytes(4)); - - $sshKeyValue = $vars['customfields']['Initial SSH Key'] ?? null; - $sshKeyId = null; - - if (!empty($sshKeyValue)) { - if (is_numeric($sshKeyValue)) { - // Existing SSH key ID - $sshKeyId = (int) $sshKeyValue; - } elseif (preg_match('/^ssh-/', $sshKeyValue) && $vfUserId) { - // Raw public key — create it via API - $sshKeyId = $this->createUserSshKey($vfUserId, $sshKeyValue); + try { + if (! $this->cp) { + return false; } + + $request = $this->initCurl($this->cp['token']); + + // Generate a hostname with sufficient entropy to avoid collisions + $hostname = 'vps-' . bin2hex(random_bytes(4)); + + $sshKeyValue = $vars['customfields']['Initial SSH Key'] ?? null; + $sshKeyId = null; + + if (! empty($sshKeyValue)) { + if (is_numeric($sshKeyValue)) { + // Existing SSH key ID + $sshKeyId = (int) $sshKeyValue; + } elseif (preg_match('/^ssh-/', $sshKeyValue) && $vfUserId) { + // Raw public key — create it via API + $sshKeyId = $this->createUserSshKey($vfUserId, $sshKeyValue); + } + } + + $inputData = [ + 'operatingSystemId' => $vars['customfields']['Initial Operating System'] ?? null, + 'name' => $hostname, + 'email' => true, + ]; + + if ($sshKeyId) { + $inputData['sshKeys'] = [$sshKeyId]; + } + + $request->addOption(CURLOPT_POSTFIELDS, json_encode($inputData)); + + $response = $request->post( + sprintf('%s/servers/%d/build', $this->cp['url'], $id), + ); + + $httpCode = $request->getRequestInfo('http_code'); + Log::insert(__FUNCTION__, $request->getRequestInfo(), $response); + + return $httpCode == 200 || $httpCode == 201; + } catch (\Exception $e) { + Log::insert(__FUNCTION__, [], $e->getMessage()); + + return false; } - - $inputData = [ - "operatingSystemId" => $vars['customfields']['Initial Operating System'] ?? null, - "name" => $hostname, - 'email' => true - ]; - - if ($sshKeyId) { - $inputData['sshKeys'] = [$sshKeyId]; - } - - $request->addOption(CURLOPT_POSTFIELDS, json_encode($inputData)); - - $response = $request->post( - sprintf("%s/servers/%d/build", $this->cp['url'], $id) - ); - - $httpCode = $request->getRequestInfo('http_code'); - Log::insert(__FUNCTION__, $request->getRequestInfo(), $response); - - return ($httpCode == 200 || $httpCode == 201); } /** * Create an SSH key for a VirtFusion user from a raw public key string. * - * @param int $userId VirtFusion user ID - * @param string $publicKey Raw SSH public key (ssh-rsa ..., ssh-ed25519 ..., etc.) + * @param int $userId VirtFusion user ID + * @param string $publicKey Raw SSH public key (ssh-rsa ..., ssh-ed25519 ..., etc.) * @return int|null Created key ID or null on failure */ public function createUserSshKey(int $userId, string $publicKey): ?int { - if (!$this->cp) return null; + try { + if (! $this->cp) { + return null; + } - $request = $this->initCurl($this->cp['token']); + $request = $this->initCurl($this->cp['token']); - $keyData = [ - 'userId' => $userId, - 'name' => 'WHMCS-' . date('Y-m-d'), - 'publicKey' => trim($publicKey), - ]; + $keyData = [ + 'userId' => $userId, + 'name' => 'WHMCS-' . date('Y-m-d'), + 'publicKey' => trim($publicKey), + ]; - $request->addOption(CURLOPT_POSTFIELDS, json_encode($keyData)); - $response = $request->post($this->cp['url'] . '/ssh_keys'); + $request->addOption(CURLOPT_POSTFIELDS, json_encode($keyData)); + $response = $request->post($this->cp['url'] . '/ssh_keys'); - Log::insert(__FUNCTION__, $request->getRequestInfo(), $response); + Log::insert(__FUNCTION__, $request->getRequestInfo(), $response); - $httpCode = $request->getRequestInfo('http_code'); - if ($httpCode == 200 || $httpCode == 201) { - $data = json_decode($response, true); - return $data['data']['id'] ?? null; + $httpCode = $request->getRequestInfo('http_code'); + if ($httpCode == 200 || $httpCode == 201) { + $data = json_decode($response, true); + + return $data['data']['id'] ?? null; + } + + return null; + } catch (\Exception $e) { + Log::insert(__FUNCTION__, [], $e->getMessage()); + + return null; } - - return null; } } diff --git a/modules/servers/VirtFusionDirect/lib/Curl.php b/modules/servers/VirtFusionDirect/lib/Curl.php index 6ff2dae..53fd40e 100644 --- a/modules/servers/VirtFusionDirect/lib/Curl.php +++ b/modules/servers/VirtFusionDirect/lib/Curl.php @@ -2,11 +2,22 @@ namespace WHMCS\Module\Server\VirtFusionDirect; +/** + * HTTP client wrapper with Bearer token auth, SSL verification, and a 30s timeout. + * Single-use — each instance makes one request. + */ class Curl { + /** @var resource|\CurlHandle cURL handle */ private $ch; + + /** @var array Response info and parsed header data collected after exec */ private $data; + + /** @var array User-supplied cURL options that override defaults */ private $customOptions = []; + + /** @var array Default cURL options applied to every request */ private $defaultOptions = [ CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, @@ -18,15 +29,17 @@ class Curl CURLOPT_CONNECTTIMEOUT => 10, ]; - + /** Initialise the cURL handle. */ public function __construct() { $this->ch = curl_init(); } /** - * @param $name - * @param $value + * Set a custom cURL option, overriding the defaults. + * + * @param int $name A CURLOPT_* constant + * @param mixed $value The option value */ public function addOption($name, $value) { @@ -34,8 +47,10 @@ class Curl } /** - * @param null $url - * @return bool|string|void + * Execute a PUT request. + * + * @param string|null $url Target URL, or null to use a previously set CURLOPT_URL + * @return bool|string Response body, or false on failure */ public function put($url = null) { @@ -43,8 +58,10 @@ class Curl } /** - * @param null $url - * @return bool|string|void + * Execute a PATCH request. + * + * @param string|null $url Target URL, or null to use a previously set CURLOPT_URL + * @return bool|string Response body, or false on failure */ public function patch($url = null) { @@ -52,14 +69,18 @@ class Curl } /** - * @param $method - * @param $url - * @return bool|string|void + * Set the HTTP method and URL, then execute the request. + * + * @param string $method HTTP method (GET, POST, PUT, PATCH, DELETE) + * @param string|null $url Target URL, or null to use a previously set CURLOPT_URL + * @return bool|string Response body, or false on failure + * + * @throws \RuntimeException If no URL is available */ private function send($method, $url) { if ($url === null) { - if (!isset($this->customOptions[CURLOPT_URL]) || empty($this->customOptions[CURLOPT_URL])) { + if (! isset($this->customOptions[CURLOPT_URL]) || empty($this->customOptions[CURLOPT_URL])) { throw new \RuntimeException('Curl: empty URL provided'); } } @@ -70,7 +91,9 @@ class Curl } /** - * @return bool|string + * Apply options, run the cURL handle, collect response info, and close the handle. + * + * @return bool|string Response body, or false on cURL error */ private function exec() { @@ -94,6 +117,7 @@ class Curl return $response; } + /** Merge custom and default cURL options and apply them to the handle. */ private function setOptions() { if (isset($this->customOptions[CURLOPT_HEADER]) && $this->customOptions[CURLOPT_HEADER]) { @@ -105,7 +129,9 @@ class Curl } /** - * @param $data + * Split a response containing headers into header and body parts and store them. + * + * @param string $data Raw response string (headers + body); replaced with body only */ private function processHeaders(&$data) { @@ -116,15 +142,17 @@ class Curl $tmp = explode("\r\n", $this->data['info']['response_header']); $this->data['data']['Message'] = $tmp[0]; - for ($i = 1, $size = count($tmp); $i < $size; ++$i) { + 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 + * Execute a GET request. + * + * @param string|null $url Target URL, or null to use a previously set CURLOPT_URL + * @return bool|string Response body, or false on failure */ public function get($url = null) { @@ -132,8 +160,10 @@ class Curl } /** - * @param null $url - * @return bool|string|void + * Execute a DELETE request. + * + * @param string|null $url Target URL, or null to use a previously set CURLOPT_URL + * @return bool|string Response body, or false on failure */ public function delete($url = null) { @@ -141,8 +171,10 @@ class Curl } /** - * @param null $url - * @return bool|string|void + * Execute a POST request. + * + * @param string|null $url Target URL, or null to use a previously set CURLOPT_URL + * @return bool|string Response body, or false on failure */ public function post($url = null) { @@ -150,8 +182,10 @@ class Curl } /** - * @param false $param - * @return mixed|null + * Return curl_getinfo data for the completed request. + * + * @param string|false $param A specific info key to retrieve, or false for the full array + * @return mixed|null The requested info value, the full info array, or null if the key is absent */ public function getRequestInfo($param = false) { @@ -163,9 +197,11 @@ class Curl } /** - * @param $what - * @param $name - * @return mixed|null + * Retrieve a single item from the internal data store by section and key. + * + * @param string $what Top-level section key (e.g. 'info', 'data') + * @param string $name Item key within that section + * @return mixed|null The stored value, or null if not found */ private function getDataItem($what, $name) { @@ -175,5 +211,4 @@ class Curl return null; } } - } diff --git a/modules/servers/VirtFusionDirect/lib/Database.php b/modules/servers/VirtFusionDirect/lib/Database.php index e489ed4..fc0008d 100644 --- a/modules/servers/VirtFusionDirect/lib/Database.php +++ b/modules/servers/VirtFusionDirect/lib/Database.php @@ -4,13 +4,28 @@ namespace WHMCS\Module\Server\VirtFusionDirect; use WHMCS\Database\Capsule as DB; +/** + * Handles all database operations for the module's custom table (mod_virtfusion_direct) + * and queries against core WHMCS tables (tblhosting, tblclients, tblservers, etc.). + */ class Database { const SYSTEM_TABLE = 'mod_virtfusion_direct'; + /** @var bool Tracks whether custom field existence has already been verified this request. */ + private static $fieldsChecked = false; + + /** + * Creates or migrates the module table schema and ensures custom fields exist. + * + * Creates mod_virtfusion_direct with service_id and server_id columns if absent, + * adds the server_object column if missing, then calls ensureCustomFields(). + * + * @return void + */ public static function schema() { - if (!DB::schema()->hasTable(self::SYSTEM_TABLE)) { + if (! DB::schema()->hasTable(self::SYSTEM_TABLE)) { try { DB::schema()->create(self::SYSTEM_TABLE, function ($table) { $table->unsignedBigInteger('service_id')->nullable()->default(null)->index(); @@ -22,7 +37,7 @@ class Database } } - if (!DB::schema()->hasColumn(self::SYSTEM_TABLE, 'server_object')) { + 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); @@ -31,92 +46,283 @@ class Database Log::insert(__FUNCTION__, [], $e->getMessage()); } } + + self::ensureCustomFields(); } + /** + * Ensures the "Initial Operating System" and "Initial SSH Key" custom fields exist + * for every VirtFusionDirect product, creating them via upsert if absent. + * + * @return void + */ + public static function ensureCustomFields() + { + if (self::$fieldsChecked) { + return; + } + self::$fieldsChecked = true; + + try { + $productIds = DB::table('tblproducts') + ->where('servertype', 'VirtFusionDirect') + ->pluck('id'); + + foreach ($productIds as $productId) { + foreach (['Initial Operating System', 'Initial SSH Key'] as $fieldName) { + DB::table('tblcustomfields')->updateOrInsert( + ['type' => 'product', 'relid' => $productId, 'fieldname' => $fieldName], + [ + 'fieldtype' => 'text', + 'description' => '', + 'fieldoptions' => '', + 'regexpr' => '', + 'adminonly' => '', + 'required' => '', + 'showorder' => 'on', + 'showinvoice' => '', + 'sortorder' => 0, + 'updated_at' => DB::raw('UTC_TIMESTAMP()'), + ], + ); + } + } + } catch (\Exception $e) { + Log::insert(__FUNCTION__, [], $e->getMessage()); + } + } + + /** + * Fetches a VirtFusionDirect server record from tblservers. + * + * When $server is non-zero, returns the matching server by ID. + * When $any is true and $server is 0, returns the first enabled server. + * + * @param int $server WHMCS server ID to look up (0 to skip ID filter). + * @param bool $any If true, fall back to the first active server. + * @return object|false Row object on success, false on failure or not found. + */ public static function getWhmcsServer(int $server, $any = false) { - if ($server) { - return DB::table('tblservers')->where('type', 'VirtFusionDirect')->where('id', $server)->first(); - } + try { + 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(); + if ($any) { + return DB::table('tblservers')->where('type', 'VirtFusionDirect')->where('disabled', 0)->first(); + } + } catch (\Exception $e) { + Log::insert(__FUNCTION__, [], $e->getMessage()); } return false; } + /** + * Checks whether a WHMCS service belongs to the given client. + * + * @param int $serviceId WHMCS hosting service ID. + * @param int $userId WHMCS client ID. + * @return bool True if the service is owned by the client, false otherwise. + */ public static function userWhmcsService(int $serviceId, int $userId) { - return DB::table('tblhosting')->where('id', $serviceId)->where('userid', $userId)->exists(); + try { + return DB::table('tblhosting')->where('id', $serviceId)->where('userid', $userId)->exists(); + } catch (\Exception $e) { + Log::insert(__FUNCTION__, [], $e->getMessage()); + + return false; + } } + /** + * Returns the WHMCS system URL from tblconfiguration. + * + * @return string The system URL, or an empty string if not found or on error. + */ public static function getSystemUrl() { - $url = DB::table('tblconfiguration')->where('setting', '=', 'SystemURL')->first(); - if (!$url) return ''; - return $url->value; + try { + $url = DB::table('tblconfiguration')->where('setting', '=', 'SystemURL')->first(); + if (! $url) { + return ''; + } + + return $url->value; + } catch (\Exception $e) { + Log::insert(__FUNCTION__, [], $e->getMessage()); + + return ''; + } } + /** + * Fetches a WHMCS client record by ID. + * + * @param int $id WHMCS client ID. + * @return object|null Row object on success, null on failure or not found. + */ public static function getUser(int $id) { - return DB::table('tblclients')->where('id', $id)->first(); + try { + return DB::table('tblclients')->where('id', $id)->first(); + } catch (\Exception $e) { + Log::insert(__FUNCTION__, [], $e->getMessage()); + + return null; + } } + /** + * Fetches a WHMCS hosting service record by ID. + * + * @param int $serviceId WHMCS hosting service ID. + * @return object|null Row object on success, null on failure or not found. + */ public static function getWhmcsService(int $serviceId) { - return DB::table('tblhosting')->where('id', $serviceId)->first(); + try { + return DB::table('tblhosting')->where('id', $serviceId)->first(); + } catch (\Exception $e) { + Log::insert(__FUNCTION__, [], $e->getMessage()); + + return null; + } } + /** + * Upserts the VirtFusion server ID for a given WHMCS service in the module table. + * + * @param int $serviceId WHMCS hosting service ID. + * @param int $serverId VirtFusion server ID. + * @return void + */ public static function updateSystemServiceServerId(int $serviceId, int $serverId) { - - DB::table(self::SYSTEM_TABLE)->updateOrInsert( - [ - "service_id" => $serviceId - ], - [ - 'server_id' => $serverId - ] - ); + try { + DB::table(self::SYSTEM_TABLE)->updateOrInsert( + [ + 'service_id' => $serviceId, + ], + [ + 'server_id' => $serverId, + ], + ); + } catch (\Exception $e) { + Log::insert(__FUNCTION__, [], $e->getMessage()); + } } + /** + * Updates one or more WHMCS tables with the provided data for a given service ID. + * + * $data is keyed by table name; each value is an associative array of column => value + * pairs passed to an update() WHERE id = $serviceId. + * + * @param int $serviceId WHMCS hosting service ID. + * @param array $data Map of table name to column-value pairs to update. + * @return void + */ public static function updateWhmcsServiceParams(int $serviceId, $data) { - if (count($data)) { - foreach ($data as $key => $items) { - DB::table($key)->where('id', $serviceId)->update($items); + try { + if (count($data)) { + foreach ($data as $key => $items) { + DB::table($key)->where('id', $serviceId)->update($items); + } } + } catch (\Exception $e) { + Log::insert(__FUNCTION__, [], $e->getMessage()); } } + /** + * Checks whether a module table record exists for the given service. + * + * @param int $serviceId WHMCS hosting service ID. + * @return bool True if a record exists, false otherwise. + */ public static function checkSystemService(int $serviceId) { - return DB::table(self::SYSTEM_TABLE)->where('service_id', $serviceId)->exists(); - } + try { + return DB::table(self::SYSTEM_TABLE)->where('service_id', $serviceId)->exists(); + } catch (\Exception $e) { + Log::insert(__FUNCTION__, [], $e->getMessage()); - 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)]); + return false; } } - public static function getSystemService(int $serviceId) + /** + * Deletes the module table record for the given service. + * + * @param int $serviceId WHMCS hosting service ID. + * @return void + */ + public static function deleteSystemService(int $serviceId) { - return DB::table(self::SYSTEM_TABLE)->where('service_id', $serviceId)->first(); + try { + DB::table(self::SYSTEM_TABLE)->where('service_id', $serviceId)->delete(); + } catch (\Exception $e) { + Log::insert(__FUNCTION__, [], $e->getMessage()); + } } -} \ No newline at end of file + /** + * Persists the raw VirtFusion server API response as JSON in the module table. + * + * @param int $serviceId WHMCS hosting service ID. + * @param mixed $data Server object from the VirtFusion API (will be JSON-encoded). + * @return void + */ + public static function updateSystemServiceServerObject(int $serviceId, $data) + { + try { + DB::table(self::SYSTEM_TABLE)->where('service_id', $serviceId)->update(['server_object' => json_encode($data, JSON_PRETTY_PRINT)]); + } catch (\Exception $e) { + Log::insert(__FUNCTION__, [], $e->getMessage()); + } + } + + /** + * Inserts or updates the module table record immediately after a VirtFusion server is created. + * + * Stores both the VirtFusion server ID (from $data->data->id) and the full server + * object JSON. Uses update if a record already exists, otherwise inserts. + * + * @param int $serviceId WHMCS hosting service ID. + * @param mixed $data Full API response object from the VirtFusion server creation call. + * @return void + */ + public static function systemOnServerCreate(int $serviceId, $data) + { + try { + 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)]); + } + } catch (\Exception $e) { + Log::insert(__FUNCTION__, [], $e->getMessage()); + } + } + + /** + * Fetches the module table record for the given service. + * + * @param int $serviceId WHMCS hosting service ID. + * @return object|null Row object on success, null on failure or not found. + */ + public static function getSystemService(int $serviceId) + { + try { + return DB::table(self::SYSTEM_TABLE)->where('service_id', $serviceId)->first(); + } catch (\Exception $e) { + Log::insert(__FUNCTION__, [], $e->getMessage()); + + return null; + } + } +} diff --git a/modules/servers/VirtFusionDirect/lib/Log.php b/modules/servers/VirtFusionDirect/lib/Log.php index b84ae59..dcaf51e 100644 --- a/modules/servers/VirtFusionDirect/lib/Log.php +++ b/modules/servers/VirtFusionDirect/lib/Log.php @@ -2,22 +2,22 @@ namespace WHMCS\Module\Server\VirtFusionDirect; +/** + * Thin wrapper around the WHMCS logModuleCall() function for module-level logging. + */ class Log { const LOG_MODULE = 'VirtFusionDirect'; + /** + * Write an entry to the WHMCS module log. + * + * @param string $action Name of the action being logged (e.g. 'CreateAccount') + * @param string|array $requestString Request data sent to the API + * @param string|array $responseData Response data received from the API + */ 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); } -} \ No newline at end of file +} diff --git a/modules/servers/VirtFusionDirect/lib/Module.php b/modules/servers/VirtFusionDirect/lib/Module.php index 338b49c..2a73fb7 100644 --- a/modules/servers/VirtFusionDirect/lib/Module.php +++ b/modules/servers/VirtFusionDirect/lib/Module.php @@ -2,49 +2,65 @@ namespace WHMCS\Module\Server\VirtFusionDirect; +use WHMCS\Authentication\CurrentUser; +use WHMCS\Database\Capsule; + +/** + * Base class providing VirtFusion API integration, authentication checks, and all + * server feature methods (power, network, VNC, backup, resource modification, + * self-service billing, traffic, rename, password reset). + * + * Extended by ModuleFunctions (service lifecycle) and ConfigureService (order-time + * operations). Most business logic lives here; subclasses delegate to these methods. + */ class Module { + /** + * Initialises the module and ensures the database schema is up to date. + */ public function __construct() { Database::schema(); } /** - * @param bool $exitOnError + * @param bool $exitOnError * @return string */ public function validateAction($exitOnError = true) { - if (!isset($_GET['action'])) { + if (! isset($_GET['action'])) { $this->output(['success' => false, 'errors' => 'no action specified'], true, $exitOnError, 400); } + return preg_replace('/[^a-zA-Z0-9_]/', '', $_GET['action']); } /** - * @param bool $exitOnError + * @param bool $exitOnError * @return int */ public function validateServiceID($exitOnError = true) { - if (!isset($_GET['serviceID']) || !is_numeric($_GET['serviceID'])) { + if (! isset($_GET['serviceID']) || ! is_numeric($_GET['serviceID'])) { $this->output(['success' => false, 'errors' => 'no valid serviceID specified'], true, $exitOnError, 400); } + return (int) $_GET['serviceID']; } /** - * @param int $serviceID - * @param bool $exitOnError + * @param int $serviceID + * @param bool $exitOnError * @return int|false */ public function validateUserOwnsService($serviceID, $exitOnError = true) { $serviceID = (int) $serviceID; - $currentUser = new \WHMCS\Authentication\CurrentUser; + $currentUser = new CurrentUser; $client = $currentUser->client(); - if (!$client) { + if (! $client) { return false; } @@ -59,231 +75,335 @@ class Module * Resolve service context: system service, WHMCS service, control panel, and curl client. * Returns false if any lookup fails. * - * @param int $serviceID + * @param int $serviceID * @return array{service: object, whmcsService: object, cp: array, request: Curl}|false */ protected function resolveServiceContext($serviceID) { - $serviceID = (int) $serviceID; - $service = Database::getSystemService($serviceID); - if (!$service) return false; + try { + $serviceID = (int) $serviceID; + $service = Database::getSystemService($serviceID); + if (! $service) { + return false; + } - $whmcsService = Database::getWhmcsService($serviceID); - if (!$whmcsService) return false; + $whmcsService = Database::getWhmcsService($serviceID); + if (! $whmcsService) { + return false; + } - $cp = $this->getCP($whmcsService->server); - if (!$cp) return false; + $cp = $this->getCP($whmcsService->server); + if (! $cp) { + return false; + } - return [ - 'service' => $service, - 'whmcsService' => $whmcsService, - 'cp' => $cp, - 'request' => $this->initCurl($cp['token']), - 'serverId' => (int) $service->server_id, - ]; + return [ + 'service' => $service, + 'whmcsService' => $whmcsService, + 'cp' => $cp, + 'request' => $this->initCurl($cp['token']), + 'serverId' => (int) $service->server_id, + ]; + } catch (\Exception $e) { + Log::insert(__FUNCTION__, [], $e->getMessage()); + + return false; + } } /** - * @param int $serviceID + * @param int $serviceID * @return false|string */ public function fetchLoginTokens($serviceID) { - $ctx = $this->resolveServiceContext($serviceID); - if (!$ctx) return false; - - $data = $ctx['request']->post($ctx['cp']['url'] . '/users/' . (int) $ctx['whmcsService']->userid . '/serverAuthenticationTokens/' . $ctx['serverId']); - Log::insert(__FUNCTION__, $ctx['request']->getRequestInfo(), $data); - - if ($ctx['request']->getRequestInfo('http_code') == '200') { - $data = json_decode($data); - if (isset($data->data->authentication->endpoint_complete)) { - return $ctx['cp']['base_url'] . $data->data->authentication->endpoint_complete; + try { + $ctx = $this->resolveServiceContext($serviceID); + if (! $ctx) { + return false; } + + $data = $ctx['request']->post($ctx['cp']['url'] . '/users/' . (int) $ctx['whmcsService']->userid . '/serverAuthenticationTokens/' . $ctx['serverId']); + Log::insert(__FUNCTION__, $ctx['request']->getRequestInfo(), $data); + + if ($ctx['request']->getRequestInfo('http_code') == '200') { + $data = json_decode($data); + if (isset($data->data->authentication->endpoint_complete)) { + return $ctx['cp']['base_url'] . $data->data->authentication->endpoint_complete; + } + } + + return false; + } catch (\Exception $e) { + Log::insert(__FUNCTION__, [], $e->getMessage()); + + return false; } - return false; } + /** + * Extract IP address and hostname from a VirtFusion server object and persist + * them to the corresponding tblhosting record (dedicatedip, domain, username, + * password). + * + * @param int $serviceId WHMCS service ID + * @param object $data Raw server object returned by the VirtFusion API + * @return void + */ public function updateWhmcsServiceParamsOnServerObject($serviceId, $data) { - $output = []; + try { + $output = []; - $serverResource = (new ServerResource())->process($data); + $serverResource = (new ServerResource)->process($data); - $dedicatedIpv4 = null; + $dedicatedIpv4 = null; - if (count($serverResource['primaryNetwork']['ipv4Unformatted'])) { - $dedicatedIpv4 = $serverResource['primaryNetwork']['ipv4Unformatted'][0]; - } - - if ($serverResource['hostname'] == '-') { - if ($serverResource['name'] == '-') { - $name = ''; - } else { - $name = $serverResource['name']; + if (count($serverResource['primaryNetwork']['ipv4Unformatted'])) { + $dedicatedIpv4 = $serverResource['primaryNetwork']['ipv4Unformatted'][0]; } - } else { - $name = $serverResource['hostname']; + + 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); + } catch (\Exception $e) { + Log::insert(__FUNCTION__, [], $e->getMessage()); } - - $output['tblhosting'] = ["dedicatedip" => $dedicatedIpv4, "domain" => $name, "username" => $serverResource['username'], "password" => $serverResource['password']]; - - Database::updateWhmcsServiceParams($serviceId, $output); } + /** + * Clear the dedicated IP on the tblhosting record when a server is terminated. + * + * @param int $serviceId WHMCS service ID + * @return void + */ public function updateWhmcsServiceParamsOnDestroy($serviceId) { - $output['tblhosting'] = ["dedicatedip" => null]; + try { + $output['tblhosting'] = ['dedicatedip' => null]; - Database::updateWhmcsServiceParams($serviceId, $output); + Database::updateWhmcsServiceParams($serviceId, $output); + } catch (\Exception $e) { + Log::insert(__FUNCTION__, [], $e->getMessage()); + } } + /** + * Fetch full server details from the VirtFusion API for a given service. + * + * @param int $serviceID WHMCS service ID + * @return object|false Decoded API response object, or false on failure + */ public function fetchServerData($serviceID) { - $ctx = $this->resolveServiceContext($serviceID); - if (!$ctx) return false; + try { + $ctx = $this->resolveServiceContext($serviceID); + if (! $ctx) { + return false; + } - $data = $ctx['request']->get($ctx['cp']['url'] . '/servers/' . $ctx['serverId']); - Log::insert(__FUNCTION__, $ctx['request']->getRequestInfo(), $data); + $data = $ctx['request']->get($ctx['cp']['url'] . '/servers/' . $ctx['serverId']); + Log::insert(__FUNCTION__, $ctx['request']->getRequestInfo(), $data); - if ($ctx['request']->getRequestInfo('http_code') == '200') { - return json_decode($data); + if ($ctx['request']->getRequestInfo('http_code') == '200') { + return json_decode($data); + } + + return false; + } catch (\Exception $e) { + Log::insert(__FUNCTION__, [], $e->getMessage()); + + return false; } - return false; } /** * Execute a power action on a server. * - * @param int $serviceID - * @param string $action One of: boot, shutdown, restart, poweroff + * @param int $serviceID + * @param string $action One of: boot, shutdown, restart, poweroff * @return object|false */ public function serverPowerAction($serviceID, $action) { - $allowedActions = ['boot', 'shutdown', 'restart', 'poweroff']; - if (!in_array($action, $allowedActions, true)) { + try { + $allowedActions = ['boot', 'shutdown', 'restart', 'poweroff']; + if (! in_array($action, $allowedActions, true)) { + return false; + } + + $ctx = $this->resolveServiceContext($serviceID); + if (! $ctx) { + return false; + } + + $data = $ctx['request']->post($ctx['cp']['url'] . '/servers/' . $ctx['serverId'] . '/power/' . $action); + Log::insert(__FUNCTION__ . ':' . $action, $ctx['request']->getRequestInfo(), $data); + + $httpCode = $ctx['request']->getRequestInfo('http_code'); + if ($httpCode == 200 || $httpCode == 204) { + return json_decode($data) ?: (object) ['success' => true]; + } + + return false; + } catch (\Exception $e) { + Log::insert(__FUNCTION__, [], $e->getMessage()); + return false; } - - $ctx = $this->resolveServiceContext($serviceID); - if (!$ctx) return false; - - $data = $ctx['request']->post($ctx['cp']['url'] . '/servers/' . $ctx['serverId'] . '/power/' . $action); - Log::insert(__FUNCTION__ . ':' . $action, $ctx['request']->getRequestInfo(), $data); - - $httpCode = $ctx['request']->getRequestInfo('http_code'); - if ($httpCode == 200 || $httpCode == 204) { - return json_decode($data) ?: (object) ['success' => true]; - } - return false; } /** * Rebuild/reinstall a server with a new OS. * - * @param int $serviceID - * @param int $osId Operating system template ID - * @param string|null $hostname Optional new hostname + * @param int $serviceID + * @param int $osId Operating system template ID + * @param string|null $hostname Optional new hostname * @return object|false */ public function rebuildServer($serviceID, $osId, $hostname = null) { - $osId = (int) $osId; - if ($osId <= 0) return false; + try { + $osId = (int) $osId; + if ($osId <= 0) { + return false; + } - $ctx = $this->resolveServiceContext($serviceID); - if (!$ctx) return false; + $ctx = $this->resolveServiceContext($serviceID); + if (! $ctx) { + return false; + } - $buildData = ['operatingSystemId' => $osId, 'email' => true]; - if ($hostname !== null && $hostname !== '') { - $buildData['hostname'] = $hostname; + $buildData = ['operatingSystemId' => $osId, 'email' => true]; + if ($hostname !== null && $hostname !== '') { + $buildData['hostname'] = $hostname; + } + + $ctx['request']->addOption(CURLOPT_POSTFIELDS, json_encode($buildData)); + $data = $ctx['request']->post($ctx['cp']['url'] . '/servers/' . $ctx['serverId'] . '/build'); + Log::insert(__FUNCTION__, $ctx['request']->getRequestInfo(), $data); + + $httpCode = $ctx['request']->getRequestInfo('http_code'); + if ($httpCode == 200 || $httpCode == 201) { + Cache::forget('backups:' . $ctx['serverId']); + + return json_decode($data) ?: (object) ['success' => true]; + } + + return false; + } catch (\Exception $e) { + Log::insert(__FUNCTION__, [], $e->getMessage()); + + return false; } - - $ctx['request']->addOption(CURLOPT_POSTFIELDS, json_encode($buildData)); - $data = $ctx['request']->post($ctx['cp']['url'] . '/servers/' . $ctx['serverId'] . '/build'); - Log::insert(__FUNCTION__, $ctx['request']->getRequestInfo(), $data); - - $httpCode = $ctx['request']->getRequestInfo('http_code'); - if ($httpCode == 200 || $httpCode == 201) { - Cache::forget('backups:' . $ctx['serverId']); - return json_decode($data) ?: (object) ['success' => true]; - } - return false; } /** * Rename a server. * - * @param int $serviceID - * @param string $newName + * @param int $serviceID + * @param string $newName * @return bool */ public function renameServer($serviceID, $newName) { - $newName = trim($newName); - if (empty($newName) || strlen($newName) > 255) return false; + try { + $newName = trim($newName); + if (empty($newName) || strlen($newName) > 255) { + return false; + } - $ctx = $this->resolveServiceContext($serviceID); - if (!$ctx) return false; + $ctx = $this->resolveServiceContext($serviceID); + if (! $ctx) { + return false; + } - $ctx['request']->addOption(CURLOPT_POSTFIELDS, json_encode(['name' => $newName])); - $data = $ctx['request']->patch($ctx['cp']['url'] . '/servers/' . $ctx['serverId'] . '/name'); - Log::insert(__FUNCTION__, $ctx['request']->getRequestInfo(), $data); + $ctx['request']->addOption(CURLOPT_POSTFIELDS, json_encode(['name' => $newName])); + $data = $ctx['request']->patch($ctx['cp']['url'] . '/servers/' . $ctx['serverId'] . '/name'); + Log::insert(__FUNCTION__, $ctx['request']->getRequestInfo(), $data); - $httpCode = $ctx['request']->getRequestInfo('http_code'); - return ($httpCode == 200 || $httpCode == 204); + $httpCode = $ctx['request']->getRequestInfo('http_code'); + + return $httpCode == 200 || $httpCode == 204; + } catch (\Exception $e) { + Log::insert(__FUNCTION__, [], $e->getMessage()); + + return false; + } } /** * Fetch available OS templates for a server's package. * - * @param int $serviceID + * @param int $serviceID * @return array|false */ public function fetchOsTemplates($serviceID) { - $ctx = $this->resolveServiceContext($serviceID); - if (!$ctx) return false; + try { + $ctx = $this->resolveServiceContext($serviceID); + if (! $ctx) { + return false; + } - $product = \WHMCS\Database\Capsule::table('tblproducts')->where('id', $ctx['whmcsService']->packageid)->first(); - if (!$product || !$product->configoption2) return false; + $product = Capsule::table('tblproducts')->where('id', $ctx['whmcsService']->packageid)->first(); + if (! $product || ! $product->configoption2) { + return false; + } - $cacheKey = 'os:' . (int) $product->configoption2; - $cached = Cache::get($cacheKey); - if ($cached !== null) return $cached; + $cacheKey = 'os:' . (int) $product->configoption2; + $cached = Cache::get($cacheKey); + if ($cached !== null) { + return $cached; + } - $data = $ctx['request']->get($ctx['cp']['url'] . '/media/templates/fromServerPackageSpec/' . (int) $product->configoption2); - Log::insert(__FUNCTION__, $ctx['request']->getRequestInfo(), $data); + $data = $ctx['request']->get($ctx['cp']['url'] . '/media/templates/fromServerPackageSpec/' . (int) $product->configoption2); + Log::insert(__FUNCTION__, $ctx['request']->getRequestInfo(), $data); - if ($ctx['request']->getRequestInfo('http_code') == '200') { - $templates = json_decode($data, true); - $baseUrl = rtrim(str_replace('/api/v1', '', $ctx['cp']['url']), '/'); + if ($ctx['request']->getRequestInfo('http_code') == '200') { + $templates = json_decode($data, true); + $baseUrl = rtrim(str_replace('/api/v1', '', $ctx['cp']['url']), '/'); - $result = [ - 'baseUrl' => $baseUrl, - 'categories' => self::groupOsTemplates($templates['data'] ?? []), - ]; + $result = [ + 'baseUrl' => $baseUrl, + 'categories' => self::groupOsTemplates($templates['data'] ?? []), + ]; - Cache::set($cacheKey, $result, 600); - return $result; + Cache::set($cacheKey, $result, 600); + + return $result; + } + + return false; + } catch (\Exception $e) { + Log::insert(__FUNCTION__, [], $e->getMessage()); + + return false; } - return false; } /** * Group OS template data into categories. Categories with only 1 template * are merged into an "Other" category. * - * @param array $data Raw template data from VirtFusion API - * @param bool $htmlEscape Whether to escape names for HTML output - * @return array + * @param array $data Raw template data from VirtFusion API + * @param bool $htmlEscape Whether to escape names for HTML output */ public static function groupOsTemplates(array $data, bool $htmlEscape = false): array { $categories = []; $otherTemplates = []; - $esc = fn($v) => $htmlEscape ? htmlspecialchars($v, ENT_QUOTES, 'UTF-8') : $v; + $esc = fn ($v) => $htmlEscape ? htmlspecialchars($v, ENT_QUOTES, 'UTF-8') : $v; foreach ($data as $osCategory) { $catTemplates = []; @@ -312,7 +432,7 @@ class Module } } - if (!empty($otherTemplates)) { + if (! empty($otherTemplates)) { $categories[] = ['name' => 'Other', 'icon' => null, 'templates' => $otherTemplates]; } @@ -326,27 +446,39 @@ class Module /** * Get traffic statistics for a server. * - * @param int $serviceID + * @param int $serviceID * @return array|false */ public function getTrafficStats($serviceID) { - $ctx = $this->resolveServiceContext($serviceID); - if (!$ctx) return false; + try { + $ctx = $this->resolveServiceContext($serviceID); + if (! $ctx) { + return false; + } - $cacheKey = 'traffic:' . $ctx['serverId']; - $cached = Cache::get($cacheKey); - if ($cached !== null) return $cached; + $cacheKey = 'traffic:' . $ctx['serverId']; + $cached = Cache::get($cacheKey); + if ($cached !== null) { + return $cached; + } - $data = $ctx['request']->get($ctx['cp']['url'] . '/servers/' . $ctx['serverId'] . '/traffic'); - Log::insert(__FUNCTION__, $ctx['request']->getRequestInfo(), $data); + $data = $ctx['request']->get($ctx['cp']['url'] . '/servers/' . $ctx['serverId'] . '/traffic'); + Log::insert(__FUNCTION__, $ctx['request']->getRequestInfo(), $data); - if ($ctx['request']->getRequestInfo('http_code') == 200) { - $result = json_decode($data, true); - Cache::set($cacheKey, $result, 120); - return $result; + if ($ctx['request']->getRequestInfo('http_code') == 200) { + $result = json_decode($data, true); + Cache::set($cacheKey, $result, 120); + + return $result; + } + + return false; + } catch (\Exception $e) { + Log::insert(__FUNCTION__, [], $e->getMessage()); + + return false; } - return false; } // ========================================================================= @@ -356,27 +488,39 @@ class Module /** * Get backup list for a server. * - * @param int $serviceID + * @param int $serviceID * @return array|false */ public function getServerBackups($serviceID) { - $ctx = $this->resolveServiceContext($serviceID); - if (!$ctx) return false; + try { + $ctx = $this->resolveServiceContext($serviceID); + if (! $ctx) { + return false; + } - $cacheKey = 'backups:' . $ctx['serverId']; - $cached = Cache::get($cacheKey); - if ($cached !== null) return $cached; + $cacheKey = 'backups:' . $ctx['serverId']; + $cached = Cache::get($cacheKey); + if ($cached !== null) { + return $cached; + } - $data = $ctx['request']->get($ctx['cp']['url'] . '/backups/server/' . $ctx['serverId']); - Log::insert(__FUNCTION__, $ctx['request']->getRequestInfo(), $data); + $data = $ctx['request']->get($ctx['cp']['url'] . '/backups/server/' . $ctx['serverId']); + Log::insert(__FUNCTION__, $ctx['request']->getRequestInfo(), $data); - if ($ctx['request']->getRequestInfo('http_code') == 200) { - $result = json_decode($data, true); - Cache::set($cacheKey, $result, 120); - return $result; + if ($ctx['request']->getRequestInfo('http_code') == 200) { + $result = json_decode($data, true); + Cache::set($cacheKey, $result, 120); + + return $result; + } + + return false; + } catch (\Exception $e) { + Log::insert(__FUNCTION__, [], $e->getMessage()); + + return false; } - return false; } // ========================================================================= @@ -386,44 +530,62 @@ class Module /** * Get VNC console connection details for a server. * - * @param int $serviceID + * @param int $serviceID * @return array|false */ public function getVncConsole($serviceID) { - $ctx = $this->resolveServiceContext($serviceID); - if (!$ctx) return false; + try { + $ctx = $this->resolveServiceContext($serviceID); + if (! $ctx) { + return false; + } - $data = $ctx['request']->get($ctx['cp']['url'] . '/servers/' . $ctx['serverId'] . '/vnc'); - Log::insert(__FUNCTION__, $ctx['request']->getRequestInfo(), $data); + $data = $ctx['request']->get($ctx['cp']['url'] . '/servers/' . $ctx['serverId'] . '/vnc'); + Log::insert(__FUNCTION__, $ctx['request']->getRequestInfo(), $data); - if ($ctx['request']->getRequestInfo('http_code') == 200) { - return json_decode($data, true); + if ($ctx['request']->getRequestInfo('http_code') == 200) { + return json_decode($data, true); + } + + return false; + } catch (\Exception $e) { + Log::insert(__FUNCTION__, [], $e->getMessage()); + + return false; } - return false; } /** * Toggle VNC on/off for a server. * - * @param int $serviceID - * @param bool $enabled + * @param int $serviceID + * @param bool $enabled * @return array|false */ public function toggleVnc($serviceID, $enabled) { - $ctx = $this->resolveServiceContext($serviceID); - if (!$ctx) return false; + try { + $ctx = $this->resolveServiceContext($serviceID); + if (! $ctx) { + return false; + } - $ctx['request']->addOption(CURLOPT_POSTFIELDS, json_encode(['enabled' => (bool) $enabled])); - $data = $ctx['request']->post($ctx['cp']['url'] . '/servers/' . $ctx['serverId'] . '/vnc'); - Log::insert(__FUNCTION__, $ctx['request']->getRequestInfo(), $data); + $ctx['request']->addOption(CURLOPT_POSTFIELDS, json_encode(['enabled' => (bool) $enabled])); + $data = $ctx['request']->post($ctx['cp']['url'] . '/servers/' . $ctx['serverId'] . '/vnc'); + Log::insert(__FUNCTION__, $ctx['request']->getRequestInfo(), $data); - $httpCode = $ctx['request']->getRequestInfo('http_code'); - if ($httpCode == 200 || $httpCode == 204) { - return json_decode($data, true) ?: ['success' => true]; + $httpCode = $ctx['request']->getRequestInfo('http_code'); + if ($httpCode == 200 || $httpCode == 204) { + return json_decode($data, true) ?: ['success' => true]; + } + + return false; + } catch (\Exception $e) { + Log::insert(__FUNCTION__, [], $e->getMessage()); + + return false; } - return false; } // ========================================================================= @@ -433,31 +595,44 @@ class Module /** * Modify a server resource (memory, cpuCores, or traffic). * - * @param int $serviceID - * @param string $resource One of: memory, cpuCores, traffic - * @param int $value New value for the resource + * @param int $serviceID + * @param string $resource One of: memory, cpuCores, traffic + * @param int $value New value for the resource * @return object|false */ public function modifyResource($serviceID, $resource, $value) { - $allowedResources = ['memory', 'cpuCores', 'traffic']; - if (!in_array($resource, $allowedResources, true)) return false; + try { + $allowedResources = ['memory', 'cpuCores', 'traffic']; + if (! in_array($resource, $allowedResources, true)) { + return false; + } - $value = (int) $value; - if ($value < 0) return false; + $value = (int) $value; + if ($value < 0) { + return false; + } - $ctx = $this->resolveServiceContext($serviceID); - if (!$ctx) return false; + $ctx = $this->resolveServiceContext($serviceID); + if (! $ctx) { + return false; + } - $ctx['request']->addOption(CURLOPT_POSTFIELDS, json_encode([$resource => $value])); - $data = $ctx['request']->put($ctx['cp']['url'] . '/servers/' . $ctx['serverId'] . '/modify/' . $resource); - Log::insert(__FUNCTION__ . ':' . $resource, $ctx['request']->getRequestInfo(), $data); + $ctx['request']->addOption(CURLOPT_POSTFIELDS, json_encode([$resource => $value])); + $data = $ctx['request']->put($ctx['cp']['url'] . '/servers/' . $ctx['serverId'] . '/modify/' . $resource); + Log::insert(__FUNCTION__ . ':' . $resource, $ctx['request']->getRequestInfo(), $data); - $httpCode = $ctx['request']->getRequestInfo('http_code'); - if ($httpCode == 200 || $httpCode == 204) { - return json_decode($data) ?: (object) ['success' => true]; + $httpCode = $ctx['request']->getRequestInfo('http_code'); + if ($httpCode == 200 || $httpCode == 204) { + return json_decode($data) ?: (object) ['success' => true]; + } + + return false; + } catch (\Exception $e) { + Log::insert(__FUNCTION__, [], $e->getMessage()); + + return false; } - return false; } // ========================================================================= @@ -467,83 +642,117 @@ class Module /** * Validate server creation parameters without actually creating a server. * - * @param array $options Server creation options - * @param int $serverId WHMCS server ID for API credentials + * @param array $options Server creation options + * @param int $serverId WHMCS server ID for API credentials * @return array ['valid' => bool, 'errors' => array] */ public function validateServerCreation($options, $serverId) { - $cp = $this->getCP($serverId, !$serverId); - if (!$cp) { - return ['valid' => false, 'errors' => ['No control server found']]; + try { + $cp = $this->getCP($serverId, ! $serverId); + if (! $cp) { + return ['valid' => false, 'errors' => ['No control server found']]; + } + + $request = $this->initCurl($cp['token']); + $request->addOption(CURLOPT_POSTFIELDS, json_encode($options)); + $data = $request->post($cp['url'] . '/servers?dryRun=true'); + + Log::insert(__FUNCTION__, $request->getRequestInfo(), $data); + + $httpCode = $request->getRequestInfo('http_code'); + $response = json_decode($data, true); + + if ($httpCode == 200 || $httpCode == 201) { + return ['valid' => true, 'errors' => []]; + } + + $errors = []; + if (isset($response['errors']) && is_array($response['errors'])) { + $errors = $response['errors']; + } elseif (isset($response['msg'])) { + $errors = [$response['msg']]; + } else { + $errors = ['Validation failed with HTTP ' . $httpCode]; + } + + return ['valid' => false, 'errors' => $errors]; + } catch (\Exception $e) { + Log::insert(__FUNCTION__, [], $e->getMessage()); + + return ['valid' => false, 'errors' => [$e->getMessage()]]; } - - $request = $this->initCurl($cp['token']); - $request->addOption(CURLOPT_POSTFIELDS, json_encode($options)); - $data = $request->post($cp['url'] . '/servers?dryRun=true'); - - Log::insert(__FUNCTION__, $request->getRequestInfo(), $data); - - $httpCode = $request->getRequestInfo('http_code'); - $response = json_decode($data, true); - - if ($httpCode == 200 || $httpCode == 201) { - return ['valid' => true, 'errors' => []]; - } - - $errors = []; - if (isset($response['errors']) && is_array($response['errors'])) { - $errors = $response['errors']; - } elseif (isset($response['msg'])) { - $errors = [$response['msg']]; - } else { - $errors = ['Validation failed with HTTP ' . $httpCode]; - } - - return ['valid' => false, 'errors' => $errors]; } /** * Reset the server's root password. * - * @param int $serviceID + * @param int $serviceID * @return array|false */ public function resetServerPassword($serviceID) { - $ctx = $this->resolveServiceContext($serviceID); - if (!$ctx) return false; + try { + $ctx = $this->resolveServiceContext($serviceID); + if (! $ctx) { + return false; + } - $data = $ctx['request']->post($ctx['cp']['url'] . '/servers/' . $ctx['serverId'] . '/resetPassword'); - Log::insert(__FUNCTION__, $ctx['request']->getRequestInfo(), $data); + $data = $ctx['request']->post($ctx['cp']['url'] . '/servers/' . $ctx['serverId'] . '/resetPassword'); + Log::insert(__FUNCTION__, $ctx['request']->getRequestInfo(), $data); - $httpCode = $ctx['request']->getRequestInfo('http_code'); - if ($httpCode == 200 || $httpCode == 201) { - return json_decode($data, true); + $httpCode = $ctx['request']->getRequestInfo('http_code'); + if ($httpCode == 200 || $httpCode == 201) { + return json_decode($data, true); + } + + return false; + } catch (\Exception $e) { + Log::insert(__FUNCTION__, [], $e->getMessage()); + + return false; } - return false; - } - - public function resetUserPassword($serviceID, $clientID) - { - $clientID = (int) $clientID; - $ctx = $this->resolveServiceContext($serviceID); - if (!$ctx) return false; - - $data = $ctx['request']->post($ctx['cp']['url'] . '/users/' . $clientID . '/byExtRelation/resetPassword'); - Log::insert(__FUNCTION__, $ctx['request']->getRequestInfo(), $data); - - if ($ctx['request']->getRequestInfo('http_code') == '201') { - return json_decode($data); - } - return false; } /** - * @param $data - * @param bool $json - * @param bool $exit - * @param int $rspCode + * Reset the VirtFusion panel login password for a user identified by their + * WHMCS client ID (used as the external relation ID in VirtFusion). + * + * @param int $serviceID WHMCS service ID + * @param int $clientID WHMCS client ID (mapped to VirtFusion external relation ID) + * @return object|false Decoded API response object, or false on failure + */ + public function resetUserPassword($serviceID, $clientID) + { + try { + $clientID = (int) $clientID; + $ctx = $this->resolveServiceContext($serviceID); + if (! $ctx) { + return false; + } + + $data = $ctx['request']->post($ctx['cp']['url'] . '/users/' . $clientID . '/byExtRelation/resetPassword'); + Log::insert(__FUNCTION__, $ctx['request']->getRequestInfo(), $data); + + if ($ctx['request']->getRequestInfo('http_code') == '201') { + return json_decode($data); + } + + return false; + } catch (\Exception $e) { + Log::insert(__FUNCTION__, [], $e->getMessage()); + + return false; + } + } + + /** + * Send a JSON or raw response to the client and optionally terminate execution. + * + * @param mixed $data Response payload; encoded as JSON when $json is true + * @param bool $json Whether to JSON-encode $data and set the Content-Type header + * @param bool $exit Whether to call exit() after sending the response + * @param int $rspCode HTTP status code to send */ public function output($data, $json = true, $exit = true, $rspCode = 200) { @@ -562,28 +771,39 @@ class Module } /** - * @param $server - * @return array|false + * Resolve a WHMCS server record into an API base URL and decrypted Bearer token. + * + * @param int|object $server WHMCS server ID or server object + * @param bool $any When true, fall back to any available server if the given one is not found + * @return array{url: string, base_url: string, token: string}|false */ public function getCP($server, $any = false) { - $cp = Database::getWhmcsServer($server, $any); + try { + $cp = Database::getWhmcsServer($server, $any); - if ($cp) { - return [ - 'url' => 'https://' . $cp->hostname . '/api/v1', - 'base_url' => 'https://' . $cp->hostname, - 'token' => decrypt($cp->password)]; + if ($cp) { + return [ + 'url' => 'https://' . $cp->hostname . '/api/v1', + 'base_url' => 'https://' . $cp->hostname, + 'token' => decrypt($cp->password)]; + } + } catch (\Exception $e) { + Log::insert(__FUNCTION__, [], $e->getMessage()); } + return false; } /** + * Enforce WHMCS admin authentication. Returns true if the current user is an + * authenticated admin; otherwise sends a 401 JSON response and exits. + * * @return bool|void */ public function adminOnly() { - if ((new \WHMCS\Authentication\CurrentUser)->isAuthenticatedAdmin()) { + if ((new CurrentUser)->isAuthenticatedAdmin()) { return true; } @@ -591,11 +811,14 @@ class Module } /** + * Enforce WHMCS client authentication. Returns true if the current user is an + * authenticated client; otherwise sends a 401 JSON response and exits. + * * @return bool|void */ public function isAuthenticated() { - if ((new \WHMCS\Authentication\CurrentUser)->isAuthenticatedUser()) { + if ((new CurrentUser)->isAuthenticatedUser()) { return true; } @@ -603,17 +826,20 @@ class Module } /** - * @param $token - * @return \WHMCS\Module\Server\VirtFusionDirect\Curl + * Create a pre-configured Curl instance with JSON Accept/Content-Type headers + * and a Bearer token for authenticating against the VirtFusion API. + * + * @param string $token VirtFusion API Bearer token + * @return Curl */ public function initCurl($token) { - $curl = new Curl(); + $curl = new Curl; $curl->addOption(CURLOPT_HTTPHEADER, [ 'Accept: application/json', 'Content-type: application/json; charset=utf-8', - 'authorization: Bearer ' . $token + 'authorization: Bearer ' . $token, ]); return $curl; @@ -626,101 +852,132 @@ class Module /** * Get self-service usage data for a WHMCS client. * - * @param int $serviceID + * @param int $serviceID * @return array|false */ public function getSelfServiceUsage($serviceID) { - $serviceID = (int) $serviceID; - $whmcsService = Database::getWhmcsService($serviceID); - if (!$whmcsService) return false; + try { + $serviceID = (int) $serviceID; + $whmcsService = Database::getWhmcsService($serviceID); + if (! $whmcsService) { + return false; + } - $cp = $this->getCP($whmcsService->server); - if (!$cp) return false; + $cp = $this->getCP($whmcsService->server); + if (! $cp) { + return false; + } - $request = $this->initCurl($cp['token']); - $data = $request->get($cp['url'] . '/selfService/usage/byUserExtRelationId/' . (int) $whmcsService->userid); + $request = $this->initCurl($cp['token']); + $data = $request->get($cp['url'] . '/selfService/usage/byUserExtRelationId/' . (int) $whmcsService->userid); - Log::insert(__FUNCTION__, $request->getRequestInfo(), $data); + Log::insert(__FUNCTION__, $request->getRequestInfo(), $data); - if ($request->getRequestInfo('http_code') == 200) { - return json_decode($data, true); + if ($request->getRequestInfo('http_code') == 200) { + return json_decode($data, true); + } + + return false; + } catch (\Exception $e) { + Log::insert(__FUNCTION__, [], $e->getMessage()); + + return false; } - return false; } /** * Get self-service billing report for a WHMCS client. * - * @param int $serviceID + * @param int $serviceID * @return array|false */ public function getSelfServiceReport($serviceID) { - $serviceID = (int) $serviceID; - $whmcsService = Database::getWhmcsService($serviceID); - if (!$whmcsService) return false; + try { + $serviceID = (int) $serviceID; + $whmcsService = Database::getWhmcsService($serviceID); + if (! $whmcsService) { + return false; + } - $cp = $this->getCP($whmcsService->server); - if (!$cp) return false; + $cp = $this->getCP($whmcsService->server); + if (! $cp) { + return false; + } - $request = $this->initCurl($cp['token']); - $data = $request->get($cp['url'] . '/selfService/report/byUserExtRelationId/' . (int) $whmcsService->userid); + $request = $this->initCurl($cp['token']); + $data = $request->get($cp['url'] . '/selfService/report/byUserExtRelationId/' . (int) $whmcsService->userid); - Log::insert(__FUNCTION__, $request->getRequestInfo(), $data); + Log::insert(__FUNCTION__, $request->getRequestInfo(), $data); - if ($request->getRequestInfo('http_code') == 200) { - return json_decode($data, true); + if ($request->getRequestInfo('http_code') == 200) { + return json_decode($data, true); + } + + return false; + } catch (\Exception $e) { + Log::insert(__FUNCTION__, [], $e->getMessage()); + + return false; } - return false; } /** * Add self-service credit for a WHMCS client. * - * @param int $serviceID - * @param float $tokens Amount of credit tokens to add - * @param string $reference Reference text for the transaction + * @param int $serviceID + * @param float $tokens Amount of credit tokens to add + * @param string $reference Reference text for the transaction * @return array|false */ public function addSelfServiceCredit($serviceID, $tokens, $reference = '') { - $serviceID = (int) $serviceID; - $tokens = (float) $tokens; + try { + $serviceID = (int) $serviceID; + $tokens = (float) $tokens; + + if ($tokens <= 0) { + return false; + } + + $whmcsService = Database::getWhmcsService($serviceID); + if (! $whmcsService) { + return false; + } + + $cp = $this->getCP($whmcsService->server); + if (! $cp) { + return false; + } + + $request = $this->initCurl($cp['token']); + $request->addOption(CURLOPT_POSTFIELDS, json_encode([ + 'tokens' => $tokens, + 'reference_1' => $reference ?: 'WHMCS Top-up', + 'reference_2' => 'Service #' . $serviceID, + ])); + $data = $request->post($cp['url'] . '/selfService/credit/byUserExtRelationId/' . (int) $whmcsService->userid); + + Log::insert(__FUNCTION__, $request->getRequestInfo(), $data); + + $httpCode = $request->getRequestInfo('http_code'); + if ($httpCode == 200 || $httpCode == 201) { + return json_decode($data, true); + } + + return false; + } catch (\Exception $e) { + Log::insert(__FUNCTION__, [], $e->getMessage()); - if ($tokens <= 0) { return false; } - - $whmcsService = Database::getWhmcsService($serviceID); - if (!$whmcsService) return false; - - $cp = $this->getCP($whmcsService->server); - if (!$cp) return false; - - $request = $this->initCurl($cp['token']); - $request->addOption(CURLOPT_POSTFIELDS, json_encode([ - 'tokens' => $tokens, - 'reference_1' => $reference ?: 'WHMCS Top-up', - 'reference_2' => 'Service #' . $serviceID, - ])); - $data = $request->post($cp['url'] . '/selfService/credit/byUserExtRelationId/' . (int) $whmcsService->userid); - - Log::insert(__FUNCTION__, $request->getRequestInfo(), $data); - - $httpCode = $request->getRequestInfo('http_code'); - if ($httpCode == 200 || $httpCode == 201) { - return json_decode($data, true); - } - return false; } /** * Decodes a response from JSON into an associative array. * - * @param string $response * - * @return array * @throws \JsonException */ public function decodeResponseFromJson(string $response): array diff --git a/modules/servers/VirtFusionDirect/lib/ModuleFunctions.php b/modules/servers/VirtFusionDirect/lib/ModuleFunctions.php index 0a6d167..5d8516b 100644 --- a/modules/servers/VirtFusionDirect/lib/ModuleFunctions.php +++ b/modules/servers/VirtFusionDirect/lib/ModuleFunctions.php @@ -2,6 +2,12 @@ namespace WHMCS\Module\Server\VirtFusionDirect; +/** + * Extends Module to handle the WHMCS service lifecycle for VirtFusion servers. + * + * Responsibilities include: provisioning (create, suspend, unsuspend, terminate), + * package changes, usage updates, client area rendering, and admin tab fields. + */ class ModuleFunctions extends Module { public function __construct() @@ -10,13 +16,13 @@ class ModuleFunctions extends Module } /** + * Provision a new VirtFusion server for a WHMCS service. * - * 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. + * Ensures a matching VirtFusion user exists (creating one if needed), then creates + * the server and triggers the OS build via ConfigureService::initServerBuild(). * + * @param array $params WHMCS service parameters + * @return string 'success' or an error message */ public function createAccount($params) { @@ -33,9 +39,9 @@ class ModuleFunctions extends Module * If no VirtFusionDirect control server exists, cancel the create account action. */ $server = $params['serverid'] ?: false; - $cp = $this->getCP($server, !$server); + $cp = $this->getCP($server, ! $server); - if (!$cp) { + if (! $cp) { return 'No Control server found. Please ensure a VirtFusion server is configured in WHMCS.'; } @@ -62,16 +68,16 @@ class ModuleFunctions extends Module */ $user = Database::getUser($params['userid']); - if (!$user) { + if (! $user) { return 'WHMCS user not found for ID ' . (int) $params['userid']; } $request = $this->initCurl($cp['token']); $userData = [ - "name" => $user->firstname . ' ' . $user->lastname, - "email" => $user->email, - "extRelationId" => $user->id, + 'name' => $user->firstname . ' ' . $user->lastname, + 'email' => $user->email, + 'extRelationId' => $user->id, ]; // Enable self-service billing if configured @@ -100,7 +106,6 @@ class ModuleFunctions extends Module /** * A user is available. We can now attempt to create a server. */ - $configOptionDefaultNaming = [ 'ipv4' => 'IPv4', 'packageId' => 'Package', @@ -122,10 +127,10 @@ class ModuleFunctions extends Module } $options = [ - "packageId" => (int) $params['configoption2'], - "userId" => $data->data->id, - "hypervisorId" => (int) $params['configoption1'], - "ipv4" => (int) $params['configoption3'], + 'packageId' => (int) $params['configoption2'], + 'userId' => $data->data->id, + 'hypervisorId' => (int) $params['configoption1'], + 'ipv4' => (int) $params['configoption3'], ]; if (array_key_exists('configoptions', $params)) { @@ -159,7 +164,7 @@ class ModuleFunctions extends Module $this->updateWhmcsServiceParamsOnServerObject($params['serviceid'], $data); // If the server is created successfully, we can initialize the server build. - $cs = new ConfigureService(); + $cs = new ConfigureService; $vfUserId = isset($data->data->owner->id) ? (int) $data->data->owner->id : null; $cs->initServerBuild($data->data->id, $params, $vfUserId); @@ -171,328 +176,440 @@ class ModuleFunctions extends Module if (isset($data->msg)) { return $data->msg; } + return 'Server creation failed. VirtFusion API returned HTTP ' . $request->getRequestInfo('http_code'); } } catch (\Exception $e) { Log::insert(__FUNCTION__, $params, $e->getMessage()); + return $e->getMessage(); } } /** - * Allows changing of the package of a server + * Change the VirtFusion package assigned to a server and apply resource modifications. * - * @param $params - * @return string + * Updates the package via the API, then individually adjusts memory, CPU, and bandwidth + * if those configurable options are present. + * + * @param array $params WHMCS service parameters + * @return string 'success' or an error message */ public function changePackage($params) { - $service = Database::getSystemService($params['serviceid']); + try { + $service = Database::getSystemService($params['serviceid']); - if ($service) { - $whmcsService = Database::getWhmcsService($params['serviceid']); - if (!$whmcsService) return 'WHMCS service record not found.'; - - $cp = $this->getCP($whmcsService->server); - if (!$cp) return 'No control server found.'; - - $request = $this->initCurl($cp['token']); - $data = $request->put($cp['url'] . '/servers/' . (int) $service->server_id . '/package/' . (int) $params['configoption2']); - $data = json_decode($data); - - Log::insert(__FUNCTION__, $request->getRequestInfo(), $data); - - switch ($request->getRequestInfo('http_code')) { - - case 204: - break; - case 404: - return 'The server or package was not found in VirtFusion (HTTP 404).'; - case 423: - if (isset($data->msg)) { - return $data->msg; - } - return 'The server is currently locked. Please try again later.'; - default: - return 'Update package request failed. VirtFusion API returned HTTP ' . $request->getRequestInfo('http_code'); - } - - // Apply individual resource modifications from configurable options - if (isset($params['configoptions']) && is_array($params['configoptions'])) { - $configOptionDefaultNaming = [ - 'memory' => 'Memory', - 'cpuCores' => 'CPU Cores', - 'traffic' => 'Bandwidth', - ]; - - $configOptionCustomNaming = []; - if (file_exists(ROOTDIR . '/modules/servers/VirtFusionDirect/config/ConfigOptionMapping.php')) { - $configOptionCustomNaming = require ROOTDIR . '/modules/servers/VirtFusionDirect/config/ConfigOptionMapping.php'; + if ($service) { + $whmcsService = Database::getWhmcsService($params['serviceid']); + if (! $whmcsService) { + return 'WHMCS service record not found.'; } - foreach ($configOptionDefaultNaming as $resource => $optionName) { - $currentOption = array_key_exists($resource, $configOptionCustomNaming) ? $configOptionCustomNaming[$resource] : $optionName; - if (isset($params['configoptions'][$currentOption]) && is_numeric($params['configoptions'][$currentOption])) { - $value = (int) $params['configoptions'][$currentOption]; - if ($resource === 'memory' && $value < 1024) { - $value = $value * 1024; + $cp = $this->getCP($whmcsService->server); + if (! $cp) { + return 'No control server found.'; + } + + $request = $this->initCurl($cp['token']); + $data = $request->put($cp['url'] . '/servers/' . (int) $service->server_id . '/package/' . (int) $params['configoption2']); + $data = json_decode($data); + + Log::insert(__FUNCTION__, $request->getRequestInfo(), $data); + + switch ($request->getRequestInfo('http_code')) { + + case 204: + break; + case 404: + return 'The server or package was not found in VirtFusion (HTTP 404).'; + case 423: + if (isset($data->msg)) { + return $data->msg; + } + + return 'The server is currently locked. Please try again later.'; + default: + return 'Update package request failed. VirtFusion API returned HTTP ' . $request->getRequestInfo('http_code'); + } + + // Apply individual resource modifications from configurable options + if (isset($params['configoptions']) && is_array($params['configoptions'])) { + $configOptionDefaultNaming = [ + 'memory' => 'Memory', + 'cpuCores' => 'CPU Cores', + 'traffic' => 'Bandwidth', + ]; + + $configOptionCustomNaming = []; + if (file_exists(ROOTDIR . '/modules/servers/VirtFusionDirect/config/ConfigOptionMapping.php')) { + $configOptionCustomNaming = require ROOTDIR . '/modules/servers/VirtFusionDirect/config/ConfigOptionMapping.php'; + } + + foreach ($configOptionDefaultNaming as $resource => $optionName) { + $currentOption = array_key_exists($resource, $configOptionCustomNaming) ? $configOptionCustomNaming[$resource] : $optionName; + if (isset($params['configoptions'][$currentOption]) && is_numeric($params['configoptions'][$currentOption])) { + $value = (int) $params['configoptions'][$currentOption]; + if ($resource === 'memory' && $value < 1024) { + $value = $value * 1024; + } + $this->modifyResource($params['serviceid'], $resource, $value); } - $this->modifyResource($params['serviceid'], $resource, $value); } } + + return 'success'; } - return 'success'; + return 'Service not found in module database.'; + } catch (\Exception $e) { + Log::insert(__FUNCTION__, $params, $e->getMessage()); + + return $e->getMessage(); } - return 'Service not found in module database.'; } /** + * Delete a VirtFusion server, applying the default 5-minute grace period before destruction. * - * 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. + * On success, removes the service record from the module database and clears WHMCS service fields. + * If VirtFusion reports the server is already gone (404 + "server not found"), treats it as success. * + * @param array $params WHMCS service parameters + * @return string 'success' or an error message */ public function terminateAccount($params) { - $service = Database::getSystemService($params['serviceid']); + try { + $service = Database::getSystemService($params['serviceid']); - if ($service) { + if ($service) { - $whmcsService = Database::getWhmcsService($params['serviceid']); - if (!$whmcsService) return 'WHMCS service record not found.'; + $whmcsService = Database::getWhmcsService($params['serviceid']); + if (! $whmcsService) { + return 'WHMCS service record not found.'; + } - $cp = $this->getCP($whmcsService->server); - if (!$cp) return 'No control server found.'; + $cp = $this->getCP($whmcsService->server); + if (! $cp) { + return 'No control server found.'; + } - $request = $this->initCurl($cp['token']); - $data = $request->delete($cp['url'] . '/servers/' . (int) $service->server_id); - $data = json_decode($data); + $request = $this->initCurl($cp['token']); + $data = $request->delete($cp['url'] . '/servers/' . (int) $service->server_id); + $data = json_decode($data); - Log::insert(__FUNCTION__, $request->getRequestInfo(), $data); + Log::insert(__FUNCTION__, $request->getRequestInfo(), $data); - switch ($request->getRequestInfo('http_code')) { + switch ($request->getRequestInfo('http_code')) { - case 204: - Database::deleteSystemService($params['serviceid']); - $this->updateWhmcsServiceParamsOnDestroy($params['serviceid']); - return 'success'; + case 204: + Database::deleteSystemService($params['serviceid']); + $this->updateWhmcsServiceParamsOnDestroy($params['serviceid']); - case 404: - if (isset($data->msg)) { - if ($data->msg == 'server not found') { - Database::deleteSystemService($params['serviceid']); - return 'success'; + return 'success'; + + case 404: + if (isset($data->msg)) { + if ($data->msg == 'server not found') { + Database::deleteSystemService($params['serviceid']); + + return 'success'; + } else { + return 'VirtFusion returned 404: ' . $data->msg; + } } else { - return 'VirtFusion returned 404: ' . $data->msg; + return 'VirtFusion returned 404 without details. The API may be unavailable.'; } - } else { - return 'VirtFusion returned 404 without details. The API may be unavailable.'; - } - default: - return 'Termination request failed. VirtFusion API returned HTTP ' . $request->getRequestInfo('http_code'); + default: + return 'Termination request failed. VirtFusion API returned HTTP ' . $request->getRequestInfo('http_code'); + } } + + return 'Service not found in module database. Has termination already been run?'; + } catch (\Exception $e) { + Log::insert(__FUNCTION__, $params, $e->getMessage()); + + return $e->getMessage(); } - return 'Service not found in module database. Has termination already been run?'; } /** + * Suspend a VirtFusion server, queuing the action if another operation is in progress. * - * 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. + * Returns 'success' whether the server is suspended immediately or queued for suspension. * + * @param array $params WHMCS service parameters + * @return string 'success' or an error message */ public function suspendAccount($params) { - $service = Database::getSystemService($params['serviceid']); + try { + $service = Database::getSystemService($params['serviceid']); - if ($service) { + if ($service) { - $whmcsService = Database::getWhmcsService($params['serviceid']); - if (!$whmcsService) return 'WHMCS service record not found.'; + $whmcsService = Database::getWhmcsService($params['serviceid']); + if (! $whmcsService) { + return 'WHMCS service record not found.'; + } - $cp = $this->getCP($whmcsService->server); - if (!$cp) return 'No control server found.'; + $cp = $this->getCP($whmcsService->server); + if (! $cp) { + return 'No control server found.'; + } - $request = $this->initCurl($cp['token']); - $data = $request->post($cp['url'] . '/servers/' . (int) $service->server_id . '/suspend'); - $data = json_decode($data); + $request = $this->initCurl($cp['token']); + $data = $request->post($cp['url'] . '/servers/' . (int) $service->server_id . '/suspend'); + $data = json_decode($data); - Log::insert(__FUNCTION__, $request->getRequestInfo(), $data); + Log::insert(__FUNCTION__, $request->getRequestInfo(), $data); - switch ($request->getRequestInfo('http_code')) { + switch ($request->getRequestInfo('http_code')) { - case 204: - return 'success'; + case 204: + return 'success'; - case 404: - if (isset($data->msg)) { - if ($data->msg == 'server not found') { - Database::deleteSystemService($params['serviceid']); - return 'success'; + case 404: + if (isset($data->msg)) { + if ($data->msg == 'server not found') { + Database::deleteSystemService($params['serviceid']); + + return 'success'; + } else { + return 'VirtFusion returned 404: ' . $data->msg; + } } else { - return 'VirtFusion returned 404: ' . $data->msg; + return 'VirtFusion returned 404 without details. The API may be unavailable.'; } - } else { - return 'VirtFusion returned 404 without details. The API may be unavailable.'; - } - case 423: - if (isset($data->msg)) { - return $data->msg; - } - return 'The server is currently locked. Please try again later.'; - - default: - return 'Suspend request failed. VirtFusion API returned HTTP ' . $request->getRequestInfo('http_code'); - } - } - return 'Service not found in module database.'; - } - - function updateServerObject($params) - { - $service = Database::getSystemService($params['serviceid']); - - if ($service) { - - $whmcsService = Database::getWhmcsService($params['serviceid']); - if (!$whmcsService) return 'WHMCS service record not found.'; - - $cp = $this->getCP($whmcsService->server); - if (!$cp) return 'No control server found.'; - - $request = $this->initCurl($cp['token']); - $data = $request->get($cp['url'] . '/servers/' . (int) $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'; - default: - return 'Request failed. VirtFusion API returned HTTP ' . $request->getRequestInfo('http_code'); - } - } - return 'Service not found in module database.'; - } - - - public function unsuspendAccount($params) - { - $service = Database::getSystemService($params['serviceid']); - - if ($service) { - $whmcsService = Database::getWhmcsService($params['serviceid']); - if (!$whmcsService) return 'WHMCS service record not found.'; - - $cp = $this->getCP($whmcsService->server); - if (!$cp) return 'No control server found.'; - - $request = $this->initCurl($cp['token']); - $data = $request->post($cp['url'] . '/servers/' . (int) $service->server_id . '/unsuspend'); - $data = json_decode($data); - - Log::insert(__FUNCTION__, $request->getRequestInfo(), $data); - - switch ($request->getRequestInfo('http_code')) { - - case 204: - return 'success'; - - case 404: - if (isset($data->msg)) { - if ($data->msg == 'server not found') { - Database::deleteSystemService($params['serviceid']); - return 'success'; - } else { - return 'VirtFusion returned 404: ' . $data->msg; + case 423: + if (isset($data->msg)) { + return $data->msg; } - } else { - return 'VirtFusion returned 404 without details. The API may be unavailable.'; - } - case 423: - if (isset($data->msg)) { - return $data->msg; - } - return 'The server is currently locked. Please try again later.'; - default: - return 'Unsuspend request failed. VirtFusion API returned HTTP ' . $request->getRequestInfo('http_code'); + return 'The server is currently locked. Please try again later.'; + + default: + return 'Suspend request failed. VirtFusion API returned HTTP ' . $request->getRequestInfo('http_code'); + } } - } - return 'Service not found in module database.'; - } - public function adminServicesTabFields($params) - { - $serverId = ''; - $serverObject = ''; + return 'Service not found in module database.'; + } catch (\Exception $e) { + Log::insert(__FUNCTION__, $params, $e->getMessage()); - $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 (!isset($_POST['modulefields'][0]) || $_POST['modulefields'][0] === '') { - Database::deleteSystemService($params['serviceid']); - } else { - $serverId = (int) $_POST['modulefields'][0]; - if ($serverId > 0) { - Database::updateSystemServiceServerId($params['serviceid'], $serverId); - } + return $e->getMessage(); } } /** - * Validate server creation parameters via dry run. + * Refresh the cached server object by fetching fresh data from the VirtFusion API. * - * @param array $params WHMCS service params - * @return string 'success' or error message + * Updates both the module database record and the WHMCS service fields (IP, username, etc.). + * + * @param array $params WHMCS service parameters + * @return string 'success' or an error message + */ + public function updateServerObject($params) + { + try { + $service = Database::getSystemService($params['serviceid']); + + if ($service) { + + $whmcsService = Database::getWhmcsService($params['serviceid']); + if (! $whmcsService) { + return 'WHMCS service record not found.'; + } + + $cp = $this->getCP($whmcsService->server); + if (! $cp) { + return 'No control server found.'; + } + + $request = $this->initCurl($cp['token']); + $data = $request->get($cp['url'] . '/servers/' . (int) $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'; + default: + return 'Request failed. VirtFusion API returned HTTP ' . $request->getRequestInfo('http_code'); + } + } + + return 'Service not found in module database.'; + } catch (\Exception $e) { + Log::insert(__FUNCTION__, $params, $e->getMessage()); + + return $e->getMessage(); + } + } + + /** + * Unsuspend a VirtFusion server, queuing the action if another operation is in progress. + * + * Returns 'success' whether the server is unsuspended immediately or queued for unsuspension. + * + * @param array $params WHMCS service parameters + * @return string 'success' or an error message + */ + public function unsuspendAccount($params) + { + try { + $service = Database::getSystemService($params['serviceid']); + + if ($service) { + $whmcsService = Database::getWhmcsService($params['serviceid']); + if (! $whmcsService) { + return 'WHMCS service record not found.'; + } + + $cp = $this->getCP($whmcsService->server); + if (! $cp) { + return 'No control server found.'; + } + + $request = $this->initCurl($cp['token']); + $data = $request->post($cp['url'] . '/servers/' . (int) $service->server_id . '/unsuspend'); + $data = json_decode($data); + + Log::insert(__FUNCTION__, $request->getRequestInfo(), $data); + + switch ($request->getRequestInfo('http_code')) { + + case 204: + return 'success'; + + case 404: + if (isset($data->msg)) { + if ($data->msg == 'server not found') { + Database::deleteSystemService($params['serviceid']); + + return 'success'; + } else { + return 'VirtFusion returned 404: ' . $data->msg; + } + } else { + return 'VirtFusion returned 404 without details. The API may be unavailable.'; + } + case 423: + if (isset($data->msg)) { + return $data->msg; + } + + return 'The server is currently locked. Please try again later.'; + + default: + return 'Unsuspend request failed. VirtFusion API returned HTTP ' . $request->getRequestInfo('http_code'); + } + } + + return 'Service not found in module database.'; + } catch (\Exception $e) { + Log::insert(__FUNCTION__, $params, $e->getMessage()); + + return $e->getMessage(); + } + } + + /** + * Generate the admin Services tab custom fields for a VirtFusion service. + * + * Returns fields for Server ID (editable), Server Info, Server Object (JSON viewer), + * and Options (action buttons), omitting Options for terminated services. + * + * @param array $params WHMCS service parameters + * @return array Associative array of field label => HTML content + */ + public function adminServicesTabFields($params) + { + try { + $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; + } catch (\Exception $e) { + Log::insert(__FUNCTION__, $params, $e->getMessage()); + + return []; + } + } + + /** + * Save the admin Services tab custom fields for a VirtFusion service. + * + * Deletes the module database record if the Server ID field is cleared, + * or updates it with the new integer server ID if a value is provided. + * + * @param array $params WHMCS service parameters + * @return void + */ + public function adminServicesTabFieldsSave($params) + { + try { + if (! isset($_POST['modulefields'][0]) || $_POST['modulefields'][0] === '') { + Database::deleteSystemService($params['serviceid']); + } else { + $serverId = (int) $_POST['modulefields'][0]; + if ($serverId > 0) { + Database::updateSystemServiceServerId($params['serviceid'], $serverId); + } + } + } catch (\Exception $e) { + Log::insert(__FUNCTION__, $params, $e->getMessage()); + } + } + + /** + * Perform a dry-run server creation to validate the current product configuration. + * + * Used by the WHMCS "Test Connection" button to confirm that the package, hypervisor, + * and IP settings are accepted by the VirtFusion API without creating a server. + * + * @param array $params WHMCS service parameters + * @return string 'success' or an error message */ public function validateServerConfig($params) { try { $server = $params['serverid'] ?: false; - $cp = $this->getCP($server, !$server); + $cp = $this->getCP($server, ! $server); - if (!$cp) { + if (! $cp) { return 'No Control server found.'; } $options = [ - "packageId" => (int) $params['configoption2'], - "hypervisorId" => (int) $params['configoption1'], - "ipv4" => (int) $params['configoption3'], + 'packageId' => (int) $params['configoption2'], + 'hypervisorId' => (int) $params['configoption1'], + 'ipv4' => (int) $params['configoption3'], ]; // We need a userId for dry run - use the service owner @@ -517,6 +634,16 @@ class ModuleFunctions extends Module } } + /** + * Render the client area overview tab for a VirtFusion service. + * + * Returns the template name and variables (system URL, service status, hostname, + * self-service mode) needed by the Smarty overview template. Falls back to an + * error template on any exception. + * + * @param array $params WHMCS service parameters + * @return array Template name and variables for WHMCS to render + */ public function clientArea($params) { $serverHostname = null; diff --git a/modules/servers/VirtFusionDirect/lib/ServerResource.php b/modules/servers/VirtFusionDirect/lib/ServerResource.php index 3c00100..c74feeb 100644 --- a/modules/servers/VirtFusionDirect/lib/ServerResource.php +++ b/modules/servers/VirtFusionDirect/lib/ServerResource.php @@ -2,8 +2,17 @@ namespace WHMCS\Module\Server\VirtFusionDirect; +/** + * Transforms a VirtFusion API server response into a flat key-value array for Smarty templates and admin display. + */ class ServerResource { + /** + * Normalise a VirtFusion API server response into a flat associative array. + * + * @param object $data VirtFusion API server response object (with a `data` property) + * @return array Flat associative array containing server name, hostname, resources, network info, and usage + */ public function process($data) { $server = json_decode(json_encode($data->data), true); diff --git a/modules/servers/VirtFusionDirect/modify.sql b/modules/servers/VirtFusionDirect/modify.sql deleted file mode 100644 index 1383c90..0000000 --- a/modules/servers/VirtFusionDirect/modify.sql +++ /dev/null @@ -1,49 +0,0 @@ --- 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); diff --git a/pint.json b/pint.json new file mode 100644 index 0000000..42e584e --- /dev/null +++ b/pint.json @@ -0,0 +1,24 @@ +{ + "preset": "laravel", + "rules": { + "declare_strict_types": false, + "blank_line_before_statement": { + "statements": ["return", "throw", "try"] + }, + "concat_space": { + "spacing": "one" + }, + "ordered_imports": { + "sort_algorithm": "alpha" + }, + "single_quote": true, + "no_unused_imports": true, + "trailing_comma_in_multiline": { + "elements": ["arrays", "arguments"] + } + }, + "exclude": [ + "vendor", + "templates" + ] +}