Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d253bd44e6 | ||
|
|
1ab2ef42a5 | ||
|
|
3ca9eb60c3 | ||
|
|
504d2926a4 | ||
|
|
64dcce3d0e | ||
|
|
6694a5e44d | ||
|
|
6528c8a53a | ||
|
|
d3d75b4752 | ||
|
|
3d3df6e2dc | ||
|
|
0ade74dd4e | ||
|
|
a9565ff6f9 | ||
|
|
9cd737c5d5 | ||
|
|
90a97c4afb |
54
.github/workflows/publish-release.yml
vendored
54
.github/workflows/publish-release.yml
vendored
@@ -1,41 +1,43 @@
|
|||||||
# .github/workflows/semantic-versioning-release.yml
|
name: Publish Release
|
||||||
name: Automated Semantic Versioning Release
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
tags:
|
||||||
- main
|
- 'v*'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: write # for creating tags and releases
|
contents: write
|
||||||
issues: write # for commenting on issues
|
|
||||||
pull-requests: write # for commenting on PRs
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
# This is required to analyze the full commit history
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Automated Semantic Release
|
- name: Extract tag name
|
||||||
# This action wraps the popular semantic-release tool
|
id: tag
|
||||||
uses: cycjimmy/semantic-release-action@v4
|
run: echo "version=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Generate release notes
|
||||||
|
id: notes
|
||||||
|
run: |
|
||||||
|
# Get previous tag
|
||||||
|
PREV_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")
|
||||||
|
if [ -n "$PREV_TAG" ]; then
|
||||||
|
NOTES=$(git log --pretty=format:"- %s" "$PREV_TAG"..HEAD)
|
||||||
|
else
|
||||||
|
NOTES=$(git log --pretty=format:"- %s")
|
||||||
|
fi
|
||||||
|
# Write to file for the release body
|
||||||
|
echo "$NOTES" > /tmp/release-notes.txt
|
||||||
|
|
||||||
|
- name: Create release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
# You can specify the branches to release from
|
tag_name: ${{ steps.tag.outputs.version }}
|
||||||
branch: main
|
name: ${{ steps.tag.outputs.version }}
|
||||||
extra_plugins: |
|
body_path: /tmp/release-notes.txt
|
||||||
@semantic-release/changelog
|
draft: false
|
||||||
@semantic-release/git
|
prerelease: false
|
||||||
env:
|
env:
|
||||||
# GITHUB_TOKEN is required for authentication
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
# To make this work, you must follow the Conventional Commits specification.
|
|
||||||
# Examples:
|
|
||||||
# - fix: correct a typo in the documentation
|
|
||||||
# - feat: add a new user authentication endpoint
|
|
||||||
# - feat(api): add rate limiting
|
|
||||||
# BREAKING CHANGE: The API now returns 429 when rate limit is exceeded.
|
|
||||||
|
|||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1 +1,3 @@
|
|||||||
/.idea/
|
/.idea/
|
||||||
|
/.superpowers/
|
||||||
|
/vendor/
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"branches": ["main"],
|
|
||||||
"plugins": [
|
|
||||||
"@semantic-release/commit-analyzer",
|
|
||||||
"@semantic-release/release-notes-generator",
|
|
||||||
["@semantic-release/changelog", { "changelogFile": "CHANGELOG.md" }],
|
|
||||||
"@semantic-release/github",
|
|
||||||
["@semantic-release/git", {
|
|
||||||
"assets": ["CHANGELOG.md"],
|
|
||||||
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
|
|
||||||
}]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
50
CHANGELOG.md
50
CHANGELOG.md
@@ -1,23 +1,39 @@
|
|||||||
# 1.0.0 (2026-02-07)
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* add null/false guards, proper error handling, and VNC popup fix ([49fdd9e](https://github.com/EZSCALE/virtfusion-whmcs-module/commit/49fdd9e49ba87bfb4b72dd741e15f790c1050033))
|
|
||||||
* TestConnection for unsaved servers, traffic display, and cache-busting ([e8d2eb0](https://github.com/EZSCALE/virtfusion-whmcs-module/commit/e8d2eb0aa1f173f13bb0b8d7dfca0acebb821ac7))
|
|
||||||
* XSS escaping, null guards, JS bug fixes, and documentation updates ([6c7cdc6](https://github.com/EZSCALE/virtfusion-whmcs-module/commit/6c7cdc6421678390746adcee4877a7ade8f2a061))
|
|
||||||
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* add client-side SSH Ed25519 key generator on order page ([209e01d](https://github.com/EZSCALE/virtfusion-whmcs-module/commit/209e01deb6832dce76a307410fbab28b1e420093))
|
|
||||||
* add VNC check, SSH key paste, resources panel, sliders, and self-service billing ([1e471af](https://github.com/EZSCALE/virtfusion-whmcs-module/commit/1e471affd0ae9a68358afa5704523bce9bb413d0))
|
|
||||||
* streamline network panel, conditional self-service, remove IP add endpoints ([e73e85c](https://github.com/EZSCALE/virtfusion-whmcs-module/commit/e73e85c5a9faa79b50e4949328c1d2a3cbc49ddf))
|
|
||||||
|
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
All notable changes to the VirtFusion Direct Provisioning Module for WHMCS.
|
All notable changes to the VirtFusion Direct Provisioning Module for WHMCS.
|
||||||
|
|
||||||
|
## [1.0.0] - 2026-03-19
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- OS template tile gallery with accordion categories, brand icons, and search
|
||||||
|
- Inline server rename with friendly name generator
|
||||||
|
- Traffic statistics canvas chart in resources panel
|
||||||
|
- Backup listing timeline in manage panel
|
||||||
|
- VNC enable/disable toggle with connection details and password copy
|
||||||
|
- Server root password reset with auto-clipboard copy
|
||||||
|
- Redis-backed API response caching with filesystem fallback
|
||||||
|
- Skeleton loading, action cooldowns, progress indicators
|
||||||
|
- Copy-to-clipboard buttons for IP addresses
|
||||||
|
- Client-side SSH Ed25519 key generator on checkout page
|
||||||
|
- VNC console support, resources panel, self-service billing
|
||||||
|
- Configurable option sliders on checkout page
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
- XSS escaping, null guards, and proper error handling
|
||||||
|
- All state-mutating operations use POST instead of GET
|
||||||
|
- Explicit break after all output() calls in client.php
|
||||||
|
- Server-side regex validation on rename endpoint
|
||||||
|
- Error messages sanitized (no raw API errors exposed to clients)
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- Client IP removal capability (IPs managed by VirtFusion)
|
||||||
|
- IP add buttons (managed by VirtFusion during provisioning)
|
||||||
|
- Firewall panel (non-functional; managed in VirtFusion admin)
|
||||||
|
|
||||||
|
### Infrastructure
|
||||||
|
- Tag-based release workflow (compatible with Gitea and GitHub)
|
||||||
|
- Codebase consolidation: resolveServiceContext(), groupOsTemplates(), vfUrl(), vfShowAlert()
|
||||||
|
|
||||||
## [0.0.18] - 2025-10-01
|
## [0.0.18] - 2025-10-01
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
@@ -74,6 +90,6 @@ All notable changes to the VirtFusion Direct Provisioning Module for WHMCS.
|
|||||||
- Admin services tab with server ID management
|
- Admin services tab with server ID management
|
||||||
- Package change (upgrade/downgrade) support
|
- Package change (upgrade/downgrade) support
|
||||||
- Configurable option mapping for dynamic resource allocation
|
- Configurable option mapping for dynamic resource allocation
|
||||||
- GitHub Actions CI/CD with semantic-release
|
- GitHub Actions CI/CD
|
||||||
- Security policy (SECURITY.md)
|
- Security policy (SECURITY.md)
|
||||||
- License (GPL v3)
|
- License (GPL v3)
|
||||||
|
|||||||
51
CLAUDE.md
51
CLAUDE.md
@@ -15,12 +15,27 @@ There is no automated test suite, linter, or build step. Testing is manual:
|
|||||||
- **Module logging:** WHMCS Admin → Utilities → Logs → Module Log captures all API calls and responses
|
- **Module logging:** WHMCS Admin → Utilities → Logs → Module Log captures all API calls and responses
|
||||||
- **Server object viewer:** Admin services tab shows full JSON response from VirtFusion API
|
- **Server object viewer:** Admin services tab shows full JSON response from VirtFusion API
|
||||||
|
|
||||||
|
## Development Rules
|
||||||
|
|
||||||
|
- **Error handling:** Always use try...catch blocks around API calls, database operations, and any code that may throw exceptions. Never let exceptions bubble up unhandled to the user. Log caught exceptions via `Log::insert()`.
|
||||||
|
- **Ownership validation:** Every client-facing action MUST verify service ownership via `validateUserOwnsService()` before performing any operation. Server IDs must be cross-referenced against the authenticated client to prevent cross-customer data access.
|
||||||
|
- **Security:** All input must be validated server-side. Never trust client-side validation alone. Cast IDs to `(int)`, validate strings with regex, escape output with `htmlspecialchars()`.
|
||||||
|
- **Control flow:** Every `$vf->output()` call in switch cases must be followed by `break`. Do not rely on `exit()` inside `output()` for flow control.
|
||||||
|
- **HTTP methods:** Read-only operations use GET. State-mutating operations (power, rebuild, rename, password reset, credit, VNC toggle) use POST with data in the request body.
|
||||||
|
- **Caching:** Use the `Cache` class for slow-changing API responses. Never cache real-time data (server status, VNC sessions, login tokens) or mutation responses.
|
||||||
|
|
||||||
## Release Process
|
## Release Process
|
||||||
|
|
||||||
Releases are automated via GitHub Actions using semantic-release on pushes to `main`. Use **conventional commits**:
|
Releases are triggered by pushing a git tag:
|
||||||
- `fix:` → patch release
|
```bash
|
||||||
- `feat:` → minor release
|
git tag v1.1.0
|
||||||
- `BREAKING CHANGE:` in commit body → major release
|
git push origin v1.1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
The `publish-release.yml` workflow creates a GitHub/Gitea release with auto-generated notes from the commit log. Use **conventional commits** for clear changelogs:
|
||||||
|
- `fix:` → patch-level change
|
||||||
|
- `feat:` → feature addition
|
||||||
|
- `refactor:` → code improvement without behavior change
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
@@ -31,38 +46,39 @@ Releases are automated via GitHub Actions using semantic-release on pushes to `m
|
|||||||
| File | Purpose |
|
| File | Purpose |
|
||||||
|------|---------|
|
|------|---------|
|
||||||
| `VirtFusionDirect.php` | WHMCS module interface — non-namespaced functions (`VirtFusionDirect_CreateAccount()`, etc.) that delegate to library classes |
|
| `VirtFusionDirect.php` | WHMCS module interface — non-namespaced functions (`VirtFusionDirect_CreateAccount()`, etc.) that delegate to library classes |
|
||||||
| `client.php` | Client-facing AJAX API — authenticated by WHMCS session + service ownership validation |
|
| `client.php` | Client-facing AJAX API — authenticated by WHMCS session + service ownership validation. POST for mutations, GET for reads. |
|
||||||
| `admin.php` | Admin-facing AJAX API — requires WHMCS admin authentication |
|
| `admin.php` | Admin-facing AJAX API — requires WHMCS admin authentication |
|
||||||
| `hooks.php` | WHMCS hooks — checkout validation (OS selection), dynamic dropdown/slider injection, SSH key paste |
|
| `hooks.php` | WHMCS hooks — checkout validation (OS selection), OS gallery + SSH key UI injection, slider UI for configurable options |
|
||||||
|
|
||||||
### Core Classes (in `lib/`)
|
### Core Classes (in `lib/`)
|
||||||
|
|
||||||
| Class | Role |
|
| Class | Role |
|
||||||
|-------|------|
|
|-------|------|
|
||||||
| `Module` | Base class with API integration, auth checks, power/network/VNC/backup/resource/self-service methods. All client/admin actions route through here. |
|
| `Module` | Base class with API integration, auth checks, and all feature methods (power, network, VNC, backup, resource, self-service, traffic, rename, password reset). Contains `resolveServiceContext()` for DRY service lookups and `groupOsTemplates()` for shared OS category logic. |
|
||||||
| `ModuleFunctions` | Extends `Module`. Service lifecycle: create, suspend, unsuspend, terminate, change package, usage updates, client area rendering. |
|
| `ModuleFunctions` | Extends `Module`. Service lifecycle: create, suspend, unsuspend, terminate, change package, usage updates, client area rendering. |
|
||||||
| `ConfigureService` | Extends `Module`. Order-time operations: package discovery, OS template fetching, server build initialization, SSH key retrieval and creation. |
|
| `ConfigureService` | Extends `Module`. Order-time operations: package discovery, OS template fetching, server build initialization, SSH key retrieval and creation. |
|
||||||
| `Database` | Static methods for `mod_virtfusion_direct` table operations and WHMCS DB queries. Auto-creates/migrates schema on first use. |
|
| `Database` | Static methods for `mod_virtfusion_direct` table operations and WHMCS DB queries. Auto-creates/migrates schema on first use. |
|
||||||
| `Curl` | HTTP client wrapper with Bearer token auth, SSL verification, 30s timeout. Methods: `get`, `post`, `put`, `patch`, `delete`. |
|
| `Curl` | HTTP client wrapper with Bearer token auth, SSL verification, 30s timeout. Methods: `get`, `post`, `put`, `patch`, `delete`. Single-use — each instance makes one request. |
|
||||||
|
| `Cache` | Two-tier caching: Redis (if `ext-redis` available) with atomic filesystem fallback. TTLs: OS templates 10min, traffic/backups 2min, packages 10min. |
|
||||||
| `ServerResource` | Transforms VirtFusion API response into flat key-value format for Smarty templates. |
|
| `ServerResource` | Transforms VirtFusion API response into flat key-value format for Smarty templates. |
|
||||||
| `AdminHTML` | Static methods generating admin services tab HTML (server ID editor, JSON viewer, action buttons). |
|
| `AdminHTML` | Static methods generating admin services tab HTML (server ID editor, JSON viewer, action buttons). |
|
||||||
| `Log` | Thin wrapper around WHMCS module logging. |
|
| `Log` | Thin wrapper around WHMCS module logging. |
|
||||||
|
|
||||||
### Class Hierarchy
|
### Class Hierarchy
|
||||||
|
|
||||||
`ModuleFunctions` and `ConfigureService` both extend `Module`. Most business logic lives in `Module` — it handles API calls, auth, validation, and all feature-specific operations (power, network, VNC, backup, resource modification). `ModuleFunctions` orchestrates the WHMCS service lifecycle (provisioning flow, suspension, termination).
|
`ModuleFunctions` and `ConfigureService` both extend `Module`. Most business logic lives in `Module` — it handles API calls, auth, validation, and all feature-specific operations. The `resolveServiceContext()` method provides a standardized way to look up service → WHMCS service → control panel → curl client in a single call, eliminating boilerplate across all API methods.
|
||||||
|
|
||||||
### Client-Side
|
### Client-Side
|
||||||
|
|
||||||
- **`templates/overview.tpl`** — Smarty template for client area (server info, power, network, rebuild, resources, VNC, self-service billing, billing overview)
|
- **`templates/overview.tpl`** — Smarty template for client area (server info, power, network, rebuild with OS gallery, resources with traffic chart, VNC toggle, self-service billing, billing overview, backups timeline, server rename, password reset)
|
||||||
- **`templates/js/module.js`** — Vanilla JS (1000+ lines) handling AJAX calls to `client.php`, DOM updates, status badges, power actions, all management UIs
|
- **`templates/js/module.js`** — Vanilla JS + jQuery handling AJAX calls, DOM updates, status badges, power actions, all management UIs. Key helpers: `vfUrl()` (URL builder), `vfShowAlert()` (alert display), `vfRenderOsGallery()` (accordion gallery), `vfDrawTrafficChart()` (canvas chart)
|
||||||
- **`templates/js/keygen.js`** — Client-side SSH Ed25519 key generator using Web Crypto API (loaded on checkout page)
|
- **`templates/js/keygen.js`** — Client-side SSH Ed25519 key generator using Web Crypto API (loaded on checkout page)
|
||||||
- **`templates/css/module.css`** — Cross-theme styles with Bootstrap 3/4/5 dual class support (`panel card`, `panel-body card-body`)
|
- **`templates/css/module.css`** — Cross-theme styles with Bootstrap 3/4/5 dual class support (`panel card`, `panel-body card-body`)
|
||||||
|
|
||||||
### Removed Features
|
### Removed Features
|
||||||
|
|
||||||
- **Firewall** — Removed (non-functional; rulesets must be created in VirtFusion admin panel)
|
- **Firewall** — Removed (non-functional; rulesets must be created in VirtFusion admin panel)
|
||||||
- **IP add buttons** — Removed (`addIPv4`, `addIPv6` endpoints and UI); IPs are managed by VirtFusion during provisioning
|
- **IP add/remove buttons** — Removed; IPs are managed by VirtFusion during provisioning
|
||||||
- **Upgrade/Downgrade link** — Removed from resources panel
|
- **Upgrade/Downgrade link** — Removed from resources panel
|
||||||
|
|
||||||
### Data Flow: Server Creation
|
### Data Flow: Server Creation
|
||||||
@@ -75,18 +91,21 @@ Releases are automated via GitHub Actions using semantic-release on pushes to `m
|
|||||||
6. Updates WHMCS hosting record (IP, username, password, domain)
|
6. Updates WHMCS hosting record (IP, username, password, domain)
|
||||||
7. Calls `ConfigureService::initServerBuild()` with selected OS + SSH key
|
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
|
### 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`.
|
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`.
|
||||||
|
|
||||||
## Security Patterns
|
## Security Patterns
|
||||||
|
|
||||||
- All PHP files start with `if (!defined("WHMCS")) die()` to prevent direct access
|
- All PHP files start with `if (!defined("WHMCS")) die()` to prevent direct access (except entry points using `init.php`)
|
||||||
- Client endpoints validate WHMCS session AND service ownership before any operation
|
- Client endpoints validate WHMCS session AND service ownership before any operation
|
||||||
- API tokens stored encrypted in WHMCS server password field (decrypted via `localAPI('DecryptPassword')`)
|
- API tokens stored encrypted in WHMCS server password field (decrypted via `localAPI('DecryptPassword')`)
|
||||||
- Input validation: type casting, regex filtering, `filter_var()` for IP addresses
|
- Input validation: type casting (`(int)`), regex filtering, `filter_var()` for IP addresses
|
||||||
- Output escaping: `htmlspecialchars()` in Smarty, `encodeURIComponent()` / `.text()` in JS
|
- Output escaping: `htmlspecialchars()` in PHP, `$('<span>').text()` in jQuery, `{escape:'htmlall'}` in Smarty
|
||||||
- SSL verification enabled on all API calls (`CURLOPT_SSL_VERIFYPEER` + `CURLOPT_SSL_VERIFYHOST = 2`)
|
- SSL verification enabled on all API calls (`CURLOPT_SSL_VERIFYPEER` + `CURLOPT_SSL_VERIFYHOST = 2`)
|
||||||
|
- Server rename validated both client-side and server-side with RFC 1123 regex
|
||||||
|
|
||||||
## VirtFusion API Compatibility
|
## VirtFusion API Compatibility
|
||||||
|
|
||||||
@@ -95,6 +114,7 @@ Custom option names can be mapped in `config/ConfigOptionMapping.php` (copy from
|
|||||||
- **VNC console:** v6.1.0+
|
- **VNC console:** v6.1.0+
|
||||||
- **Resource modification:** v6.2.0+
|
- **Resource modification:** v6.2.0+
|
||||||
- **Self-service billing:** Requires self-service feature enabled in VirtFusion
|
- **Self-service billing:** Requires self-service feature enabled in VirtFusion
|
||||||
|
- **OS icon path:** `{baseUrl}/img/logo/{icon_filename}` (public, no auth required)
|
||||||
|
|
||||||
## Product Config Options
|
## Product Config Options
|
||||||
|
|
||||||
@@ -111,4 +131,5 @@ Custom option names can be mapped in `config/ConfigOptionMapping.php` (copy from
|
|||||||
|
|
||||||
- WHMCS 8.x+ (tested 8.0–8.10)
|
- WHMCS 8.x+ (tested 8.0–8.10)
|
||||||
- PHP 8.0+ with cURL extension
|
- PHP 8.0+ with cURL extension
|
||||||
|
- Redis extension optional (improves caching performance, falls back to filesystem)
|
||||||
- All WHMCS themes supported (Six, Twenty-One, Lagom, custom) via Bootstrap 3/4/5 dual classes
|
- All WHMCS themes supported (Six, Twenty-One, Lagom, custom) via Bootstrap 3/4/5 dual classes
|
||||||
|
|||||||
123
README.md
123
README.md
@@ -62,7 +62,7 @@ You also need a VirtFusion API token with the following permissions:
|
|||||||
- **Control Panel SSO** - One-click login to VirtFusion panel
|
- **Control Panel SSO** - One-click login to VirtFusion panel
|
||||||
- **Server Rebuild** - Reinstall with any available OS template
|
- **Server Rebuild** - Reinstall with any available OS template
|
||||||
- **Password Reset** - Reset VirtFusion panel login credentials
|
- **Password Reset** - Reset VirtFusion panel login credentials
|
||||||
- **Network Management** - View and remove IPv4 addresses; view IPv6 subnets
|
- **Network Management** - View IPv4 addresses and IPv6 subnets with copy-to-clipboard
|
||||||
- **Resources Panel** - Current memory, CPU, storage, traffic allocation with usage bars
|
- **Resources Panel** - Current memory, CPU, storage, traffic allocation with usage bars
|
||||||
- **VNC Console** - Browser-based console access (panel auto-hides when VNC is disabled on the server)
|
- **VNC Console** - Browser-based console access (panel auto-hides when VNC is disabled on the server)
|
||||||
- **Self-Service Billing** - Credit balance display, usage breakdown, and credit top-up (when enabled)
|
- **Self-Service Billing** - Credit balance display, usage breakdown, and credit top-up (when enabled)
|
||||||
@@ -79,7 +79,7 @@ You also need a VirtFusion API token with the following permissions:
|
|||||||
- **Update Server Object** - Refresh cached server data from VirtFusion
|
- **Update Server Object** - Refresh cached server data from VirtFusion
|
||||||
|
|
||||||
### Ordering Process
|
### Ordering Process
|
||||||
- Dynamic OS template dropdown populated from VirtFusion API
|
- OS template tile gallery with accordion categories, search, and brand icons
|
||||||
- SSH key selection dropdown for users with saved keys, with option to paste a new public key
|
- SSH key selection dropdown for users with saved keys, with option to paste a new public key
|
||||||
- **SSH Ed25519 key generator** — Client-side keypair generation using Web Crypto API
|
- **SSH Ed25519 key generator** — Client-side keypair generation using Web Crypto API
|
||||||
- Checkout validation ensuring OS selection before order placement
|
- Checkout validation ensuring OS selection before order placement
|
||||||
@@ -108,95 +108,28 @@ You also need a VirtFusion API token with the following permissions:
|
|||||||
|
|
||||||
## Installation
|
## 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
|
```bash
|
||||||
cd /tmp
|
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
|
||||||
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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
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:
|
||||||
|
|
||||||
```
|
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.
|
||||||
modules/servers/VirtFusionDirect/
|
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.
|
||||||
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
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 2: Set Up Server in WHMCS
|
That's it. Hooks activate automatically and custom fields are created on module load.
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
## Upgrading
|
## 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
|
```bash
|
||||||
cd /tmp
|
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
|
||||||
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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Restore your custom `config/ConfigOptionMapping.php` if applicable
|
> **Note:** If you have a custom `config/ConfigOptionMapping.php`, back it up first — `--delete` will remove it. Restore it after upgrading.
|
||||||
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**
|
|
||||||
|
|
||||||
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
|
## Configuration
|
||||||
|
|
||||||
@@ -222,20 +155,9 @@ Each WHMCS product using this module needs:
|
|||||||
|
|
||||||
### Custom Fields
|
### 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 |
|
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".
|
||||||
|---|---|---|---|---|
|
|
||||||
| 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
|
|
||||||
```
|
|
||||||
|
|
||||||
### Module Configuration Options
|
### Module Configuration Options
|
||||||
|
|
||||||
@@ -305,7 +227,7 @@ Four power control buttons:
|
|||||||
|
|
||||||
### Network Management
|
### Network Management
|
||||||
- View all IPv4 addresses and IPv6 subnets assigned to the server
|
- View all IPv4 addresses and IPv6 subnets assigned to the server
|
||||||
- Remove secondary IPv4 addresses (primary cannot be removed)
|
- Copy IP addresses to clipboard with one click
|
||||||
|
|
||||||
### VNC Console
|
### VNC Console
|
||||||
- Opens a browser-based VNC console to the server
|
- Opens a browser-based VNC console to the server
|
||||||
@@ -407,12 +329,6 @@ WHMCS automatically loads theme-specific templates when they exist. Copy the ori
|
|||||||
| `GET` | `/media/templates/fromServerPackageSpec/{id}` | OS templates |
|
| `GET` | `/media/templates/fromServerPackageSpec/{id}` | OS templates |
|
||||||
| `GET` | `/ssh_keys/user/{id}` | SSH key listing |
|
| `GET` | `/ssh_keys/user/{id}` | SSH key listing |
|
||||||
|
|
||||||
### Network
|
|
||||||
|
|
||||||
| Method | Endpoint | Purpose |
|
|
||||||
|---|---|---|
|
|
||||||
| `DELETE` | `/servers/{id}/ipv4` | Remove IPv4 address |
|
|
||||||
|
|
||||||
### SSH Keys
|
### SSH Keys
|
||||||
|
|
||||||
| Method | Endpoint | Purpose |
|
| Method | Endpoint | Purpose |
|
||||||
@@ -426,7 +342,10 @@ WHMCS automatically loads theme-specific templates when they exist. Copy the ori
|
|||||||
| `GET` | `/selfService/usage/byUserExtRelationId/{id}` | Usage data by WHMCS client ID |
|
| `GET` | `/selfService/usage/byUserExtRelationId/{id}` | Usage data by WHMCS client ID |
|
||||||
| `GET` | `/selfService/report/byUserExtRelationId/{id}` | Billing report by WHMCS client ID |
|
| `GET` | `/selfService/report/byUserExtRelationId/{id}` | Billing report by WHMCS client ID |
|
||||||
| `POST` | `/selfService/credit/byUserExtRelationId/{id}` | Add credit by WHMCS client ID |
|
| `POST` | `/selfService/credit/byUserExtRelationId/{id}` | Add credit by WHMCS client ID |
|
||||||
| `GET` | `/selfService/currencies` | Available self-service currencies |
|
| `GET` | `/servers/{id}/traffic` | Traffic statistics |
|
||||||
|
| `GET` | `/backups/server/{id}` | Backup listing |
|
||||||
|
| `POST` | `/servers/{id}/vnc` | Toggle VNC on/off |
|
||||||
|
| `POST` | `/servers/{id}/resetPassword` | Reset server root password |
|
||||||
|
|
||||||
### Advanced
|
### Advanced
|
||||||
|
|
||||||
@@ -533,9 +452,7 @@ This data appears in the WHMCS client area and admin product details.
|
|||||||
|
|
||||||
7. **Concurrent API Calls** - The module makes individual API calls for each feature panel on the client area page. If the VirtFusion API is slow, the page may take longer to fully load. All panels load asynchronously to minimize perceived delay.
|
7. **Concurrent API Calls** - The module makes individual API calls for each feature panel on the client area page. If the VirtFusion API is slow, the page may take longer to fully load. All panels load asynchronously to minimize perceived delay.
|
||||||
|
|
||||||
8. **Primary IPv4 Protection** - The first IPv4 address cannot be removed through the client area interface. This is by design to prevent users from accidentally removing their primary IP address.
|
8. **Self-Signed SSL Certificates** - SSL verification is enforced by default. VirtFusion panels using self-signed certificates will cause connection failures. Use a valid SSL certificate (e.g., Let's Encrypt) on your VirtFusion panel.
|
||||||
|
|
||||||
9. **Self-Signed SSL Certificates** - SSL verification is enforced by default. VirtFusion panels using self-signed certificates will cause connection failures. Use a valid SSL certificate (e.g., Let's Encrypt) on your VirtFusion panel.
|
|
||||||
|
|
||||||
## Security
|
## Security
|
||||||
|
|
||||||
@@ -564,12 +481,12 @@ modules/servers/VirtFusionDirect/
|
|||||||
client.php # Client-facing AJAX API (authenticated, ownership-validated)
|
client.php # Client-facing AJAX API (authenticated, ownership-validated)
|
||||||
admin.php # Admin-facing AJAX API (admin authentication required)
|
admin.php # Admin-facing AJAX API (admin authentication required)
|
||||||
hooks.php # WHMCS hooks (order form OS/SSH dropdowns, checkout validation)
|
hooks.php # WHMCS hooks (order form OS/SSH dropdowns, checkout validation)
|
||||||
modify.sql # SQL for creating custom fields
|
|
||||||
lib/
|
lib/
|
||||||
Module.php # Base class: API communication, power, network, VNC, rebuild
|
Module.php # Base class: API communication, power, network, VNC, rebuild
|
||||||
ModuleFunctions.php # Provisioning: create, suspend, unsuspend, terminate, change package
|
ModuleFunctions.php # Provisioning: create, suspend, unsuspend, terminate, change package
|
||||||
ConfigureService.php # Order configuration: OS templates, SSH keys, server build init
|
ConfigureService.php # Order configuration: OS templates, SSH keys, server build init
|
||||||
Database.php # Database operations: custom table, WHMCS table queries
|
Database.php # Database operations: custom table, WHMCS table queries
|
||||||
|
Cache.php # Two-tier cache: Redis with filesystem fallback
|
||||||
Curl.php # HTTP client: GET, POST, PUT, PATCH, DELETE with SSL verification
|
Curl.php # HTTP client: GET, POST, PUT, PATCH, DELETE with SSL verification
|
||||||
ServerResource.php # Data transformer: VirtFusion API response -> display format
|
ServerResource.php # Data transformer: VirtFusion API response -> display format
|
||||||
AdminHTML.php # Admin interface: HTML generation for admin services tab
|
AdminHTML.php # Admin interface: HTML generation for admin services tab
|
||||||
|
|||||||
19
composer.json
Normal file
19
composer.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
87
composer.lock
generated
Normal file
87
composer.lock
generated
Normal file
@@ -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"
|
||||||
|
}
|
||||||
26
hooks/pre-commit
Executable file
26
hooks/pre-commit
Executable file
@@ -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
|
||||||
49
modify.sql
49
modify.sql
@@ -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);
|
|
||||||
@@ -1,13 +1,20 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
if (!defined("WHMCS")) {
|
if (! defined('WHMCS')) {
|
||||||
die("This file cannot be accessed directly");
|
exit('This file cannot be accessed directly');
|
||||||
}
|
}
|
||||||
|
|
||||||
use WHMCS\Module\Server\VirtFusionDirect\ModuleFunctions;
|
use WHMCS\Database\Capsule;
|
||||||
use WHMCS\Module\Server\VirtFusionDirect\Module;
|
|
||||||
use WHMCS\Module\Server\VirtFusionDirect\Database;
|
use WHMCS\Module\Server\VirtFusionDirect\Database;
|
||||||
|
use WHMCS\Module\Server\VirtFusionDirect\Log;
|
||||||
|
use WHMCS\Module\Server\VirtFusionDirect\Module;
|
||||||
|
use WHMCS\Module\Server\VirtFusionDirect\ModuleFunctions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns module metadata consumed by WHMCS.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
function VirtFusionDirect_MetaData()
|
function VirtFusionDirect_MetaData()
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
@@ -19,50 +26,55 @@ function VirtFusionDirect_MetaData()
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns product configuration options displayed in the WHMCS product editor.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
function VirtFusionDirect_ConfigOptions()
|
function VirtFusionDirect_ConfigOptions()
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
"defaultHypervisorGroupId" => [
|
'defaultHypervisorGroupId' => [
|
||||||
"FriendlyName" => "Hypervisor Group ID",
|
'FriendlyName' => 'Hypervisor Group ID',
|
||||||
"Type" => "text",
|
'Type' => 'text',
|
||||||
"Size" => "20",
|
'Size' => '20',
|
||||||
"Description" => "The default hypervisor group ID for server placement.",
|
'Description' => 'The default hypervisor group ID for server placement.',
|
||||||
"Default" => "1",
|
'Default' => '1',
|
||||||
],
|
],
|
||||||
"packageID" => [
|
'packageID' => [
|
||||||
"FriendlyName" => "Package ID",
|
'FriendlyName' => 'Package ID',
|
||||||
"Type" => "text",
|
'Type' => 'text',
|
||||||
"Size" => "20",
|
'Size' => '20',
|
||||||
"Description" => "The VirtFusion package ID that defines server resources.",
|
'Description' => 'The VirtFusion package ID that defines server resources.',
|
||||||
"Default" => "1",
|
'Default' => '1',
|
||||||
],
|
],
|
||||||
"defaultIPv4" => [
|
'defaultIPv4' => [
|
||||||
"FriendlyName" => "Default IPv4",
|
'FriendlyName' => 'Default IPv4',
|
||||||
"Type" => "dropdown",
|
'Type' => 'dropdown',
|
||||||
"Options" => "0,1,2,3,4,5,6,7,8,9,10",
|
'Options' => '0,1,2,3,4,5,6,7,8,9,10',
|
||||||
"Description" => "The default number of IPv4 addresses to assign to each server.",
|
'Description' => 'The default number of IPv4 addresses to assign to each server.',
|
||||||
"Default" => "1",
|
'Default' => '1',
|
||||||
],
|
],
|
||||||
"selfServiceMode" => [
|
'selfServiceMode' => [
|
||||||
"FriendlyName" => "Self-Service Mode",
|
'FriendlyName' => 'Self-Service Mode',
|
||||||
"Type" => "dropdown",
|
'Type' => 'dropdown',
|
||||||
"Options" => "0|Disabled,1|Hourly,2|Resource Packs,3|Both",
|
'Options' => '0|Disabled,1|Hourly,2|Resource Packs,3|Both',
|
||||||
"Description" => "Enable VirtFusion self-service billing for users created by this product.",
|
'Description' => 'Enable VirtFusion self-service billing for users created by this product.',
|
||||||
"Default" => "0",
|
'Default' => '0',
|
||||||
],
|
],
|
||||||
"autoTopOffThreshold" => [
|
'autoTopOffThreshold' => [
|
||||||
"FriendlyName" => "Auto Top-Off Threshold",
|
'FriendlyName' => 'Auto Top-Off Threshold',
|
||||||
"Type" => "text",
|
'Type' => 'text',
|
||||||
"Size" => "10",
|
'Size' => '10',
|
||||||
"Description" => "Credit balance below which auto top-off triggers during cron. 0 = disabled.",
|
'Description' => 'Credit balance below which auto top-off triggers during cron. 0 = disabled.',
|
||||||
"Default" => "0",
|
'Default' => '0',
|
||||||
],
|
],
|
||||||
"autoTopOffAmount" => [
|
'autoTopOffAmount' => [
|
||||||
"FriendlyName" => "Auto Top-Off Amount",
|
'FriendlyName' => 'Auto Top-Off Amount',
|
||||||
"Type" => "text",
|
'Type' => 'text',
|
||||||
"Size" => "10",
|
'Size' => '10',
|
||||||
"Description" => "Credit amount to add when auto top-off triggers.",
|
'Description' => 'Credit amount to add when auto top-off triggers.',
|
||||||
"Default" => "100",
|
'Default' => '100',
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -78,7 +90,7 @@ function VirtFusionDirect_TestConnection(array $params)
|
|||||||
}
|
}
|
||||||
|
|
||||||
$url = 'https://' . $hostname . '/api/v1';
|
$url = 'https://' . $hostname . '/api/v1';
|
||||||
$module = new Module();
|
$module = new Module;
|
||||||
$request = $module->initCurl($password);
|
$request = $module->initCurl($password);
|
||||||
$data = $request->get($url . '/connect');
|
$data = $request->get($url . '/connect');
|
||||||
|
|
||||||
@@ -94,27 +106,33 @@ function VirtFusionDirect_TestConnection(array $params)
|
|||||||
|
|
||||||
if ($httpCode == 0) {
|
if ($httpCode == 0) {
|
||||||
$curlError = $request->getRequestInfo('curl_error');
|
$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' => '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.'];
|
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()];
|
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()
|
function VirtFusionDirect_AdminCustomButtonArray()
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
"Update Server Object" => "updateServerObject",
|
'Update Server Object' => 'updateServerObject',
|
||||||
"Validate Server Config" => "validateServerConfig",
|
'Validate Server Config' => 'validateServerConfig',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
function VirtFusionDirect_ServiceSingleSignOn(array $params)
|
function VirtFusionDirect_ServiceSingleSignOn(array $params)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$module = new Module();
|
$module = new Module;
|
||||||
$token = $module->fetchLoginTokens($params['serviceid']);
|
$token = $module->fetchLoginTokens($params['serviceid']);
|
||||||
|
|
||||||
if ($token) {
|
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.'];
|
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()];
|
return ['success' => false, 'errorMsg' => $e->getMessage()];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -132,64 +150,104 @@ function VirtFusionDirect_ServiceSingleSignOn(array $params)
|
|||||||
*/
|
*/
|
||||||
function VirtFusionDirect_CreateAccount(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)
|
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)
|
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)
|
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)
|
function VirtFusionDirect_updateServerObject(array $params)
|
||||||
{
|
{
|
||||||
return (new ModuleFunctions())->updateServerObject($params);
|
return (new ModuleFunctions)->updateServerObject($params);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allows changing of the package of a server
|
* Allows changing of the package of a server
|
||||||
*
|
*
|
||||||
* @param array $params
|
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
function VirtFusionDirect_ChangePackage(array $params)
|
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)
|
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)
|
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)
|
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.
|
* Validates server configuration via dry run without creating the server.
|
||||||
*
|
*
|
||||||
* @param array $params
|
|
||||||
* @return string 'success' or error message
|
* @return string 'success' or error message
|
||||||
*/
|
*/
|
||||||
function VirtFusionDirect_validateServerConfig(array $params)
|
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.
|
* Updates tblhosting with disk and bandwidth usage data from VirtFusion.
|
||||||
* Fields updated: diskused, disklimit, bwused, bwlimit, lastupdate
|
* 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
|
* @return string 'success' or error message
|
||||||
*/
|
*/
|
||||||
function VirtFusionDirect_UsageUpdate(array $params)
|
function VirtFusionDirect_UsageUpdate(array $params)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$module = new Module();
|
$module = new Module;
|
||||||
$cp = $module->getCP($params['serverid']);
|
$cp = $module->getCP($params['serverid']);
|
||||||
|
|
||||||
if (!$cp) {
|
if (! $cp) {
|
||||||
return 'No control server found for usage update.';
|
return 'No control server found for usage update.';
|
||||||
}
|
}
|
||||||
|
|
||||||
$services = \WHMCS\Database\Capsule::table('tblhosting')
|
$services = Capsule::table('tblhosting')
|
||||||
->where('server', $params['serverid'])
|
->where('server', $params['serverid'])
|
||||||
->where('domainstatus', 'Active')
|
->where('domainstatus', 'Active')
|
||||||
->get();
|
->get();
|
||||||
@@ -219,7 +277,7 @@ function VirtFusionDirect_UsageUpdate(array $params)
|
|||||||
foreach ($services as $service) {
|
foreach ($services as $service) {
|
||||||
try {
|
try {
|
||||||
$systemService = Database::getSystemService($service->id);
|
$systemService = Database::getSystemService($service->id);
|
||||||
if (!$systemService) {
|
if (! $systemService) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,7 +289,7 @@ function VirtFusionDirect_UsageUpdate(array $params)
|
|||||||
}
|
}
|
||||||
|
|
||||||
$serverData = json_decode($data, true);
|
$serverData = json_decode($data, true);
|
||||||
if (!isset($serverData['data'])) {
|
if (! isset($serverData['data'])) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,15 +313,15 @@ function VirtFusionDirect_UsageUpdate(array $params)
|
|||||||
$update['bwlimit'] = $trafficGB > 0 ? $trafficGB * 1024 : 0;
|
$update['bwlimit'] = $trafficGB > 0 ? $trafficGB * 1024 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!empty($update)) {
|
if (! empty($update)) {
|
||||||
$update['lastupdate'] = date('Y-m-d H:i:s');
|
$update['lastupdate'] = date('Y-m-d H:i:s');
|
||||||
\WHMCS\Database\Capsule::table('tblhosting')
|
Capsule::table('tblhosting')
|
||||||
->where('id', $service->id)
|
->where('id', $service->id)
|
||||||
->update($update);
|
->update($update);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Self-service auto top-off
|
// Self-service auto top-off
|
||||||
$product = \WHMCS\Database\Capsule::table('tblproducts')
|
$product = Capsule::table('tblproducts')
|
||||||
->where('id', $service->packageid)
|
->where('id', $service->packageid)
|
||||||
->first();
|
->first();
|
||||||
|
|
||||||
@@ -278,24 +336,24 @@ function VirtFusionDirect_UsageUpdate(array $params)
|
|||||||
$credit = $usageInner['credit'] ?? $usageInner['balance'] ?? null;
|
$credit = $usageInner['credit'] ?? $usageInner['balance'] ?? null;
|
||||||
if ($credit !== null && (float) $credit < $threshold) {
|
if ($credit !== null && (float) $credit < $threshold) {
|
||||||
$module->addSelfServiceCredit($service->id, $topOffAmount, 'Auto top-off');
|
$module->addSelfServiceCredit($service->id, $topOffAmount, 'Auto top-off');
|
||||||
\WHMCS\Module\Server\VirtFusionDirect\Log::insert(
|
Log::insert(
|
||||||
'UsageUpdate:autoTopOff',
|
'UsageUpdate:autoTopOff',
|
||||||
['serviceId' => $service->id, 'credit' => $credit, 'threshold' => $threshold],
|
['serviceId' => $service->id, 'credit' => $credit, 'threshold' => $threshold],
|
||||||
['amount' => $topOffAmount]
|
['amount' => $topOffAmount],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (\Exception $e) {
|
} catch (Exception $e) {
|
||||||
// Log but continue processing other services
|
// 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;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'success';
|
return 'success';
|
||||||
} catch (\Exception $e) {
|
} catch (Exception $e) {
|
||||||
return 'Usage update failed: ' . $e->getMessage();
|
return 'Usage update failed: ' . $e->getMessage();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,92 +2,97 @@
|
|||||||
|
|
||||||
require dirname(__DIR__, 3) . '/init.php';
|
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\Database;
|
||||||
|
use WHMCS\Module\Server\VirtFusionDirect\Log;
|
||||||
use WHMCS\Module\Server\VirtFusionDirect\Module;
|
use WHMCS\Module\Server\VirtFusionDirect\Module;
|
||||||
use WHMCS\Module\Server\VirtFusionDirect\ServerResource;
|
use WHMCS\Module\Server\VirtFusionDirect\ServerResource;
|
||||||
|
|
||||||
$vf = new Module();
|
$vf = new Module;
|
||||||
|
|
||||||
$vf->adminOnly();
|
try {
|
||||||
|
|
||||||
switch ($vf->validateAction(true)) {
|
$vf->adminOnly();
|
||||||
|
|
||||||
/**
|
switch ($vf->validateAction(true)) {
|
||||||
*
|
|
||||||
* Get server information.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
case 'serverData':
|
|
||||||
|
|
||||||
if ($vf->validateServiceID(true)) {
|
/**
|
||||||
|
* Get server information.
|
||||||
|
*/
|
||||||
|
case 'serverData':
|
||||||
|
|
||||||
/** No need to validate ownership **/
|
$serviceID = $vf->validateServiceID(true);
|
||||||
|
|
||||||
$whmcsService = Database::getWhmcsService((int)$_GET['serviceID']);
|
$whmcsService = Database::getWhmcsService($serviceID);
|
||||||
|
|
||||||
if (!$whmcsService) {
|
if (! $whmcsService) {
|
||||||
$vf->output(['success' => false, 'errors' => 'Service not found.'], true, true, 404);
|
$vf->output(['success' => false, 'errors' => 'Service not found.'], true, true, 404);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($whmcsService->domainstatus == 'Pending' || $whmcsService->domainstatus == 'Terminated' || $whmcsService->domainstatus == 'Cancelled' || $whmcsService->domainstatus == 'Fraud') {
|
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);
|
$vf->output(['success' => false, 'errors' => 'Server is not Active, Suspended or Completed. Not fetching remote data.'], true, true, 400);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
$data = $vf->fetchServerData((int)$_GET['serviceID']);
|
$data = $vf->fetchServerData($serviceID);
|
||||||
|
|
||||||
if (!$data) {
|
if (! $data) {
|
||||||
$vf->output(['success' => false, 'errors' => 'No data returned from VirtFusion.'], true, true, 502);
|
$vf->output(['success' => false, 'errors' => 'No data returned from VirtFusion.'], true, true, 502);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
(new Module())->updateWhmcsServiceParamsOnServerObject((int)$_GET['serviceID'], $data);
|
$vf->updateWhmcsServiceParamsOnServerObject($serviceID, $data);
|
||||||
$vf->output(['success' => true, 'data' => (new ServerResource())->process($data)], true, true, 200);
|
$vf->output(['success' => true, 'data' => (new ServerResource)->process($data)], true, true, 200);
|
||||||
|
break;
|
||||||
|
|
||||||
}
|
/**
|
||||||
break;
|
* Impersonate server owner.
|
||||||
|
*/
|
||||||
|
case 'impersonateServerOwner':
|
||||||
|
|
||||||
/**
|
$serviceID = $vf->validateServiceID(true);
|
||||||
*
|
|
||||||
* Impersonate server owner.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
case 'impersonateServerOwner':
|
|
||||||
|
|
||||||
if ($vf->validateServiceID(true)) {
|
$service = Database::getSystemService($serviceID);
|
||||||
|
if (! $service) {
|
||||||
$service = Database::getSystemService((int)$_GET['serviceID']);
|
|
||||||
|
|
||||||
if (!$service) {
|
|
||||||
$vf->output(['success' => false, 'errors' => 'Service not found'], true, true, 404);
|
$vf->output(['success' => false, 'errors' => 'Service not found'], true, true, 404);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
$whmcsService = Database::getWhmcsService((int)$_GET['serviceID']);
|
$whmcsService = Database::getWhmcsService($serviceID);
|
||||||
|
if (! $whmcsService) {
|
||||||
if (!$whmcsService) {
|
|
||||||
$vf->output(['success' => false, 'errors' => 'WHMCS service not found'], true, true, 404);
|
$vf->output(['success' => false, 'errors' => 'WHMCS service not found'], true, true, 404);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
$cp = $vf->getCP($whmcsService->server);
|
$cp = $vf->getCP($whmcsService->server);
|
||||||
|
if (! $cp) {
|
||||||
if (!$cp) {
|
|
||||||
$vf->output(['success' => false, 'errors' => 'Control server not found'], true, true, 500);
|
$vf->output(['success' => false, 'errors' => 'Control server not found'], true, true, 500);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
$request = $vf->initCurl($cp['token']);
|
$request = $vf->initCurl($cp['token']);
|
||||||
|
$data = $request->get($cp['url'] . '/users/' . (int) $whmcsService->userid . '/byExtRelation');
|
||||||
$data = $request->get($cp['url'] . '/users/' . $whmcsService->userid . '/byExtRelation');
|
|
||||||
|
|
||||||
if ($request->getRequestInfo('http_code') === 200) {
|
if ($request->getRequestInfo('http_code') === 200) {
|
||||||
$vf->output(['success' => true, 'url' => $cp['base_url'], 'user' => json_decode($data, true)['data']], true, true, 200);
|
$vf->output(['success' => true, 'url' => $cp['base_url'], 'user' => json_decode($data, true)['data']], true, true, 200);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
$vf->output(['success' => false, 'errors' => 'Received HTTP code ' . $request->getRequestInfo('http_code')], true, true, 502);
|
$vf->output(['success' => false, 'errors' => 'Unable to fetch user data'], true, true, 502);
|
||||||
|
break;
|
||||||
|
|
||||||
}
|
default:
|
||||||
break;
|
$vf->output(['success' => false, 'errors' => 'invalid action'], true, true, 400);
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
} catch (Exception $e) {
|
||||||
/** No valid action was specified **/
|
Log::insert('admin.php', [], $e->getMessage());
|
||||||
|
$vf->output(['success' => false, 'errors' => 'An unexpected error occurred'], true, true, 500);
|
||||||
$vf->output(['success' => false, 'errors' => 'invalid action'], true, true, 400);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,302 +2,414 @@
|
|||||||
|
|
||||||
require dirname(__DIR__, 3) . '/init.php';
|
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\Module;
|
||||||
use WHMCS\Module\Server\VirtFusionDirect\ServerResource;
|
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);
|
||||||
|
|
||||||
/**
|
switch ($action) {
|
||||||
* Reset Password.
|
|
||||||
*/
|
|
||||||
case 'resetPassword':
|
|
||||||
|
|
||||||
$serviceID = $vf->validateServiceID(true);
|
/**
|
||||||
$client = $vf->validateUserOwnsService($serviceID);
|
* Reset Password.
|
||||||
|
*/
|
||||||
|
case 'resetPassword':
|
||||||
|
|
||||||
if (!$client) {
|
$serviceID = $vf->validateServiceID(true);
|
||||||
$vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403);
|
$client = $vf->validateUserOwnsService($serviceID);
|
||||||
}
|
|
||||||
|
|
||||||
$data = $vf->resetUserPassword($serviceID, $client);
|
if (! $client) {
|
||||||
|
$vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if ($data) {
|
$data = $vf->resetUserPassword($serviceID, $client);
|
||||||
$vf->output(['success' => true, 'data' => $data->data], true, true, 200);
|
|
||||||
}
|
|
||||||
|
|
||||||
$vf->output(['success' => false, 'errors' => 'Password reset failed'], true, true, 500);
|
if ($data) {
|
||||||
break;
|
$vf->output(['success' => true, 'data' => $data->data], true, true, 200);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
$vf->output(['success' => false, 'errors' => 'Password reset failed'], true, true, 500);
|
||||||
* Get server information.
|
break;
|
||||||
*/
|
|
||||||
case 'serverData':
|
|
||||||
|
|
||||||
$serviceID = $vf->validateServiceID(true);
|
/**
|
||||||
|
* Get server information.
|
||||||
|
*/
|
||||||
|
case 'serverData':
|
||||||
|
|
||||||
if (!$vf->validateUserOwnsService($serviceID)) {
|
$serviceID = $vf->validateServiceID(true);
|
||||||
$vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403);
|
|
||||||
}
|
|
||||||
|
|
||||||
$data = $vf->fetchServerData($serviceID);
|
if (! $vf->validateUserOwnsService($serviceID)) {
|
||||||
|
$vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if ($data) {
|
$data = $vf->fetchServerData($serviceID);
|
||||||
(new Module())->updateWhmcsServiceParamsOnServerObject($serviceID, $data);
|
|
||||||
$vf->output(['success' => true, 'data' => (new ServerResource())->process($data)], true, true, 200);
|
|
||||||
}
|
|
||||||
|
|
||||||
$vf->output(['success' => false, 'errors' => 'Unable to retrieve server data'], true, true, 500);
|
if ($data) {
|
||||||
break;
|
$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);
|
||||||
* Login as server owner.
|
break;
|
||||||
*/
|
|
||||||
case 'loginAsServerOwner':
|
|
||||||
|
|
||||||
$serviceID = $vf->validateServiceID(true);
|
/**
|
||||||
|
* Login as server owner.
|
||||||
|
*/
|
||||||
|
case 'loginAsServerOwner':
|
||||||
|
|
||||||
if (!$vf->validateUserOwnsService($serviceID)) {
|
$serviceID = $vf->validateServiceID(true);
|
||||||
$vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403);
|
|
||||||
}
|
|
||||||
|
|
||||||
$token = $vf->fetchLoginTokens($serviceID);
|
if (! $vf->validateUserOwnsService($serviceID)) {
|
||||||
|
$vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if ($token) {
|
$token = $vf->fetchLoginTokens($serviceID);
|
||||||
$vf->output(['success' => true, 'token_url' => $token], true, true, 200);
|
|
||||||
}
|
|
||||||
|
|
||||||
$vf->output(['success' => false, 'errors' => 'Unable to generate login token'], true, true, 500);
|
if ($token) {
|
||||||
break;
|
$vf->output(['success' => true, 'token_url' => $token], true, true, 200);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
$vf->output(['success' => false, 'errors' => 'Unable to generate login token'], true, true, 500);
|
||||||
* Power management actions: boot, shutdown, restart, poweroff
|
break;
|
||||||
*/
|
|
||||||
case 'powerAction':
|
|
||||||
|
|
||||||
$serviceID = $vf->validateServiceID(true);
|
/**
|
||||||
|
* Power management actions: boot, shutdown, restart, poweroff
|
||||||
|
*/
|
||||||
|
case 'powerAction':
|
||||||
|
|
||||||
if (!$vf->validateUserOwnsService($serviceID)) {
|
$serviceID = $vf->validateServiceID(true);
|
||||||
$vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403);
|
|
||||||
}
|
|
||||||
|
|
||||||
$powerAction = isset($_GET['powerAction']) ? preg_replace('/[^a-zA-Z]/', '', $_GET['powerAction']) : '';
|
if (! $vf->validateUserOwnsService($serviceID)) {
|
||||||
$allowedActions = ['boot', 'shutdown', 'restart', 'poweroff'];
|
$vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if (!in_array($powerAction, $allowedActions, true)) {
|
$powerAction = isset($_POST['powerAction']) ? preg_replace('/[^a-zA-Z]/', '', $_POST['powerAction']) : '';
|
||||||
$vf->output(['success' => false, 'errors' => 'Invalid power action'], true, true, 400);
|
$allowedActions = ['boot', 'shutdown', 'restart', 'poweroff'];
|
||||||
}
|
|
||||||
|
|
||||||
$result = $vf->serverPowerAction($serviceID, $powerAction);
|
if (! in_array($powerAction, $allowedActions, true)) {
|
||||||
|
$vf->output(['success' => false, 'errors' => 'Invalid power action'], true, true, 400);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if ($result) {
|
$result = $vf->serverPowerAction($serviceID, $powerAction);
|
||||||
$vf->output(['success' => true, 'data' => ['action' => $powerAction, 'message' => 'Power action queued successfully']], true, true, 200);
|
|
||||||
}
|
|
||||||
|
|
||||||
$vf->output(['success' => false, 'errors' => 'Power action failed. The server may be locked or unavailable.'], true, true, 500);
|
if ($result) {
|
||||||
break;
|
$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);
|
||||||
* Rebuild/reinstall server with new OS.
|
break;
|
||||||
*/
|
|
||||||
case 'rebuild':
|
|
||||||
|
|
||||||
$serviceID = $vf->validateServiceID(true);
|
/**
|
||||||
|
* Rebuild/reinstall server with new OS.
|
||||||
|
*/
|
||||||
|
case 'rebuild':
|
||||||
|
|
||||||
if (!$vf->validateUserOwnsService($serviceID)) {
|
$serviceID = $vf->validateServiceID(true);
|
||||||
$vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403);
|
|
||||||
}
|
|
||||||
|
|
||||||
$osId = isset($_GET['osId']) ? (int) $_GET['osId'] : 0;
|
if (! $vf->validateUserOwnsService($serviceID)) {
|
||||||
$hostname = isset($_GET['hostname']) ? preg_replace('/[^a-zA-Z0-9.\-]/', '', $_GET['hostname']) : null;
|
$vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if ($osId <= 0) {
|
$osId = isset($_POST['osId']) ? (int) $_POST['osId'] : 0;
|
||||||
$vf->output(['success' => false, 'errors' => 'Invalid operating system ID'], true, true, 400);
|
$hostname = isset($_POST['hostname']) ? preg_replace('/[^a-zA-Z0-9.\-]/', '', $_POST['hostname']) : null;
|
||||||
}
|
|
||||||
|
|
||||||
$result = $vf->rebuildServer($serviceID, $osId, $hostname);
|
if ($osId <= 0) {
|
||||||
|
$vf->output(['success' => false, 'errors' => 'Invalid operating system ID'], true, true, 400);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if ($result) {
|
$result = $vf->rebuildServer($serviceID, $osId, $hostname);
|
||||||
$vf->output(['success' => true, 'data' => ['message' => 'Server rebuild initiated successfully']], true, true, 200);
|
|
||||||
}
|
|
||||||
|
|
||||||
$vf->output(['success' => false, 'errors' => 'Server rebuild failed. The server may be locked or unavailable.'], true, true, 500);
|
if ($result) {
|
||||||
break;
|
$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);
|
||||||
* Rename server.
|
break;
|
||||||
*/
|
|
||||||
case 'rename':
|
|
||||||
|
|
||||||
$serviceID = $vf->validateServiceID(true);
|
/**
|
||||||
|
* Rename server.
|
||||||
|
*/
|
||||||
|
case 'rename':
|
||||||
|
|
||||||
if (!$vf->validateUserOwnsService($serviceID)) {
|
$serviceID = $vf->validateServiceID(true);
|
||||||
$vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403);
|
|
||||||
}
|
|
||||||
|
|
||||||
$newName = isset($_GET['name']) ? trim($_GET['name']) : '';
|
if (! $vf->validateUserOwnsService($serviceID)) {
|
||||||
$newName = htmlspecialchars($newName, ENT_QUOTES, 'UTF-8');
|
$vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if (empty($newName) || strlen($newName) > 255) {
|
$newName = isset($_POST['name']) ? trim($_POST['name']) : '';
|
||||||
$vf->output(['success' => false, 'errors' => 'Invalid server name'], true, true, 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
$result = $vf->renameServer($serviceID, $newName);
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
if ($result) {
|
$result = $vf->renameServer($serviceID, $newName);
|
||||||
$vf->output(['success' => true, 'data' => ['message' => 'Server renamed successfully']], true, true, 200);
|
|
||||||
}
|
|
||||||
|
|
||||||
$vf->output(['success' => false, 'errors' => 'Server rename failed'], true, true, 500);
|
if ($result) {
|
||||||
break;
|
$vf->output(['success' => true, 'data' => ['message' => 'Server renamed successfully']], true, true, 200);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
$vf->output(['success' => false, 'errors' => 'Server rename failed'], true, true, 500);
|
||||||
* Get available OS templates for rebuild.
|
break;
|
||||||
*/
|
|
||||||
case 'osTemplates':
|
|
||||||
|
|
||||||
$serviceID = $vf->validateServiceID(true);
|
/**
|
||||||
|
* Get available OS templates for rebuild.
|
||||||
|
*/
|
||||||
|
case 'osTemplates':
|
||||||
|
|
||||||
if (!$vf->validateUserOwnsService($serviceID)) {
|
$serviceID = $vf->validateServiceID(true);
|
||||||
$vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403);
|
|
||||||
}
|
|
||||||
|
|
||||||
$templates = $vf->fetchOsTemplates($serviceID);
|
if (! $vf->validateUserOwnsService($serviceID)) {
|
||||||
|
$vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if ($templates !== false) {
|
$templates = $vf->fetchOsTemplates($serviceID);
|
||||||
$vf->output(['success' => true, 'data' => $templates], true, true, 200);
|
|
||||||
}
|
|
||||||
|
|
||||||
$vf->output(['success' => false, 'errors' => 'Unable to fetch OS templates'], true, true, 500);
|
if ($templates !== false) {
|
||||||
break;
|
$vf->output(['success' => true, 'data' => $templates], true, true, 200);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// =================================================================
|
$vf->output(['success' => false, 'errors' => 'Unable to fetch OS templates'], true, true, 500);
|
||||||
// IP Address Management
|
break;
|
||||||
// =================================================================
|
|
||||||
|
|
||||||
/**
|
// =================================================================
|
||||||
* Remove an IPv4 address.
|
// Server Password Reset
|
||||||
*/
|
// =================================================================
|
||||||
case 'removeIPv4':
|
|
||||||
|
|
||||||
$serviceID = $vf->validateServiceID(true);
|
/**
|
||||||
|
* Reset server root password.
|
||||||
|
*/
|
||||||
|
case 'resetServerPassword':
|
||||||
|
|
||||||
if (!$vf->validateUserOwnsService($serviceID)) {
|
$serviceID = $vf->validateServiceID(true);
|
||||||
$vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403);
|
|
||||||
}
|
|
||||||
|
|
||||||
$ipAddress = isset($_GET['ip']) ? trim($_GET['ip']) : '';
|
if (! $vf->validateUserOwnsService($serviceID)) {
|
||||||
if (!filter_var($ipAddress, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
$vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403);
|
||||||
$vf->output(['success' => false, 'errors' => 'Invalid IPv4 address'], true, true, 400);
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
$result = $vf->removeIPv4($serviceID, $ipAddress);
|
$result = $vf->resetServerPassword($serviceID);
|
||||||
|
|
||||||
if ($result) {
|
if ($result !== false) {
|
||||||
$vf->output(['success' => true, 'data' => ['message' => 'IPv4 address removed successfully']], true, true, 200);
|
$vf->output(['success' => true, 'data' => $result], true, true, 200);
|
||||||
}
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
$vf->output(['success' => false, 'errors' => 'Failed to remove IPv4 address'], true, true, 500);
|
$vf->output(['success' => false, 'errors' => 'Password reset failed'], true, true, 500);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// =================================================================
|
// =================================================================
|
||||||
// VNC Console
|
// Backup Listing
|
||||||
// =================================================================
|
// =================================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get VNC console URL.
|
* Get server backups.
|
||||||
*/
|
*/
|
||||||
case 'vnc':
|
case 'backups':
|
||||||
|
|
||||||
$serviceID = $vf->validateServiceID(true);
|
$serviceID = $vf->validateServiceID(true);
|
||||||
|
|
||||||
if (!$vf->validateUserOwnsService($serviceID)) {
|
if (! $vf->validateUserOwnsService($serviceID)) {
|
||||||
$vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403);
|
$vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403);
|
||||||
}
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
$result = $vf->getVncConsole($serviceID);
|
$result = $vf->getServerBackups($serviceID);
|
||||||
|
|
||||||
if ($result !== false) {
|
if ($result !== false) {
|
||||||
$vf->output(['success' => true, 'data' => $result], true, true, 200);
|
$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);
|
$vf->output(['success' => false, 'errors' => 'Unable to retrieve backups'], true, true, 500);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// =================================================================
|
// =================================================================
|
||||||
// Self Service — Credit & Usage
|
// Traffic Statistics
|
||||||
// =================================================================
|
// =================================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get self-service usage data.
|
* Get traffic statistics for a server.
|
||||||
*/
|
*/
|
||||||
case 'selfServiceUsage':
|
case 'trafficStats':
|
||||||
|
|
||||||
$serviceID = $vf->validateServiceID(true);
|
$serviceID = $vf->validateServiceID(true);
|
||||||
|
|
||||||
if (!$vf->validateUserOwnsService($serviceID)) {
|
if (! $vf->validateUserOwnsService($serviceID)) {
|
||||||
$vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403);
|
$vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403);
|
||||||
}
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
$result = $vf->getSelfServiceUsage($serviceID);
|
$result = $vf->getTrafficStats($serviceID);
|
||||||
|
|
||||||
if ($result !== false) {
|
if ($result !== false) {
|
||||||
$vf->output(['success' => true, 'data' => $result], true, true, 200);
|
$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);
|
$vf->output(['success' => false, 'errors' => 'Unable to retrieve traffic statistics'], true, true, 500);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
/**
|
// =================================================================
|
||||||
* Get self-service billing report.
|
// VNC Console
|
||||||
*/
|
// =================================================================
|
||||||
case 'selfServiceReport':
|
|
||||||
|
|
||||||
$serviceID = $vf->validateServiceID(true);
|
/**
|
||||||
|
* Get VNC console URL.
|
||||||
|
*/
|
||||||
|
case 'vnc':
|
||||||
|
|
||||||
if (!$vf->validateUserOwnsService($serviceID)) {
|
$serviceID = $vf->validateServiceID(true);
|
||||||
$vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403);
|
|
||||||
}
|
|
||||||
|
|
||||||
$result = $vf->getSelfServiceReport($serviceID);
|
if (! $vf->validateUserOwnsService($serviceID)) {
|
||||||
|
$vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if ($result !== false) {
|
$result = $vf->getVncConsole($serviceID);
|
||||||
$vf->output(['success' => true, 'data' => $result], true, true, 200);
|
|
||||||
}
|
|
||||||
|
|
||||||
$vf->output(['success' => false, 'errors' => 'Unable to retrieve self-service report'], true, true, 500);
|
if ($result !== false) {
|
||||||
break;
|
$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);
|
||||||
* Add self-service credit.
|
break;
|
||||||
*/
|
|
||||||
case 'selfServiceAddCredit':
|
|
||||||
|
|
||||||
$serviceID = $vf->validateServiceID(true);
|
/**
|
||||||
|
* Toggle VNC on/off.
|
||||||
|
*/
|
||||||
|
case 'toggleVnc':
|
||||||
|
|
||||||
if (!$vf->validateUserOwnsService($serviceID)) {
|
$serviceID = $vf->validateServiceID(true);
|
||||||
$vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403);
|
|
||||||
}
|
|
||||||
|
|
||||||
$tokens = isset($_GET['tokens']) ? (float) $_GET['tokens'] : 0;
|
if (! $vf->validateUserOwnsService($serviceID)) {
|
||||||
if ($tokens <= 0) {
|
$vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403);
|
||||||
$vf->output(['success' => false, 'errors' => 'Invalid credit amount. Must be a positive number.'], true, true, 400);
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
$result = $vf->addSelfServiceCredit($serviceID, $tokens);
|
$enabled = isset($_POST['enabled']) && $_POST['enabled'] === '1';
|
||||||
|
$result = $vf->toggleVnc($serviceID, $enabled);
|
||||||
|
|
||||||
if ($result !== false) {
|
if ($result !== false) {
|
||||||
$vf->output(['success' => true, 'data' => $result], true, true, 200);
|
$vf->output(['success' => true, 'data' => $result], true, true, 200);
|
||||||
}
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
$vf->output(['success' => false, 'errors' => 'Failed to add credit'], true, true, 500);
|
$vf->output(['success' => false, 'errors' => 'Failed to toggle VNC'], true, true, 500);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
// =================================================================
|
||||||
$vf->output(['success' => false, 'errors' => 'invalid action'], true, true, 400);
|
// 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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
if (!defined("WHMCS")) {
|
if (! defined('WHMCS')) {
|
||||||
die("This file cannot be accessed directly");
|
exit('This file cannot be accessed directly');
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@@ -16,4 +16,4 @@ return [
|
|||||||
'cpuCores' => 'CPU Cores',
|
'cpuCores' => 'CPU Cores',
|
||||||
'networkProfile' => 'Network Type',
|
'networkProfile' => 'Network Type',
|
||||||
'storageProfile' => 'Storage Type',
|
'storageProfile' => 'Storage Type',
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use WHMCS\Database\Capsule;
|
||||||
use WHMCS\Module\Server\VirtFusionDirect\ConfigureService;
|
use WHMCS\Module\Server\VirtFusionDirect\ConfigureService;
|
||||||
use WHMCS\Module\Server\VirtFusionDirect\Database;
|
use WHMCS\Module\Server\VirtFusionDirect\Database;
|
||||||
use WHMCS\User\User;
|
use WHMCS\Module\Server\VirtFusionDirect\Module;
|
||||||
|
|
||||||
if (!defined("WHMCS")) {
|
if (! defined('WHMCS')) {
|
||||||
die("This file cannot be accessed directly");
|
exit('This file cannot be accessed directly');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -17,47 +18,51 @@ if (!defined("WHMCS")) {
|
|||||||
add_hook('ShoppingCartValidateCheckout', 1, function ($vars) {
|
add_hook('ShoppingCartValidateCheckout', 1, function ($vars) {
|
||||||
$errors = [];
|
$errors = [];
|
||||||
|
|
||||||
if (!isset($_SESSION['cart']['products']) || !is_array($_SESSION['cart']['products'])) {
|
try {
|
||||||
return $errors;
|
if (! isset($_SESSION['cart']['products']) || ! is_array($_SESSION['cart']['products'])) {
|
||||||
}
|
return $errors;
|
||||||
|
|
||||||
foreach ($_SESSION['cart']['products'] as $key => $product) {
|
|
||||||
$pid = $product['pid'] ?? null;
|
|
||||||
if (!$pid) {
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$dbProduct = \WHMCS\Database\Capsule::table('tblproducts')
|
foreach ($_SESSION['cart']['products'] as $key => $product) {
|
||||||
->where('id', $pid)
|
$pid = $product['pid'] ?? null;
|
||||||
->where('servertype', 'VirtFusionDirect')
|
if (! $pid) {
|
||||||
->first();
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (!$dbProduct) {
|
$dbProduct = Capsule::table('tblproducts')
|
||||||
continue;
|
->where('id', $pid)
|
||||||
}
|
->where('servertype', 'VirtFusionDirect')
|
||||||
|
->first();
|
||||||
|
|
||||||
// Check if Initial Operating System custom field has a value
|
if (! $dbProduct) {
|
||||||
if (isset($product['customfields']) && is_array($product['customfields'])) {
|
continue;
|
||||||
$osSelected = false;
|
}
|
||||||
$customFields = \WHMCS\Database\Capsule::table('tblcustomfields')
|
|
||||||
->where('relid', $pid)
|
|
||||||
->where('type', 'product')
|
|
||||||
->get();
|
|
||||||
|
|
||||||
foreach ($customFields as $field) {
|
// Check if Initial Operating System custom field has a value
|
||||||
if (strtolower(str_replace(' ', '', $field->fieldname)) === 'initialoperatingsystem') {
|
if (isset($product['customfields']) && is_array($product['customfields'])) {
|
||||||
$fieldValue = $product['customfields'][$field->id] ?? '';
|
$osSelected = false;
|
||||||
if (!empty($fieldValue) && is_numeric($fieldValue)) {
|
$customFields = Capsule::table('tblcustomfields')
|
||||||
$osSelected = true;
|
->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;
|
return $errors;
|
||||||
@@ -71,34 +76,31 @@ add_hook('ShoppingCartValidateCheckout', 1, function ($vars) {
|
|||||||
* Works with all WHMCS themes by using vanilla JavaScript and standard form-control classes.
|
* Works with all WHMCS themes by using vanilla JavaScript and standard form-control classes.
|
||||||
*/
|
*/
|
||||||
add_hook('ClientAreaFooterOutput', 1, function ($vars) {
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$cs = new ConfigureService();
|
$cs = new ConfigureService;
|
||||||
|
|
||||||
$templates_data = $cs->fetchTemplates(
|
$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)) {
|
if (empty($templates_data)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$dropdownOptions = [];
|
$vfServer = Capsule::table('tblservers')
|
||||||
|
->where('type', 'VirtFusionDirect')
|
||||||
|
->where('disabled', 0)
|
||||||
|
->first();
|
||||||
|
$baseUrl = $vfServer ? rtrim('https://' . $vfServer->hostname, '/') : '';
|
||||||
|
|
||||||
foreach ($templates_data['data'] as $osCategory) {
|
$galleryData = [
|
||||||
foreach ($osCategory['templates'] as $template) {
|
'baseUrl' => $baseUrl,
|
||||||
$optionValue = $template['id'];
|
'categories' => Module::groupOsTemplates($templates_data['data'] ?? [], true),
|
||||||
$optionLabel = htmlspecialchars($template['name'] . " " . $template['version'] . " " . $template['variant'], ENT_QUOTES, 'UTF-8');
|
];
|
||||||
$dropdownOptions[] = ['id' => $optionValue, 'name' => $optionLabel];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
usort($dropdownOptions, function ($a, $b) {
|
|
||||||
return strcmp($a['name'], $b['name']);
|
|
||||||
});
|
|
||||||
|
|
||||||
$sshKeys = [];
|
$sshKeys = [];
|
||||||
$sshKeysOptions = [];
|
$sshKeysOptions = [];
|
||||||
@@ -109,9 +111,10 @@ add_hook('ClientAreaFooterOutput', 1, function ($vars) {
|
|||||||
if ($sshKey['enabled'] === false) {
|
if ($sshKey['enabled'] === false) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'id' => $sshKey['id'],
|
'id' => $sshKey['id'],
|
||||||
'name' => htmlspecialchars($sshKey['name'], ENT_QUOTES, 'UTF-8')
|
'name' => htmlspecialchars($sshKey['name'], ENT_QUOTES, 'UTF-8'),
|
||||||
];
|
];
|
||||||
}, $sshKeysData['data'])));
|
}, $sshKeysData['data'])));
|
||||||
}
|
}
|
||||||
@@ -138,41 +141,186 @@ add_hook('ClientAreaFooterOutput', 1, function ($vars) {
|
|||||||
|
|
||||||
$systemUrl = Database::getSystemUrl();
|
$systemUrl = Database::getSystemUrl();
|
||||||
|
|
||||||
return "
|
return '
|
||||||
<script src=\"" . htmlspecialchars($systemUrl, ENT_QUOTES, 'UTF-8') . "modules/servers/VirtFusionDirect/templates/js/keygen.js?v=20260207\"></script>
|
<link href="' . htmlspecialchars($systemUrl, ENT_QUOTES, 'UTF-8') . 'modules/servers/VirtFusionDirect/templates/css/module.css?v=' . time() . '" rel="stylesheet">
|
||||||
|
<script src="' . htmlspecialchars($systemUrl, ENT_QUOTES, 'UTF-8') . 'modules/servers/VirtFusionDirect/templates/js/keygen.js?v=' . time() . "\"></script>
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
var osTemplates = " . json_encode($dropdownOptions, JSON_THROW_ON_ERROR | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) . ";
|
var osGalleryData = " . json_encode($galleryData, JSON_THROW_ON_ERROR | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) . ';
|
||||||
var sshKeys = " . json_encode($sshKeysOptions, JSON_THROW_ON_ERROR | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) . ";
|
var sshKeys = ' . json_encode($sshKeysOptions, JSON_THROW_ON_ERROR | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) . ";
|
||||||
|
|
||||||
var osInputField = document.querySelector('[name=\"customfield[" . (int) $osFieldId . "]\"]');
|
var osInputField = document.querySelector('[name=\"customfield[" . (int) $osFieldId . "]\"]');
|
||||||
var sshInputField = " . ($sshFieldId !== null ? "document.querySelector('[name=\"customfield[" . (int) $sshFieldId . "]\"]')" : "null") . ";
|
var sshInputField = " . ($sshFieldId !== null ? "document.querySelector('[name=\"customfield[" . (int) $sshFieldId . "]\"]')" : 'null') . ';
|
||||||
var sshInputLabel = " . ($sshFieldId !== null ? "document.querySelector('[for=\"customfield" . (int) $sshFieldId . "\"]')" : "null") . ";
|
var sshInputLabel = ' . ($sshFieldId !== null ? "document.querySelector('[for=\"customfield" . (int) $sshFieldId . "\"]')" : 'null') . ";
|
||||||
|
|
||||||
if (!osInputField) return;
|
if (!osInputField) return;
|
||||||
|
|
||||||
// Create OS dropdown
|
// Brand color map (must match vfOsBrandColors in module.js)
|
||||||
var osSelect = document.createElement('select');
|
var brandColors = {
|
||||||
osSelect.className = 'form-control';
|
'ubuntu':'#E95420','debian':'#A81D33','rocky':'#10B981','centos':'#932279',
|
||||||
osSelect.setAttribute('id', 'vf-os-select');
|
'almalinux':'#0F4266','alma':'#0F4266','windows':'#0078D4','fedora':'#51A2DA',
|
||||||
|
'arch':'#1793D1','opensuse':'#73BA25','suse':'#73BA25','freebsd':'#AB2B28',
|
||||||
|
'oracle':'#F80000','rhel':'#EE0000','red hat':'#EE0000','cloudlinux':'#0095D9',
|
||||||
|
'gentoo':'#54487A','slackware':'#000','nixos':'#7EBAE4','alpine':'#0D597F'
|
||||||
|
};
|
||||||
|
function getBrandColor(name) {
|
||||||
|
var l = (name || '').toLowerCase();
|
||||||
|
for (var k in brandColors) { if (l.indexOf(k) !== -1) return brandColors[k]; }
|
||||||
|
return '#6c757d';
|
||||||
|
}
|
||||||
|
|
||||||
var defaultOption = document.createElement('option');
|
// Build gallery container
|
||||||
defaultOption.value = '';
|
var galleryWrap = document.createElement('div');
|
||||||
defaultOption.text = '-- Select Operating System --';
|
galleryWrap.style.marginTop = '8px';
|
||||||
osSelect.appendChild(defaultOption);
|
|
||||||
|
|
||||||
osTemplates.forEach(function(template) {
|
var searchInput = document.createElement('input');
|
||||||
var option = document.createElement('option');
|
searchInput.type = 'text';
|
||||||
option.value = template.id;
|
searchInput.className = 'form-control vf-os-search';
|
||||||
option.text = template.name;
|
searchInput.placeholder = 'Search templates...';
|
||||||
osSelect.appendChild(option);
|
galleryWrap.appendChild(searchInput);
|
||||||
|
|
||||||
|
var galleryContainer = document.createElement('div');
|
||||||
|
galleryContainer.setAttribute('id', 'vf-checkout-os-gallery');
|
||||||
|
galleryContainer.style.marginTop = '8px';
|
||||||
|
|
||||||
|
if (osGalleryData.categories && osGalleryData.categories.length > 0) {
|
||||||
|
osGalleryData.categories.forEach(function(cat, ci) {
|
||||||
|
var section = document.createElement('div');
|
||||||
|
section.className = 'vf-os-category';
|
||||||
|
|
||||||
|
var header = document.createElement('div');
|
||||||
|
header.className = 'vf-os-category-header';
|
||||||
|
var catColor = getBrandColor(cat.name);
|
||||||
|
|
||||||
|
var catIcon = document.createElement('span');
|
||||||
|
catIcon.className = 'vf-os-category-icon';
|
||||||
|
if (cat.icon && osGalleryData.baseUrl) {
|
||||||
|
var catImg = document.createElement('img');
|
||||||
|
catImg.src = osGalleryData.baseUrl + '/img/logo/' + encodeURIComponent(cat.icon);
|
||||||
|
catImg.alt = '';
|
||||||
|
catImg.onerror = function() { this.parentNode.style.background = catColor; this.parentNode.textContent = (cat.name || '?')[0].toUpperCase(); };
|
||||||
|
catIcon.appendChild(catImg);
|
||||||
|
} else if (cat.name === 'Other') {
|
||||||
|
catIcon.style.background = '#6c757d';
|
||||||
|
catIcon.innerHTML = '<svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"#fff\"><path d=\"M3 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3zm1 2h8v2H4V4zm0 3h8v1H4V7zm0 2h5v1H4V9z\"/></svg>';
|
||||||
|
} else {
|
||||||
|
catIcon.style.background = catColor;
|
||||||
|
catIcon.textContent = (cat.name || '?')[0].toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
var catTitle = document.createElement('span');
|
||||||
|
catTitle.textContent = cat.name + ' (' + cat.templates.length + ')';
|
||||||
|
|
||||||
|
var arrow = document.createElement('span');
|
||||||
|
arrow.className = 'vf-os-category-arrow';
|
||||||
|
arrow.textContent = ci === 0 ? '\u25BC' : '\u25B6';
|
||||||
|
|
||||||
|
header.appendChild(catIcon);
|
||||||
|
header.appendChild(catTitle);
|
||||||
|
header.appendChild(arrow);
|
||||||
|
section.appendChild(header);
|
||||||
|
|
||||||
|
var grid = document.createElement('div');
|
||||||
|
grid.className = 'vf-os-grid';
|
||||||
|
if (ci !== 0) grid.style.display = 'none';
|
||||||
|
|
||||||
|
header.addEventListener('click', function() {
|
||||||
|
var isOpen = grid.style.display !== 'none';
|
||||||
|
// Collapse all
|
||||||
|
galleryContainer.querySelectorAll('.vf-os-grid').forEach(function(g) { g.style.display = 'none'; });
|
||||||
|
galleryContainer.querySelectorAll('.vf-os-category-arrow').forEach(function(a) { a.textContent = '\u25B6'; });
|
||||||
|
// Toggle this one
|
||||||
|
if (!isOpen) {
|
||||||
|
grid.style.display = '';
|
||||||
|
arrow.textContent = '\u25BC';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
cat.templates.forEach(function(tpl) {
|
||||||
|
var fullLabel = tpl.name + (tpl.version ? ' ' + tpl.version : '') + (tpl.variant ? ' ' + tpl.variant : '');
|
||||||
|
var card = document.createElement('div');
|
||||||
|
card.className = 'vf-os-card' + (tpl.eol ? ' vf-os-card-eol' : '');
|
||||||
|
card.setAttribute('data-id', tpl.id);
|
||||||
|
card.setAttribute('data-search', fullLabel.toLowerCase());
|
||||||
|
|
||||||
|
var iconDiv = document.createElement('div');
|
||||||
|
iconDiv.className = 'vf-os-icon';
|
||||||
|
if (tpl.icon && osGalleryData.baseUrl) {
|
||||||
|
var tplImg = document.createElement('img');
|
||||||
|
tplImg.src = osGalleryData.baseUrl + '/img/logo/' + encodeURIComponent(tpl.icon);
|
||||||
|
tplImg.alt = '';
|
||||||
|
tplImg.onerror = function() { this.parentNode.style.background = catColor; this.parentNode.textContent = ''; var s = document.createElement('span'); s.textContent = (tpl.name || '?')[0].toUpperCase(); this.parentNode.appendChild(s); };
|
||||||
|
iconDiv.appendChild(tplImg);
|
||||||
|
} else {
|
||||||
|
iconDiv.style.background = catColor;
|
||||||
|
var sp = document.createElement('span');
|
||||||
|
sp.textContent = (tpl.name || '?')[0].toUpperCase();
|
||||||
|
iconDiv.appendChild(sp);
|
||||||
|
}
|
||||||
|
card.appendChild(iconDiv);
|
||||||
|
|
||||||
|
var labelDiv = document.createElement('div');
|
||||||
|
labelDiv.className = 'vf-os-label';
|
||||||
|
labelDiv.textContent = tpl.name;
|
||||||
|
card.appendChild(labelDiv);
|
||||||
|
|
||||||
|
var verDiv = document.createElement('div');
|
||||||
|
verDiv.className = 'vf-os-version';
|
||||||
|
verDiv.textContent = (tpl.version || '') + (tpl.variant ? ' ' + tpl.variant : '');
|
||||||
|
card.appendChild(verDiv);
|
||||||
|
|
||||||
|
if (tpl.eol) {
|
||||||
|
var eolBadge = document.createElement('span');
|
||||||
|
eolBadge.className = 'vf-os-eol-badge';
|
||||||
|
eolBadge.textContent = 'EOL';
|
||||||
|
card.appendChild(eolBadge);
|
||||||
|
}
|
||||||
|
|
||||||
|
card.addEventListener('click', function() {
|
||||||
|
galleryContainer.querySelectorAll('.vf-os-card').forEach(function(c) { c.classList.remove('vf-os-card-selected'); });
|
||||||
|
card.classList.add('vf-os-card-selected');
|
||||||
|
osInputField.value = tpl.id;
|
||||||
|
galleryContainer.style.borderColor = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
grid.appendChild(card);
|
||||||
|
});
|
||||||
|
|
||||||
|
section.appendChild(grid);
|
||||||
|
galleryContainer.appendChild(section);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
galleryWrap.appendChild(galleryContainer);
|
||||||
|
|
||||||
|
// Search handler
|
||||||
|
searchInput.addEventListener('keyup', function() {
|
||||||
|
var q = this.value.toLowerCase();
|
||||||
|
galleryContainer.querySelectorAll('.vf-os-card').forEach(function(c) {
|
||||||
|
c.style.display = c.getAttribute('data-search').indexOf(q) !== -1 ? '' : 'none';
|
||||||
|
});
|
||||||
|
galleryContainer.querySelectorAll('.vf-os-category').forEach(function(s) {
|
||||||
|
var cards = s.querySelectorAll('.vf-os-card');
|
||||||
|
var hasVisible = false;
|
||||||
|
cards.forEach(function(c) { if (c.style.display !== 'none') hasVisible = true; });
|
||||||
|
s.style.display = hasVisible ? '' : 'none';
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
osSelect.addEventListener('change', function() {
|
// Validation: red border if no selection on form submit
|
||||||
osInputField.value = this.value;
|
var form = osInputField.closest('form');
|
||||||
});
|
if (form) {
|
||||||
|
form.addEventListener('submit', function(e) {
|
||||||
|
if (!osInputField.value) {
|
||||||
|
galleryContainer.style.border = '2px solid #dc3545';
|
||||||
|
galleryContainer.style.borderRadius = '8px';
|
||||||
|
galleryContainer.style.padding = '4px';
|
||||||
|
galleryContainer.scrollIntoView({behavior: 'smooth', block: 'center'});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
osInputField.parentNode.insertBefore(osSelect, osInputField.nextSibling);
|
osInputField.parentNode.insertBefore(galleryWrap, osInputField.nextSibling);
|
||||||
osInputField.style.display = 'none';
|
osInputField.style.display = 'none';
|
||||||
|
|
||||||
// Handle SSH keys
|
// Handle SSH keys
|
||||||
@@ -423,7 +571,7 @@ add_hook('ClientAreaFooterOutput', 1, function ($vars) {
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
";
|
";
|
||||||
} catch (\Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
// Silently fail - don't break the checkout page
|
// Silently fail - don't break the checkout page
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,41 +2,76 @@
|
|||||||
|
|
||||||
namespace WHMCS\Module\Server\VirtFusionDirect;
|
namespace WHMCS\Module\Server\VirtFusionDirect;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Static methods that generate HTML fragments for the WHMCS admin services tab.
|
||||||
|
*/
|
||||||
class AdminHTML
|
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)
|
public static function options($systemUrl, $serviceId)
|
||||||
{
|
{
|
||||||
$systemUrl = htmlspecialchars($systemUrl, ENT_QUOTES, 'UTF-8');
|
$systemUrl = htmlspecialchars($systemUrl, ENT_QUOTES, 'UTF-8');
|
||||||
|
|
||||||
return <<<EOT
|
return <<<EOT
|
||||||
<button onclick="impersonateServerOwner('${serviceId}', '${systemUrl}')" type="button" class="btn btn-primary">Impersonate Server Owner</button>
|
<button onclick="impersonateServerOwner('${serviceId}', '${systemUrl}')" type="button" class="btn btn-primary">Impersonate Server Owner</button>
|
||||||
<span class="text-info"> A valid VirtFusion admin session in the same browser is required for this functionality to work.</span>
|
<span class="text-info"> A valid VirtFusion admin session in the same browser is required for this functionality to work.</span>
|
||||||
EOT;
|
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)
|
public static function serverObject($serverObject)
|
||||||
{
|
{
|
||||||
$serverObject = htmlspecialchars($serverObject, ENT_QUOTES, 'UTF-8');
|
$serverObject = htmlspecialchars($serverObject, ENT_QUOTES, 'UTF-8');
|
||||||
|
|
||||||
return <<<EOT
|
return <<<EOT
|
||||||
<textarea class="form-control" name="modulefields[1]" rows="10" style="width: 100%" disabled>${serverObject}</textarea>
|
<textarea class="form-control" name="modulefields[1]" rows="10" style="width: 100%" disabled>${serverObject}</textarea>
|
||||||
EOT;
|
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)
|
public static function serverId($serverId)
|
||||||
{
|
{
|
||||||
|
$serverId = (int) $serverId;
|
||||||
|
|
||||||
return <<<EOT
|
return <<<EOT
|
||||||
<input type="text" class="form-control input-200 input-inline" name="modulefields[0]" size="20" value="${serverId}" />
|
<input type="text" class="form-control input-200 input-inline" name="modulefields[0]" size="20" value="${serverId}" />
|
||||||
<span class="text-info"> Changing the Sever ID manually is not recommended. Alterations to this field are usually handled automatically.</span>
|
<span class="text-info"> Changing the Sever ID manually is not recommended. Alterations to this field are usually handled automatically.</span>
|
||||||
EOT;
|
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)
|
public static function serverInfo($systemUrl, $serviceId)
|
||||||
{
|
{
|
||||||
$systemUrl = htmlspecialchars($systemUrl, ENT_QUOTES, 'UTF-8');
|
$systemUrl = htmlspecialchars($systemUrl, ENT_QUOTES, 'UTF-8');
|
||||||
|
$serviceId = (int) $serviceId;
|
||||||
|
$cacheV = time();
|
||||||
|
|
||||||
return <<<EOT
|
return <<<EOT
|
||||||
<link href="${systemUrl}modules/servers/VirtFusionDirect/templates/css/module.css?v=20260207" rel="stylesheet">
|
<link href="${systemUrl}modules/servers/VirtFusionDirect/templates/css/module.css?v=${cacheV}" rel="stylesheet">
|
||||||
<script src="${systemUrl}modules/servers/VirtFusionDirect/templates/js/module.js?v=20260207"></script>
|
<script src="${systemUrl}modules/servers/VirtFusionDirect/templates/js/module.js?v=${cacheV}"></script>
|
||||||
<div id="vf-loader" class="vf-loader">
|
<div id="vf-loader" class="vf-loader">
|
||||||
<div id="vf-loading"></div>
|
<div id="vf-loading"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -114,4 +149,4 @@ EOT;
|
|||||||
<script>vfServerDataAdmin("${serviceId}","${systemUrl}");</script>
|
<script>vfServerDataAdmin("${serviceId}","${systemUrl}");</script>
|
||||||
EOT;
|
EOT;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
184
modules/servers/VirtFusionDirect/lib/Cache.php
Normal file
184
modules/servers/VirtFusionDirect/lib/Cache.php
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
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:';
|
||||||
|
|
||||||
|
/** @var \Redis|null */
|
||||||
|
private static $redis = null;
|
||||||
|
|
||||||
|
/** @var bool|null */
|
||||||
|
private static $redisAvailable = null;
|
||||||
|
|
||||||
|
/** @var string */
|
||||||
|
private static $fileDir = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try to connect to Redis. Returns the connection or null.
|
||||||
|
*/
|
||||||
|
private static function redis(): ?\Redis
|
||||||
|
{
|
||||||
|
if (self::$redisAvailable === false) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self::$redis !== null) {
|
||||||
|
return self::$redis;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! extension_loaded('redis')) {
|
||||||
|
self::$redisAvailable = false;
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the filesystem cache directory, creating it if needed.
|
||||||
|
*/
|
||||||
|
private static function fileDir(): string
|
||||||
|
{
|
||||||
|
if (self::$fileDir !== '') {
|
||||||
|
return self::$fileDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
$dir = sys_get_temp_dir() . '/vfd_cache';
|
||||||
|
if (! is_dir($dir)) {
|
||||||
|
@mkdir($dir, 0700, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
self::$fileDir = $dir;
|
||||||
|
|
||||||
|
return $dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a cache key to a safe filename.
|
||||||
|
*/
|
||||||
|
private static function filePath(string $key): string
|
||||||
|
{
|
||||||
|
return self::fileDir() . '/' . md5($key) . '.cache';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a cached value.
|
||||||
|
*
|
||||||
|
* @param string $key
|
||||||
|
* @return mixed|null Returns null on miss
|
||||||
|
*/
|
||||||
|
public static function get($key)
|
||||||
|
{
|
||||||
|
// Try Redis first
|
||||||
|
$redis = self::redis();
|
||||||
|
if ($redis) {
|
||||||
|
try {
|
||||||
|
$data = $redis->get(self::PREFIX . $key);
|
||||||
|
if ($data !== false) {
|
||||||
|
return json_decode($data, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Fall through to file cache
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// File cache fallback
|
||||||
|
$path = self::filePath($key);
|
||||||
|
if (! file_exists($path)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$raw = @file_get_contents($path);
|
||||||
|
if ($raw === false) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$entry = json_decode($raw, true);
|
||||||
|
if (! $entry || ! isset($entry['expires']) || ! isset($entry['data'])) {
|
||||||
|
@unlink($path);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($entry['expires'] < time()) {
|
||||||
|
@unlink($path);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $entry['data'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store a value in cache.
|
||||||
|
*
|
||||||
|
* @param string $key
|
||||||
|
* @param mixed $value
|
||||||
|
* @param int $ttl Time-to-live in seconds
|
||||||
|
*/
|
||||||
|
public static function set($key, $value, $ttl = 300)
|
||||||
|
{
|
||||||
|
// Try Redis first
|
||||||
|
$redis = self::redis();
|
||||||
|
if ($redis) {
|
||||||
|
try {
|
||||||
|
$redis->setex(self::PREFIX . $key, $ttl, json_encode($value));
|
||||||
|
|
||||||
|
return;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Fall through to file cache
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// File cache fallback with atomic write (race condition safe)
|
||||||
|
$path = self::filePath($key);
|
||||||
|
$tmp = $path . '.' . getmypid() . '.tmp';
|
||||||
|
$entry = json_encode(['expires' => time() + $ttl, 'data' => $value]);
|
||||||
|
|
||||||
|
if (@file_put_contents($tmp, $entry, LOCK_EX) !== false) {
|
||||||
|
@rename($tmp, $path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a cached value.
|
||||||
|
*
|
||||||
|
* @param string $key
|
||||||
|
*/
|
||||||
|
public static function forget($key)
|
||||||
|
{
|
||||||
|
$redis = self::redis();
|
||||||
|
if ($redis) {
|
||||||
|
try {
|
||||||
|
$redis->del(self::PREFIX . $key);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Continue to file cleanup
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$path = self::filePath($key);
|
||||||
|
if (file_exists($path)) {
|
||||||
|
@unlink($path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,17 +2,33 @@
|
|||||||
|
|
||||||
namespace WHMCS\Module\Server\VirtFusionDirect;
|
namespace WHMCS\Module\Server\VirtFusionDirect;
|
||||||
|
|
||||||
use JsonException;
|
|
||||||
use WHMCS\Database\Capsule as DB;
|
use WHMCS\Database\Capsule as DB;
|
||||||
use WHMCS\User\User;
|
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
|
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;
|
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()
|
public function __construct()
|
||||||
{
|
{
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
@@ -20,193 +36,298 @@ class ConfigureService extends Module
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $packageName
|
* Find a VirtFusion package ID by its name via the API.
|
||||||
* @return int|null
|
*
|
||||||
* @throws JsonException
|
* 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
|
public function fetchPackageId(string $packageName): ?int
|
||||||
{
|
{
|
||||||
if (!$this->cp) return null;
|
try {
|
||||||
|
$cacheKey = 'pkg_name:' . md5($packageName);
|
||||||
$request = $this->initCurl($this->cp['token']);
|
$cached = Cache::get($cacheKey);
|
||||||
|
if ($cached !== null) {
|
||||||
$response = $request->get(
|
return $cached;
|
||||||
sprintf("%s/packages", $this->cp['url'])
|
|
||||||
);
|
|
||||||
|
|
||||||
$packages = $this->decodeResponseFromJson($response);
|
|
||||||
|
|
||||||
foreach ($packages['data'] as $package) {
|
|
||||||
if ($package['name'] === $packageName && $package['enabled'] === true) {
|
|
||||||
return $package['id'];
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
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
|
* Get the VirtFusion package ID from a WHMCS product's config option.
|
||||||
* @return int|null
|
*
|
||||||
|
* 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
|
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 null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (int)$product->configoption2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param int $serverPackageId
|
* Fetch the available OS templates for a given VirtFusion server package.
|
||||||
* @return array|null
|
*
|
||||||
* @throws JsonException
|
* 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
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$this->cp) return null;
|
|
||||||
|
|
||||||
$request = $this->initCurl($this->cp['token']);
|
|
||||||
|
|
||||||
$response = $request->get(
|
|
||||||
sprintf("%s/media/templates/fromServerPackageSpec/%d", $this->cp['url'], $serverPackageId)
|
|
||||||
);
|
|
||||||
|
|
||||||
return $this->decodeResponseFromJson($response);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param User|null $user
|
* Get the SSH keys registered for a VirtFusion user.
|
||||||
* @return array|null
|
*
|
||||||
* @throws JsonException
|
* 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
|
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;
|
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
|
* Look up a VirtFusion user by WHMCS external relation ID.
|
||||||
* @return array|null
|
*
|
||||||
* @throws JsonException
|
* 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
|
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(
|
$response = $this->decodeResponseFromJson($request->get(
|
||||||
sprintf("%s/users/%d/byExtRelation", $this->cp['url'], $id)
|
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
|
* Trigger OS installation on a newly created VirtFusion server.
|
||||||
* @param array $vars
|
*
|
||||||
* @param int|null $vfUserId VirtFusion user ID (for creating SSH keys from raw public key)
|
* Posts a build request to the VirtFusion API with the selected OS template
|
||||||
* @return bool
|
* 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
|
public function initServerBuild(int $id, array $vars, ?int $vfUserId = null): bool
|
||||||
{
|
{
|
||||||
if (!$this->cp) return false;
|
try {
|
||||||
|
if (! $this->cp) {
|
||||||
$request = $this->initCurl($this->cp['token']);
|
return false;
|
||||||
|
|
||||||
// Generate a random 8 character hostname
|
|
||||||
$hostname = substr(str_shuffle('abcdefghijklmnopqrstuvwxyz'), 0, 8);
|
|
||||||
|
|
||||||
$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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$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.
|
* Create an SSH key for a VirtFusion user from a raw public key string.
|
||||||
*
|
*
|
||||||
* @param int $userId VirtFusion user ID
|
* @param int $userId VirtFusion user ID
|
||||||
* @param string $publicKey Raw SSH public key (ssh-rsa ..., ssh-ed25519 ..., etc.)
|
* @param string $publicKey Raw SSH public key (ssh-rsa ..., ssh-ed25519 ..., etc.)
|
||||||
* @return int|null Created key ID or null on failure
|
* @return int|null Created key ID or null on failure
|
||||||
*/
|
*/
|
||||||
public function createUserSshKey(int $userId, string $publicKey): ?int
|
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 = [
|
$keyData = [
|
||||||
'userId' => $userId,
|
'userId' => $userId,
|
||||||
'name' => 'WHMCS-' . date('Y-m-d'),
|
'name' => 'WHMCS-' . date('Y-m-d'),
|
||||||
'publicKey' => trim($publicKey),
|
'publicKey' => trim($publicKey),
|
||||||
];
|
];
|
||||||
|
|
||||||
$request->addOption(CURLOPT_POSTFIELDS, json_encode($keyData));
|
$request->addOption(CURLOPT_POSTFIELDS, json_encode($keyData));
|
||||||
$response = $request->post($this->cp['url'] . '/ssh_keys');
|
$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');
|
$httpCode = $request->getRequestInfo('http_code');
|
||||||
if ($httpCode == 200 || $httpCode == 201) {
|
if ($httpCode == 200 || $httpCode == 201) {
|
||||||
$data = json_decode($response, true);
|
$data = json_decode($response, true);
|
||||||
return $data['data']['id'] ?? null;
|
|
||||||
|
return $data['data']['id'] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::insert(__FUNCTION__, [], $e->getMessage());
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,22 @@
|
|||||||
|
|
||||||
namespace WHMCS\Module\Server\VirtFusionDirect;
|
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
|
class Curl
|
||||||
{
|
{
|
||||||
|
/** @var resource|\CurlHandle cURL handle */
|
||||||
private $ch;
|
private $ch;
|
||||||
|
|
||||||
|
/** @var array Response info and parsed header data collected after exec */
|
||||||
private $data;
|
private $data;
|
||||||
|
|
||||||
|
/** @var array User-supplied cURL options that override defaults */
|
||||||
private $customOptions = [];
|
private $customOptions = [];
|
||||||
|
|
||||||
|
/** @var array Default cURL options applied to every request */
|
||||||
private $defaultOptions = [
|
private $defaultOptions = [
|
||||||
CURLOPT_SSL_VERIFYPEER => true,
|
CURLOPT_SSL_VERIFYPEER => true,
|
||||||
CURLOPT_SSL_VERIFYHOST => 2,
|
CURLOPT_SSL_VERIFYHOST => 2,
|
||||||
@@ -18,32 +29,17 @@ class Curl
|
|||||||
CURLOPT_CONNECTTIMEOUT => 10,
|
CURLOPT_CONNECTTIMEOUT => 10,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/** Initialise the cURL handle. */
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->ch = curl_init();
|
$this->ch = curl_init();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function useCookies()
|
|
||||||
{
|
|
||||||
$cookiesFile = tempnam(sys_get_temp_dir(), 'virtfusion_cookies');
|
|
||||||
$this->defaultOptions[CURLOPT_COOKIEFILE] = $cookiesFile;
|
|
||||||
$this->defaultOptions[CURLOPT_COOKIEJAR] = $cookiesFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setLog()
|
|
||||||
{
|
|
||||||
$log = fopen(__DIR__ . '/CURL.log', 'a');
|
|
||||||
if ($log) {
|
|
||||||
fwrite($log, str_repeat('=', 80) . PHP_EOL);
|
|
||||||
$this->addOption(CURLOPT_STDERR, $log);
|
|
||||||
$this->addOption(CURLOPT_VERBOSE, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param $name
|
* Set a custom cURL option, overriding the defaults.
|
||||||
* @param $value
|
*
|
||||||
|
* @param int $name A CURLOPT_* constant
|
||||||
|
* @param mixed $value The option value
|
||||||
*/
|
*/
|
||||||
public function addOption($name, $value)
|
public function addOption($name, $value)
|
||||||
{
|
{
|
||||||
@@ -51,8 +47,10 @@ class Curl
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param null $url
|
* Execute a PUT request.
|
||||||
* @return bool|string|void
|
*
|
||||||
|
* @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)
|
public function put($url = null)
|
||||||
{
|
{
|
||||||
@@ -60,8 +58,10 @@ class Curl
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param null $url
|
* Execute a PATCH request.
|
||||||
* @return bool|string|void
|
*
|
||||||
|
* @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)
|
public function patch($url = null)
|
||||||
{
|
{
|
||||||
@@ -69,14 +69,18 @@ class Curl
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param $method
|
* Set the HTTP method and URL, then execute the request.
|
||||||
* @param $url
|
*
|
||||||
* @return bool|string|void
|
* @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)
|
private function send($method, $url)
|
||||||
{
|
{
|
||||||
if ($url === null) {
|
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');
|
throw new \RuntimeException('Curl: empty URL provided');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -87,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()
|
private function exec()
|
||||||
{
|
{
|
||||||
@@ -111,6 +117,7 @@ class Curl
|
|||||||
return $response;
|
return $response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Merge custom and default cURL options and apply them to the handle. */
|
||||||
private function setOptions()
|
private function setOptions()
|
||||||
{
|
{
|
||||||
if (isset($this->customOptions[CURLOPT_HEADER]) && $this->customOptions[CURLOPT_HEADER]) {
|
if (isset($this->customOptions[CURLOPT_HEADER]) && $this->customOptions[CURLOPT_HEADER]) {
|
||||||
@@ -122,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)
|
private function processHeaders(&$data)
|
||||||
{
|
{
|
||||||
@@ -133,15 +142,17 @@ class Curl
|
|||||||
|
|
||||||
$tmp = explode("\r\n", $this->data['info']['response_header']);
|
$tmp = explode("\r\n", $this->data['info']['response_header']);
|
||||||
$this->data['data']['Message'] = $tmp[0];
|
$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);
|
$string = explode(': ', $tmp[$i], 2);
|
||||||
$this->data['data'][$string[0]] = $string[1];
|
$this->data['data'][$string[0]] = $string[1];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param null $url
|
* Execute a GET request.
|
||||||
* @return bool|string|void
|
*
|
||||||
|
* @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)
|
public function get($url = null)
|
||||||
{
|
{
|
||||||
@@ -149,8 +160,10 @@ class Curl
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param null $url
|
* Execute a DELETE request.
|
||||||
* @return bool|string|void
|
*
|
||||||
|
* @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)
|
public function delete($url = null)
|
||||||
{
|
{
|
||||||
@@ -158,8 +171,10 @@ class Curl
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param null $url
|
* Execute a POST request.
|
||||||
* @return bool|string|void
|
*
|
||||||
|
* @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)
|
public function post($url = null)
|
||||||
{
|
{
|
||||||
@@ -167,17 +182,10 @@ class Curl
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param null $url
|
* Return curl_getinfo data for the completed request.
|
||||||
* @return bool|string|void
|
*
|
||||||
*/
|
* @param string|false $param A specific info key to retrieve, or false for the full array
|
||||||
public function head($url = null)
|
* @return mixed|null The requested info value, the full info array, or null if the key is absent
|
||||||
{
|
|
||||||
return $this->send('HEAD', $url);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param false $param
|
|
||||||
* @return mixed|null
|
|
||||||
*/
|
*/
|
||||||
public function getRequestInfo($param = false)
|
public function getRequestInfo($param = false)
|
||||||
{
|
{
|
||||||
@@ -189,9 +197,11 @@ class Curl
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param $what
|
* Retrieve a single item from the internal data store by section and key.
|
||||||
* @param $name
|
*
|
||||||
* @return mixed|null
|
* @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)
|
private function getDataItem($what, $name)
|
||||||
{
|
{
|
||||||
@@ -201,17 +211,4 @@ class Curl
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param false $param
|
|
||||||
* @return mixed|null
|
|
||||||
*/
|
|
||||||
public function getHeadersData($param = false)
|
|
||||||
{
|
|
||||||
if ($param) {
|
|
||||||
return $this->getDataItem('data', $param);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->data['data'];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,13 +4,28 @@ namespace WHMCS\Module\Server\VirtFusionDirect;
|
|||||||
|
|
||||||
use WHMCS\Database\Capsule as DB;
|
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
|
class Database
|
||||||
{
|
{
|
||||||
const SYSTEM_TABLE = 'mod_virtfusion_direct';
|
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()
|
public static function schema()
|
||||||
{
|
{
|
||||||
if (!DB::schema()->hasTable(self::SYSTEM_TABLE)) {
|
if (! DB::schema()->hasTable(self::SYSTEM_TABLE)) {
|
||||||
try {
|
try {
|
||||||
DB::schema()->create(self::SYSTEM_TABLE, function ($table) {
|
DB::schema()->create(self::SYSTEM_TABLE, function ($table) {
|
||||||
$table->unsignedBigInteger('service_id')->nullable()->default(null)->index();
|
$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 {
|
try {
|
||||||
DB::schema()->table(self::SYSTEM_TABLE, function ($table) {
|
DB::schema()->table(self::SYSTEM_TABLE, function ($table) {
|
||||||
$table->longText('server_object')->nullable()->default(null);
|
$table->longText('server_object')->nullable()->default(null);
|
||||||
@@ -31,92 +46,283 @@ class Database
|
|||||||
Log::insert(__FUNCTION__, [], $e->getMessage());
|
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)
|
public static function getWhmcsServer(int $server, $any = false)
|
||||||
{
|
{
|
||||||
if ($server) {
|
try {
|
||||||
return DB::table('tblservers')->where('type', 'VirtFusionDirect')->where('id', $server)->first();
|
if ($server) {
|
||||||
}
|
return DB::table('tblservers')->where('type', 'VirtFusionDirect')->where('id', $server)->first();
|
||||||
|
}
|
||||||
|
|
||||||
if ($any) {
|
if ($any) {
|
||||||
return DB::table('tblservers')->where('type', 'VirtFusionDirect')->where('disabled', 0)->first();
|
return DB::table('tblservers')->where('type', 'VirtFusionDirect')->where('disabled', 0)->first();
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::insert(__FUNCTION__, [], $e->getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
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)
|
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()
|
public static function getSystemUrl()
|
||||||
{
|
{
|
||||||
$url = DB::table('tblconfiguration')->where('setting', '=', 'SystemURL')->first();
|
try {
|
||||||
if (!$url) return '';
|
$url = DB::table('tblconfiguration')->where('setting', '=', 'SystemURL')->first();
|
||||||
return $url->value;
|
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)
|
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)
|
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)
|
public static function updateSystemServiceServerId(int $serviceId, int $serverId)
|
||||||
{
|
{
|
||||||
|
try {
|
||||||
DB::table(self::SYSTEM_TABLE)->updateOrInsert(
|
DB::table(self::SYSTEM_TABLE)->updateOrInsert(
|
||||||
[
|
[
|
||||||
"service_id" => $serviceId
|
'service_id' => $serviceId,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'server_id' => $serverId
|
'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)
|
public static function updateWhmcsServiceParams(int $serviceId, $data)
|
||||||
{
|
{
|
||||||
if (count($data)) {
|
try {
|
||||||
foreach ($data as $key => $items) {
|
if (count($data)) {
|
||||||
DB::table($key)->where('id', $serviceId)->update($items);
|
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)
|
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)
|
return false;
|
||||||
{
|
|
||||||
DB::table(self::SYSTEM_TABLE)->where('service_id', $serviceId)->delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function updateSystemServiceServerObject(int $serviceId, $data)
|
|
||||||
{
|
|
||||||
DB::table(self::SYSTEM_TABLE)->where('service_id', $serviceId)->update(['server_object' => json_encode($data, JSON_PRETTY_PRINT)]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function systemOnServerCreate(int $serviceId, $data)
|
|
||||||
{
|
|
||||||
if (DB::table(self::SYSTEM_TABLE)->where('service_id', $serviceId)->exists()) {
|
|
||||||
DB::table(self::SYSTEM_TABLE)->where('service_id', $serviceId)->update(['server_id' => $data->data->id, 'server_object' => json_encode($data, JSON_PRETTY_PRINT)]);
|
|
||||||
} else {
|
|
||||||
DB::table(self::SYSTEM_TABLE)->insert(['service_id' => $serviceId, 'server_id' => $data->data->id, 'server_object' => json_encode($data, JSON_PRETTY_PRINT)]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function getSystemService(int $serviceId)
|
/**
|
||||||
|
* 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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,22 +2,22 @@
|
|||||||
|
|
||||||
namespace WHMCS\Module\Server\VirtFusionDirect;
|
namespace WHMCS\Module\Server\VirtFusionDirect;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thin wrapper around the WHMCS logModuleCall() function for module-level logging.
|
||||||
|
*/
|
||||||
class Log
|
class Log
|
||||||
{
|
{
|
||||||
const LOG_MODULE = 'VirtFusionDirect';
|
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)
|
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);
|
logModuleCall(self::LOG_MODULE, $action, $requestString, $responseData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,12 @@
|
|||||||
|
|
||||||
namespace WHMCS\Module\Server\VirtFusionDirect;
|
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
|
class ModuleFunctions extends Module
|
||||||
{
|
{
|
||||||
public function __construct()
|
public function __construct()
|
||||||
@@ -10,13 +16,13 @@ class ModuleFunctions extends Module
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Provision a new VirtFusion server for a WHMCS service.
|
||||||
*
|
*
|
||||||
* CREATE SERVER
|
* Ensures a matching VirtFusion user exists (creating one if needed), then creates
|
||||||
*
|
* the server and triggers the OS build via ConfigureService::initServerBuild().
|
||||||
* 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.
|
|
||||||
*
|
*
|
||||||
|
* @param array $params WHMCS service parameters
|
||||||
|
* @return string 'success' or an error message
|
||||||
*/
|
*/
|
||||||
public function createAccount($params)
|
public function createAccount($params)
|
||||||
{
|
{
|
||||||
@@ -33,9 +39,9 @@ class ModuleFunctions extends Module
|
|||||||
* If no VirtFusionDirect control server exists, cancel the create account action.
|
* If no VirtFusionDirect control server exists, cancel the create account action.
|
||||||
*/
|
*/
|
||||||
$server = $params['serverid'] ?: false;
|
$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.';
|
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']);
|
$user = Database::getUser($params['userid']);
|
||||||
|
|
||||||
if (!$user) {
|
if (! $user) {
|
||||||
return 'WHMCS user not found for ID ' . (int) $params['userid'];
|
return 'WHMCS user not found for ID ' . (int) $params['userid'];
|
||||||
}
|
}
|
||||||
|
|
||||||
$request = $this->initCurl($cp['token']);
|
$request = $this->initCurl($cp['token']);
|
||||||
|
|
||||||
$userData = [
|
$userData = [
|
||||||
"name" => $user->firstname . ' ' . $user->lastname,
|
'name' => $user->firstname . ' ' . $user->lastname,
|
||||||
"email" => $user->email,
|
'email' => $user->email,
|
||||||
"extRelationId" => $user->id,
|
'extRelationId' => $user->id,
|
||||||
];
|
];
|
||||||
|
|
||||||
// Enable self-service billing if configured
|
// 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.
|
* A user is available. We can now attempt to create a server.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
$configOptionDefaultNaming = [
|
$configOptionDefaultNaming = [
|
||||||
'ipv4' => 'IPv4',
|
'ipv4' => 'IPv4',
|
||||||
'packageId' => 'Package',
|
'packageId' => 'Package',
|
||||||
@@ -122,10 +127,10 @@ class ModuleFunctions extends Module
|
|||||||
}
|
}
|
||||||
|
|
||||||
$options = [
|
$options = [
|
||||||
"packageId" => (int) $params['configoption2'],
|
'packageId' => (int) $params['configoption2'],
|
||||||
"userId" => $data->data->id,
|
'userId' => $data->data->id,
|
||||||
"hypervisorId" => (int) $params['configoption1'],
|
'hypervisorId' => (int) $params['configoption1'],
|
||||||
"ipv4" => (int) $params['configoption3'],
|
'ipv4' => (int) $params['configoption3'],
|
||||||
];
|
];
|
||||||
|
|
||||||
if (array_key_exists('configoptions', $params)) {
|
if (array_key_exists('configoptions', $params)) {
|
||||||
@@ -159,7 +164,7 @@ class ModuleFunctions extends Module
|
|||||||
$this->updateWhmcsServiceParamsOnServerObject($params['serviceid'], $data);
|
$this->updateWhmcsServiceParamsOnServerObject($params['serviceid'], $data);
|
||||||
|
|
||||||
// If the server is created successfully, we can initialize the server build.
|
// 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;
|
$vfUserId = isset($data->data->owner->id) ? (int) $data->data->owner->id : null;
|
||||||
$cs->initServerBuild($data->data->id, $params, $vfUserId);
|
$cs->initServerBuild($data->data->id, $params, $vfUserId);
|
||||||
|
|
||||||
@@ -171,328 +176,440 @@ class ModuleFunctions extends Module
|
|||||||
if (isset($data->msg)) {
|
if (isset($data->msg)) {
|
||||||
return $data->msg;
|
return $data->msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'Server creation failed. VirtFusion API returned HTTP ' . $request->getRequestInfo('http_code');
|
return 'Server creation failed. VirtFusion API returned HTTP ' . $request->getRequestInfo('http_code');
|
||||||
}
|
}
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
Log::insert(__FUNCTION__, $params, $e->getMessage());
|
Log::insert(__FUNCTION__, $params, $e->getMessage());
|
||||||
|
|
||||||
return $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
|
* Updates the package via the API, then individually adjusts memory, CPU, and bandwidth
|
||||||
* @return string
|
* if those configurable options are present.
|
||||||
|
*
|
||||||
|
* @param array $params WHMCS service parameters
|
||||||
|
* @return string 'success' or an error message
|
||||||
*/
|
*/
|
||||||
public function changePackage($params)
|
public function changePackage($params)
|
||||||
{
|
{
|
||||||
$service = Database::getSystemService($params['serviceid']);
|
try {
|
||||||
|
$service = Database::getSystemService($params['serviceid']);
|
||||||
|
|
||||||
if ($service) {
|
if ($service) {
|
||||||
$whmcsService = Database::getWhmcsService($params['serviceid']);
|
$whmcsService = Database::getWhmcsService($params['serviceid']);
|
||||||
if (!$whmcsService) return 'WHMCS service record not found.';
|
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';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($configOptionDefaultNaming as $resource => $optionName) {
|
$cp = $this->getCP($whmcsService->server);
|
||||||
$currentOption = array_key_exists($resource, $configOptionCustomNaming) ? $configOptionCustomNaming[$resource] : $optionName;
|
if (! $cp) {
|
||||||
if (isset($params['configoptions'][$currentOption]) && is_numeric($params['configoptions'][$currentOption])) {
|
return 'No control server found.';
|
||||||
$value = (int) $params['configoptions'][$currentOption];
|
}
|
||||||
if ($resource === 'memory' && $value < 1024) {
|
|
||||||
$value = $value * 1024;
|
$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
|
* 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.
|
||||||
* 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.
|
|
||||||
*
|
*
|
||||||
|
* @param array $params WHMCS service parameters
|
||||||
|
* @return string 'success' or an error message
|
||||||
*/
|
*/
|
||||||
public function terminateAccount($params)
|
public function terminateAccount($params)
|
||||||
{
|
{
|
||||||
$service = Database::getSystemService($params['serviceid']);
|
try {
|
||||||
|
$service = Database::getSystemService($params['serviceid']);
|
||||||
|
|
||||||
if ($service) {
|
if ($service) {
|
||||||
|
|
||||||
$whmcsService = Database::getWhmcsService($params['serviceid']);
|
$whmcsService = Database::getWhmcsService($params['serviceid']);
|
||||||
if (!$whmcsService) return 'WHMCS service record not found.';
|
if (! $whmcsService) {
|
||||||
|
return 'WHMCS service record not found.';
|
||||||
|
}
|
||||||
|
|
||||||
$cp = $this->getCP($whmcsService->server);
|
$cp = $this->getCP($whmcsService->server);
|
||||||
if (!$cp) return 'No control server found.';
|
if (! $cp) {
|
||||||
|
return 'No control server found.';
|
||||||
|
}
|
||||||
|
|
||||||
$request = $this->initCurl($cp['token']);
|
$request = $this->initCurl($cp['token']);
|
||||||
$data = $request->delete($cp['url'] . '/servers/' . (int) $service->server_id);
|
$data = $request->delete($cp['url'] . '/servers/' . (int) $service->server_id);
|
||||||
$data = json_decode($data);
|
$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:
|
case 204:
|
||||||
Database::deleteSystemService($params['serviceid']);
|
Database::deleteSystemService($params['serviceid']);
|
||||||
$this->updateWhmcsServiceParamsOnDestroy($params['serviceid']);
|
$this->updateWhmcsServiceParamsOnDestroy($params['serviceid']);
|
||||||
return 'success';
|
|
||||||
|
|
||||||
case 404:
|
return 'success';
|
||||||
if (isset($data->msg)) {
|
|
||||||
if ($data->msg == 'server not found') {
|
case 404:
|
||||||
Database::deleteSystemService($params['serviceid']);
|
if (isset($data->msg)) {
|
||||||
return 'success';
|
if ($data->msg == 'server not found') {
|
||||||
|
Database::deleteSystemService($params['serviceid']);
|
||||||
|
|
||||||
|
return 'success';
|
||||||
|
} else {
|
||||||
|
return 'VirtFusion returned 404: ' . $data->msg;
|
||||||
|
}
|
||||||
} else {
|
} 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:
|
default:
|
||||||
return 'Termination request failed. VirtFusion API returned HTTP ' . $request->getRequestInfo('http_code');
|
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
|
* Returns 'success' whether the server is suspended immediately or queued for suspension.
|
||||||
*
|
|
||||||
* 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.
|
|
||||||
*
|
*
|
||||||
|
* @param array $params WHMCS service parameters
|
||||||
|
* @return string 'success' or an error message
|
||||||
*/
|
*/
|
||||||
public function suspendAccount($params)
|
public function suspendAccount($params)
|
||||||
{
|
{
|
||||||
$service = Database::getSystemService($params['serviceid']);
|
try {
|
||||||
|
$service = Database::getSystemService($params['serviceid']);
|
||||||
|
|
||||||
if ($service) {
|
if ($service) {
|
||||||
|
|
||||||
$whmcsService = Database::getWhmcsService($params['serviceid']);
|
$whmcsService = Database::getWhmcsService($params['serviceid']);
|
||||||
if (!$whmcsService) return 'WHMCS service record not found.';
|
if (! $whmcsService) {
|
||||||
|
return 'WHMCS service record not found.';
|
||||||
|
}
|
||||||
|
|
||||||
$cp = $this->getCP($whmcsService->server);
|
$cp = $this->getCP($whmcsService->server);
|
||||||
if (!$cp) return 'No control server found.';
|
if (! $cp) {
|
||||||
|
return 'No control server found.';
|
||||||
|
}
|
||||||
|
|
||||||
$request = $this->initCurl($cp['token']);
|
$request = $this->initCurl($cp['token']);
|
||||||
$data = $request->post($cp['url'] . '/servers/' . (int) $service->server_id . '/suspend');
|
$data = $request->post($cp['url'] . '/servers/' . (int) $service->server_id . '/suspend');
|
||||||
$data = json_decode($data);
|
$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:
|
case 204:
|
||||||
return 'success';
|
return 'success';
|
||||||
|
|
||||||
case 404:
|
case 404:
|
||||||
if (isset($data->msg)) {
|
if (isset($data->msg)) {
|
||||||
if ($data->msg == 'server not found') {
|
if ($data->msg == 'server not found') {
|
||||||
Database::deleteSystemService($params['serviceid']);
|
Database::deleteSystemService($params['serviceid']);
|
||||||
return 'success';
|
|
||||||
|
return 'success';
|
||||||
|
} else {
|
||||||
|
return 'VirtFusion returned 404: ' . $data->msg;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return 'VirtFusion returned 404: ' . $data->msg;
|
return 'VirtFusion returned 404 without details. The API may be unavailable.';
|
||||||
}
|
}
|
||||||
} else {
|
case 423:
|
||||||
return 'VirtFusion returned 404 without details. The API may be unavailable.';
|
if (isset($data->msg)) {
|
||||||
}
|
return $data->msg;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
} 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 'The server is currently locked. Please try again later.';
|
||||||
return 'Unsuspend request failed. VirtFusion API returned HTTP ' . $request->getRequestInfo('http_code');
|
|
||||||
|
default:
|
||||||
|
return 'Suspend request failed. VirtFusion API returned HTTP ' . $request->getRequestInfo('http_code');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return 'Service not found in module database.';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function adminServicesTabFields($params)
|
return 'Service not found in module database.';
|
||||||
{
|
} catch (\Exception $e) {
|
||||||
$serverId = '';
|
Log::insert(__FUNCTION__, $params, $e->getMessage());
|
||||||
$serverObject = '';
|
|
||||||
|
|
||||||
$service = Database::getSystemService($params['serviceid']);
|
return $e->getMessage();
|
||||||
$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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
* Updates both the module database record and the WHMCS service fields (IP, username, etc.).
|
||||||
* @return string 'success' or error message
|
*
|
||||||
|
* @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)
|
public function validateServerConfig($params)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$server = $params['serverid'] ?: false;
|
$server = $params['serverid'] ?: false;
|
||||||
$cp = $this->getCP($server, !$server);
|
$cp = $this->getCP($server, ! $server);
|
||||||
|
|
||||||
if (!$cp) {
|
if (! $cp) {
|
||||||
return 'No Control server found.';
|
return 'No Control server found.';
|
||||||
}
|
}
|
||||||
|
|
||||||
$options = [
|
$options = [
|
||||||
"packageId" => (int) $params['configoption2'],
|
'packageId' => (int) $params['configoption2'],
|
||||||
"hypervisorId" => (int) $params['configoption1'],
|
'hypervisorId' => (int) $params['configoption1'],
|
||||||
"ipv4" => (int) $params['configoption3'],
|
'ipv4' => (int) $params['configoption3'],
|
||||||
];
|
];
|
||||||
|
|
||||||
// We need a userId for dry run - use the service owner
|
// 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)
|
public function clientArea($params)
|
||||||
{
|
{
|
||||||
$serverHostname = null;
|
$serverHostname = null;
|
||||||
|
|||||||
@@ -2,8 +2,17 @@
|
|||||||
|
|
||||||
namespace WHMCS\Module\Server\VirtFusionDirect;
|
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
|
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)
|
public function process($data)
|
||||||
{
|
{
|
||||||
$server = json_decode(json_encode($data->data), true);
|
$server = json_decode(json_encode($data->data), true);
|
||||||
|
|||||||
@@ -9,16 +9,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Buttons */
|
/* Buttons */
|
||||||
.vf-button {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
padding: 0.95rem 1.5rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
.vf-button-small {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
padding: 0.75rem 1.3rem;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
.vf-spinner-margin {
|
.vf-spinner-margin {
|
||||||
margin-right: 7px;
|
margin-right: 7px;
|
||||||
}
|
}
|
||||||
@@ -84,9 +74,48 @@
|
|||||||
}
|
}
|
||||||
#vf-server-info-error {
|
#vf-server-info-error {
|
||||||
display: none;
|
display: none;
|
||||||
|
margin: 10px;
|
||||||
}
|
}
|
||||||
#vf-data-server-traffic-sep {
|
|
||||||
display: inline;
|
/* Skeleton Loading */
|
||||||
|
.vf-skeleton {
|
||||||
|
background: linear-gradient(90deg, #e9ecef 25%, #f4f4f4 50%, #e9ecef 75%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: vf-skeleton-pulse 1.5s ease-in-out infinite;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.vf-skeleton-line {
|
||||||
|
height: 14px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.vf-skeleton-line-short {
|
||||||
|
width: 60%;
|
||||||
|
}
|
||||||
|
.vf-skeleton-line-medium {
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
|
@keyframes vf-skeleton-pulse {
|
||||||
|
0% { background-position: 200% 0; }
|
||||||
|
100% { background-position: -200% 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Action Progress Banner */
|
||||||
|
#vf-action-progress {
|
||||||
|
background: #337ab7;
|
||||||
|
color: #fff;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
#vf-action-progress .spinner-border {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-width: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Loader */
|
/* Loader */
|
||||||
@@ -118,11 +147,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Error message spacing */
|
|
||||||
#vf-server-info-error {
|
|
||||||
margin: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Network / IP Management */
|
/* Network / IP Management */
|
||||||
.vf-ip-row {
|
.vf-ip-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -134,9 +158,226 @@
|
|||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
.vf-ip-remove {
|
/* Backup Timeline */
|
||||||
|
.vf-timeline {
|
||||||
|
position: relative;
|
||||||
|
padding-left: 20px;
|
||||||
|
border-left: 2px solid #dee2e6;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
.vf-timeline-item {
|
||||||
|
position: relative;
|
||||||
|
padding: 8px 0 8px 12px;
|
||||||
|
}
|
||||||
|
.vf-timeline-dot {
|
||||||
|
position: absolute;
|
||||||
|
left: -27px;
|
||||||
|
top: 12px;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid #fff;
|
||||||
|
}
|
||||||
|
.vf-timeline-dot-success {
|
||||||
|
background: #28a745;
|
||||||
|
}
|
||||||
|
.vf-timeline-dot-pending {
|
||||||
|
background: #ffc107;
|
||||||
|
}
|
||||||
|
.vf-timeline-content {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Server Name Dropdown */
|
||||||
|
#vf-name-dropdown {
|
||||||
|
position: relative;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-top: 4px;
|
||||||
|
max-width: 250px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
.vf-name-option {
|
||||||
|
padding: 6px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
.vf-name-option:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.vf-name-option:hover {
|
||||||
|
background: rgba(51,122,183,0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Copy to Clipboard */
|
||||||
|
.vf-ip-copy {
|
||||||
|
padding: 2px 5px;
|
||||||
|
line-height: 1;
|
||||||
|
color: #6c757d;
|
||||||
|
background: none;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.vf-ip-copy:hover {
|
||||||
|
color: #337ab7;
|
||||||
|
background: rgba(51,122,183,0.08);
|
||||||
|
border-color: rgba(51,122,183,0.2);
|
||||||
|
}
|
||||||
|
.vf-copy-tooltip {
|
||||||
|
position: absolute;
|
||||||
|
margin-left: 4px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #fff;
|
||||||
|
background: #28a745;
|
||||||
|
border-radius: 3px;
|
||||||
|
white-space: nowrap;
|
||||||
|
animation: vf-fade-in 0.2s ease;
|
||||||
|
}
|
||||||
|
@keyframes vf-fade-in {
|
||||||
|
from { opacity: 0; }
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* OS Template Gallery */
|
||||||
|
.vf-os-category-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
margin-top: 6px;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
user-select: none;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
.vf-os-category:first-child .vf-os-category-header {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
.vf-os-category-header:hover {
|
||||||
|
background: rgba(0,0,0,0.03);
|
||||||
|
}
|
||||||
|
.vf-os-category-icon {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
min-width: 28px;
|
||||||
|
min-height: 28px;
|
||||||
|
border-radius: 6px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.vf-os-category-icon img {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
.vf-os-category-arrow {
|
||||||
|
margin-left: auto;
|
||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
padding: 0.15rem 0.4rem;
|
color: #888;
|
||||||
|
}
|
||||||
|
.vf-os-grid {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 0;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.vf-os-card {
|
||||||
|
width: 120px;
|
||||||
|
border: 2px solid #dee2e6;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px 8px;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: border-color 0.15s, background-color 0.15s, box-shadow 0.15s;
|
||||||
|
}
|
||||||
|
.vf-os-card:hover {
|
||||||
|
border-color: #337ab7;
|
||||||
|
}
|
||||||
|
.vf-os-card-selected {
|
||||||
|
border-color: #337ab7;
|
||||||
|
background: rgba(51,122,183,0.06);
|
||||||
|
box-shadow: 0 0 0 1px rgba(51,122,183,0.3);
|
||||||
|
}
|
||||||
|
.vf-os-card-eol {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
.vf-os-icon {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 0 auto 6px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
overflow: hidden;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.vf-os-icon img {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
.vf-os-label {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
.vf-os-version {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #888;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
.vf-os-eol-badge {
|
||||||
|
display: inline-block;
|
||||||
|
background: #dc3545;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 1px 5px;
|
||||||
|
border-radius: 3px;
|
||||||
|
margin-top: 3px;
|
||||||
|
}
|
||||||
|
#vf-os-details {
|
||||||
|
border-top: 1px solid #dee2e6;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
.vf-os-search {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.vf-os-grid {
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.vf-os-card {
|
||||||
|
width: calc(50% - 3px);
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Resource panel */
|
/* Resource panel */
|
||||||
@@ -186,6 +427,38 @@
|
|||||||
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
|
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Toggle Switch */
|
||||||
|
.vf-toggle-input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.vf-toggle-switch {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: 36px;
|
||||||
|
height: 20px;
|
||||||
|
background: #ccc;
|
||||||
|
border-radius: 10px;
|
||||||
|
transition: background 0.2s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.vf-toggle-switch::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
left: 2px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
.vf-toggle-input:checked + .vf-toggle-switch {
|
||||||
|
background: #28a745;
|
||||||
|
}
|
||||||
|
.vf-toggle-input:checked + .vf-toggle-switch::after {
|
||||||
|
transform: translateX(16px);
|
||||||
|
}
|
||||||
|
|
||||||
/* Responsive adjustments */
|
/* Responsive adjustments */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.vf-power-buttons {
|
.vf-power-buttons {
|
||||||
|
|||||||
@@ -8,17 +8,62 @@
|
|||||||
* - Password reset
|
* - Password reset
|
||||||
* - Server rebuild
|
* - Server rebuild
|
||||||
* - OS template loading
|
* - OS template loading
|
||||||
|
* - Traffic statistics
|
||||||
|
* - Backup listing
|
||||||
|
* - VNC management
|
||||||
|
* - Server naming
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Shared Helpers
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
function vfUrl(systemUrl, serviceId, action, endpoint) {
|
||||||
|
return (systemUrl || "") + "modules/servers/VirtFusionDirect/" + (endpoint || "client") + ".php?serviceID=" + encodeURIComponent(serviceId) + "&action=" + encodeURIComponent(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
function vfShowAlert(alertDiv, type, message) {
|
||||||
|
alertDiv.removeClass("alert-danger alert-success alert-warning alert");
|
||||||
|
alertDiv.addClass("alert alert-" + type);
|
||||||
|
alertDiv.text(message);
|
||||||
|
alertDiv.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Progress Indicator
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
var _vfProgressTimer = null;
|
||||||
|
|
||||||
|
function vfShowProgress(label) {
|
||||||
|
var startTime = Date.now();
|
||||||
|
$("#vf-action-progress-text").text(label);
|
||||||
|
$("#vf-action-progress-timer").text("0s");
|
||||||
|
$("#vf-action-progress").show();
|
||||||
|
|
||||||
|
_vfProgressTimer = setInterval(function () {
|
||||||
|
var elapsed = Math.floor((Date.now() - startTime) / 1000);
|
||||||
|
$("#vf-action-progress-timer").text(elapsed + "s");
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function vfHideProgress() {
|
||||||
|
if (_vfProgressTimer) {
|
||||||
|
clearInterval(_vfProgressTimer);
|
||||||
|
_vfProgressTimer = null;
|
||||||
|
}
|
||||||
|
$("#vf-action-progress").hide();
|
||||||
|
}
|
||||||
|
|
||||||
function vfServerData(serviceId, systemUrl) {
|
function vfServerData(serviceId, systemUrl) {
|
||||||
$("#vf-server-info-error").hide();
|
$("#vf-server-info-error").hide();
|
||||||
$.ajax({
|
$.ajax({
|
||||||
type: "GET",
|
type: "GET",
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
url: systemUrl + "modules/servers/VirtFusionDirect/client.php?serviceID=" + encodeURIComponent(serviceId) + "&action=serverData"
|
url: vfUrl(systemUrl, serviceId, "serverData")
|
||||||
}).done(function (response) {
|
}).done(function (response) {
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
$("#vf-data-server-name").text(response.data.name);
|
$("#vf-rename-input").val(response.data.name);
|
||||||
$("#vf-data-server-hostname").text(response.data.hostname);
|
$("#vf-data-server-hostname").text(response.data.hostname);
|
||||||
$("#vf-data-server-memory").text(response.data.memory);
|
$("#vf-data-server-memory").text(response.data.memory);
|
||||||
$("#vf-data-server-traffic").text(response.data.traffic);
|
$("#vf-data-server-traffic").text(response.data.traffic);
|
||||||
@@ -93,9 +138,7 @@ function vfServerData(serviceId, systemUrl) {
|
|||||||
$.each(ipv4Arr, function (i, ip) {
|
$.each(ipv4Arr, function (i, ip) {
|
||||||
var row = $('<div class="vf-ip-row"></div>');
|
var row = $('<div class="vf-ip-row"></div>');
|
||||||
row.append('<span class="vf-ip-address">' + $('<span>').text(ip).html() + '</span>');
|
row.append('<span class="vf-ip-address">' + $('<span>').text(ip).html() + '</span>');
|
||||||
if (i > 0) {
|
row.append(vfCopyButton(ip));
|
||||||
row.append(' <button class="btn btn-sm btn-outline-danger vf-ip-remove" onclick="vfRemoveIP(\'' + serviceId + '\',\'' + systemUrl + '\',\'removeIPv4\',\'' + encodeURIComponent(ip) + '\')">Remove</button>');
|
|
||||||
}
|
|
||||||
ipv4List.append(row);
|
ipv4List.append(row);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -106,6 +149,7 @@ function vfServerData(serviceId, systemUrl) {
|
|||||||
$.each(ipv6Arr, function (i, subnet) {
|
$.each(ipv6Arr, function (i, subnet) {
|
||||||
var row = $('<div class="vf-ip-row"></div>');
|
var row = $('<div class="vf-ip-row"></div>');
|
||||||
row.append('<span class="vf-ip-address">' + $('<span>').text(subnet).html() + '</span>');
|
row.append('<span class="vf-ip-address">' + $('<span>').text(subnet).html() + '</span>');
|
||||||
|
row.append(vfCopyButton(subnet));
|
||||||
ipv6List.append(row);
|
ipv6List.append(row);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -133,7 +177,7 @@ function vfServerDataAdmin(serviceId, systemUrl) {
|
|||||||
$.ajax({
|
$.ajax({
|
||||||
type: "GET",
|
type: "GET",
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
url: systemUrl + "modules/servers/VirtFusionDirect/admin.php?serviceID=" + encodeURIComponent(serviceId) + "&action=serverData"
|
url: vfUrl(systemUrl, serviceId, "serverData", "admin")
|
||||||
}).done(function (response) {
|
}).done(function (response) {
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
$("#vf-data-server-name").text(response.data.name);
|
$("#vf-data-server-name").text(response.data.name);
|
||||||
@@ -148,7 +192,7 @@ function vfServerDataAdmin(serviceId, systemUrl) {
|
|||||||
$("#vf-server-info").show();
|
$("#vf-server-info").show();
|
||||||
} else {
|
} else {
|
||||||
$("#vf-server-info-error").show();
|
$("#vf-server-info-error").show();
|
||||||
$("#vf-server-info-error-message").text(response.errors);
|
$("#vf-server-info-error-message").text("Unable to retrieve server information.");
|
||||||
$("#vf-server-info").hide();
|
$("#vf-server-info").hide();
|
||||||
}
|
}
|
||||||
}).fail(function () {
|
}).fail(function () {
|
||||||
@@ -163,9 +207,9 @@ function vfUserPasswordReset(serviceId, systemUrl) {
|
|||||||
$("#vf-password-reset-error").hide();
|
$("#vf-password-reset-error").hide();
|
||||||
$("#vf-password-reset-success").hide();
|
$("#vf-password-reset-success").hide();
|
||||||
$.ajax({
|
$.ajax({
|
||||||
type: "GET",
|
type: "POST",
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
url: systemUrl + "modules/servers/VirtFusionDirect/client.php?serviceID=" + encodeURIComponent(serviceId) + "&action=resetPassword"
|
url: vfUrl(systemUrl, serviceId, "resetPassword")
|
||||||
}).done(function (response) {
|
}).done(function (response) {
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
$("#vf-password-reset-success").show();
|
$("#vf-password-reset-success").show();
|
||||||
@@ -189,7 +233,7 @@ function vfLoginAsServerOwner(serviceId, systemUrl, newWindow) {
|
|||||||
$.ajax({
|
$.ajax({
|
||||||
type: "GET",
|
type: "GET",
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
url: systemUrl + "modules/servers/VirtFusionDirect/client.php?serviceID=" + encodeURIComponent(serviceId) + "&action=loginAsServerOwner"
|
url: vfUrl(systemUrl, serviceId, "loginAsServerOwner")
|
||||||
}).done(function (response) {
|
}).done(function (response) {
|
||||||
if (response.success && response.token_url) {
|
if (response.success && response.token_url) {
|
||||||
if (newWindow) {
|
if (newWindow) {
|
||||||
@@ -236,48 +280,175 @@ function vfPowerAction(serviceId, systemUrl, action) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
type: "GET",
|
type: "POST",
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
url: systemUrl + "modules/servers/VirtFusionDirect/client.php?serviceID=" + encodeURIComponent(serviceId) + "&action=powerAction&powerAction=" + encodeURIComponent(action)
|
url: vfUrl(systemUrl, serviceId, "powerAction"),
|
||||||
|
data: { powerAction: action }
|
||||||
}).done(function (response) {
|
}).done(function (response) {
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
alertDiv.removeClass("alert-danger").addClass("alert-success");
|
vfShowAlert(alertDiv, "success",response.data.message || (actionLabels[action] + " server..."));
|
||||||
alertDiv.text(response.data.message || (actionLabels[action] + " server..."));
|
|
||||||
} else {
|
} else {
|
||||||
alertDiv.removeClass("alert-success").addClass("alert-danger");
|
vfShowAlert(alertDiv, "danger","Power action failed. Please try again.");
|
||||||
alertDiv.text(response.errors || "Power action failed.");
|
|
||||||
}
|
}
|
||||||
alertDiv.show();
|
alertDiv.show();
|
||||||
}).fail(function () {
|
}).fail(function () {
|
||||||
alertDiv.removeClass("alert-success").addClass("alert-danger");
|
vfShowAlert(alertDiv, "danger","An error occurred. Please try again.");
|
||||||
alertDiv.text("An error occurred. Please try again.");
|
|
||||||
alertDiv.show();
|
|
||||||
}).always(function () {
|
}).always(function () {
|
||||||
spinner.hide();
|
spinner.hide();
|
||||||
$(".vf-btn-power").prop("disabled", false);
|
// Cooldown: keep buttons disabled for 3 seconds
|
||||||
|
setTimeout(function () {
|
||||||
|
$(".vf-btn-power").prop("disabled", false);
|
||||||
|
}, 3000);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var vfOsBrandColors = {
|
||||||
|
"ubuntu": "#E95420", "debian": "#A81D33", "rocky": "#10B981", "centos": "#932279",
|
||||||
|
"almalinux": "#0F4266", "alma": "#0F4266", "windows": "#0078D4", "fedora": "#51A2DA",
|
||||||
|
"arch": "#1793D1", "opensuse": "#73BA25", "suse": "#73BA25", "freebsd": "#AB2B28",
|
||||||
|
"oracle": "#F80000", "rhel": "#EE0000", "red hat": "#EE0000", "cloudlinux": "#0095D9",
|
||||||
|
"gentoo": "#54487A", "slackware": "#000", "nixos": "#7EBAE4", "alpine": "#0D597F"
|
||||||
|
};
|
||||||
|
|
||||||
|
function vfGetBrandColor(name) {
|
||||||
|
var lower = (name || "").toLowerCase();
|
||||||
|
for (var key in vfOsBrandColors) {
|
||||||
|
if (lower.indexOf(key) !== -1) return vfOsBrandColors[key];
|
||||||
|
}
|
||||||
|
return "#6c757d";
|
||||||
|
}
|
||||||
|
|
||||||
|
function vfRenderOsGallery(container, data, hiddenInput) {
|
||||||
|
var $container = $(container);
|
||||||
|
$container.empty();
|
||||||
|
|
||||||
|
if (!data || !data.categories || data.categories.length === 0) {
|
||||||
|
$container.append($('<p class="text-muted"></p>').text("No templates available"));
|
||||||
|
$container.show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var baseUrl = data.baseUrl || "";
|
||||||
|
|
||||||
|
$.each(data.categories, function (ci, category) {
|
||||||
|
var section = $('<div class="vf-os-category"></div>').attr("data-category", ci);
|
||||||
|
var brandColor = vfGetBrandColor(category.name);
|
||||||
|
|
||||||
|
// Accordion header
|
||||||
|
var header = $('<div class="vf-os-category-header"></div>');
|
||||||
|
var iconSpan = $('<span class="vf-os-category-icon"></span>');
|
||||||
|
if (category.icon && baseUrl) {
|
||||||
|
var catImg = $('<img alt="">').attr("src", baseUrl + "/img/logo/" + encodeURIComponent(category.icon));
|
||||||
|
catImg.on("error", function () {
|
||||||
|
$(this).parent().css("background", brandColor);
|
||||||
|
$(this).replaceWith($('<span></span>').text((category.name || "?")[0].toUpperCase()));
|
||||||
|
});
|
||||||
|
iconSpan.append(catImg);
|
||||||
|
} else if (category.name === "Other") {
|
||||||
|
iconSpan.css("background", "#6c757d").html('<svg width="16" height="16" viewBox="0 0 16 16" fill="#fff"><path d="M3 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3zm1 2h8v2H4V4zm0 3h8v1H4V7zm0 2h5v1H4V9z"/></svg>');
|
||||||
|
} else {
|
||||||
|
iconSpan.css("background", brandColor).text((category.name || "?")[0].toUpperCase());
|
||||||
|
}
|
||||||
|
var titleSpan = $('<span></span>').text(category.name + " (" + category.templates.length + ")");
|
||||||
|
var arrow = $('<span class="vf-os-category-arrow">' + (ci === 0 ? '▼' : '▶') + '</span>');
|
||||||
|
header.append(iconSpan).append(titleSpan).append(arrow);
|
||||||
|
section.append(header);
|
||||||
|
|
||||||
|
// Collapsible grid — first category open by default
|
||||||
|
var grid = $('<div class="vf-os-grid"></div>');
|
||||||
|
if (ci !== 0) grid.hide();
|
||||||
|
|
||||||
|
header.on("click", function () {
|
||||||
|
var isVisible = grid.is(":visible");
|
||||||
|
// Collapse all other categories
|
||||||
|
$container.find(".vf-os-grid").slideUp(200);
|
||||||
|
$container.find(".vf-os-category-arrow").html('▶');
|
||||||
|
// Toggle this one
|
||||||
|
if (!isVisible) {
|
||||||
|
grid.slideDown(200);
|
||||||
|
arrow.html('▼');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$.each(category.templates, function (ti, tpl) {
|
||||||
|
var label = tpl.name + (tpl.version ? " " + tpl.version : "") + (tpl.variant ? " " + tpl.variant : "");
|
||||||
|
var card = $('<div class="vf-os-card"></div>')
|
||||||
|
.attr("data-id", tpl.id)
|
||||||
|
.attr("data-search", label.toLowerCase());
|
||||||
|
if (tpl.eol) card.addClass("vf-os-card-eol");
|
||||||
|
|
||||||
|
var iconDiv = $('<div class="vf-os-icon"></div>');
|
||||||
|
if (tpl.icon && baseUrl) {
|
||||||
|
var tplImg = $('<img alt="">').attr("src", baseUrl + "/img/logo/" + encodeURIComponent(tpl.icon));
|
||||||
|
tplImg.on("error", function () {
|
||||||
|
$(this).parent().css("background", brandColor);
|
||||||
|
$(this).replaceWith($('<span></span>').text((tpl.name || "?")[0].toUpperCase()));
|
||||||
|
});
|
||||||
|
iconDiv.append(tplImg);
|
||||||
|
} else {
|
||||||
|
iconDiv.css("background", brandColor);
|
||||||
|
iconDiv.append($('<span></span>').text((tpl.name || "?")[0].toUpperCase()));
|
||||||
|
}
|
||||||
|
|
||||||
|
card.append(iconDiv);
|
||||||
|
card.append($('<div class="vf-os-label"></div>').text(tpl.name));
|
||||||
|
card.append($('<div class="vf-os-version"></div>').text((tpl.version || "") + (tpl.variant ? " " + tpl.variant : "")));
|
||||||
|
if (tpl.eol) {
|
||||||
|
card.append($('<span class="vf-os-eol-badge"></span>').text("EOL"));
|
||||||
|
}
|
||||||
|
|
||||||
|
card.on("click", function () {
|
||||||
|
$container.find(".vf-os-card").removeClass("vf-os-card-selected");
|
||||||
|
$(this).addClass("vf-os-card-selected");
|
||||||
|
$(hiddenInput).val(tpl.id);
|
||||||
|
|
||||||
|
var details = $("#vf-os-details");
|
||||||
|
details.empty();
|
||||||
|
details.append($('<strong></strong>').text(label));
|
||||||
|
if (tpl.description) {
|
||||||
|
details.append($('<p class="mb-0 mt-1 text-muted"></p>').text(tpl.description));
|
||||||
|
}
|
||||||
|
details.show();
|
||||||
|
});
|
||||||
|
|
||||||
|
grid.append(card);
|
||||||
|
});
|
||||||
|
|
||||||
|
section.append(grid);
|
||||||
|
$container.append(section);
|
||||||
|
});
|
||||||
|
|
||||||
|
$container.show();
|
||||||
|
}
|
||||||
|
|
||||||
function vfLoadOsTemplates(serviceId, systemUrl) {
|
function vfLoadOsTemplates(serviceId, systemUrl) {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
type: "GET",
|
type: "GET",
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
url: systemUrl + "modules/servers/VirtFusionDirect/client.php?serviceID=" + encodeURIComponent(serviceId) + "&action=osTemplates"
|
url: vfUrl(systemUrl, serviceId, "osTemplates")
|
||||||
}).done(function (response) {
|
}).done(function (response) {
|
||||||
var select = $("#vf-rebuild-os");
|
$("#vf-os-gallery-loader").hide();
|
||||||
select.empty();
|
if (response.success && response.data) {
|
||||||
if (response.success && response.data && response.data.length > 0) {
|
vfRenderOsGallery("#vf-os-gallery", response.data, "#vf-rebuild-os");
|
||||||
select.append('<option value="">-- Select Operating System --</option>');
|
|
||||||
$.each(response.data, function (i, template) {
|
// Bind search after gallery is rendered
|
||||||
select.append('<option value="' + template.id + '">' + $('<span>').text(template.name).html() + '</option>');
|
$("#vf-os-search").on("keyup", function () {
|
||||||
|
var query = $(this).val().toLowerCase();
|
||||||
|
$("#vf-os-gallery .vf-os-card").each(function () {
|
||||||
|
var match = $(this).data("search").indexOf(query) !== -1;
|
||||||
|
$(this).toggle(match);
|
||||||
|
});
|
||||||
|
$("#vf-os-gallery .vf-os-category").each(function () {
|
||||||
|
var hasVisible = $(this).find(".vf-os-card:visible").length > 0;
|
||||||
|
$(this).toggle(hasVisible);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
select.append('<option value="">No templates available</option>');
|
$("#vf-os-gallery").append($('<p class="text-muted"></p>').text("No templates available")).show();
|
||||||
}
|
}
|
||||||
}).fail(function () {
|
}).fail(function () {
|
||||||
var select = $("#vf-rebuild-os");
|
$("#vf-os-gallery-loader").hide();
|
||||||
select.empty();
|
$("#vf-os-gallery").append($('<p class="text-danger"></p>').text("Error loading templates")).show();
|
||||||
select.append('<option value="">Error loading templates</option>');
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,9 +457,7 @@ function vfRebuildServer(serviceId, systemUrl) {
|
|||||||
var alertDiv = $("#vf-rebuild-alert");
|
var alertDiv = $("#vf-rebuild-alert");
|
||||||
|
|
||||||
if (!osId) {
|
if (!osId) {
|
||||||
alertDiv.removeClass("alert-success").addClass("alert-danger");
|
vfShowAlert(alertDiv, "danger","Please select an operating system.");
|
||||||
alertDiv.text("Please select an operating system.");
|
|
||||||
alertDiv.show();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,27 +468,29 @@ function vfRebuildServer(serviceId, systemUrl) {
|
|||||||
$("#vf-rebuild-button").prop("disabled", true);
|
$("#vf-rebuild-button").prop("disabled", true);
|
||||||
$("#vf-rebuild-spinner").show();
|
$("#vf-rebuild-spinner").show();
|
||||||
alertDiv.hide();
|
alertDiv.hide();
|
||||||
|
vfShowProgress("Rebuilding server...");
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
type: "GET",
|
type: "POST",
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
url: systemUrl + "modules/servers/VirtFusionDirect/client.php?serviceID=" + encodeURIComponent(serviceId) + "&action=rebuild&osId=" + encodeURIComponent(osId)
|
url: vfUrl(systemUrl, serviceId, "rebuild"),
|
||||||
|
data: { osId: osId }
|
||||||
}).done(function (response) {
|
}).done(function (response) {
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
alertDiv.removeClass("alert-danger").addClass("alert-success");
|
vfShowAlert(alertDiv, "success",response.data.message || "Server rebuild initiated. You will receive an email when the process is complete.");
|
||||||
alertDiv.text(response.data.message || "Server rebuild initiated. You will receive an email when the process is complete.");
|
|
||||||
} else {
|
} else {
|
||||||
alertDiv.removeClass("alert-success").addClass("alert-danger");
|
vfShowAlert(alertDiv, "danger","Rebuild failed. Please try again.");
|
||||||
alertDiv.text(response.errors || "Rebuild failed.");
|
|
||||||
}
|
}
|
||||||
alertDiv.show();
|
alertDiv.show();
|
||||||
}).fail(function () {
|
}).fail(function () {
|
||||||
alertDiv.removeClass("alert-success").addClass("alert-danger");
|
vfShowAlert(alertDiv, "danger","An error occurred. Please try again.");
|
||||||
alertDiv.text("An error occurred. Please try again.");
|
|
||||||
alertDiv.show();
|
|
||||||
}).always(function () {
|
}).always(function () {
|
||||||
|
vfHideProgress();
|
||||||
$("#vf-rebuild-spinner").hide();
|
$("#vf-rebuild-spinner").hide();
|
||||||
$("#vf-rebuild-button").prop("disabled", false);
|
// Cooldown: keep button disabled for 30 seconds after rebuild
|
||||||
|
setTimeout(function () {
|
||||||
|
$("#vf-rebuild-button").prop("disabled", false);
|
||||||
|
}, 30000);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -327,7 +498,7 @@ function impersonateServerOwner(serviceId, systemUrl) {
|
|||||||
$.ajax({
|
$.ajax({
|
||||||
type: "GET",
|
type: "GET",
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
url: systemUrl + "modules/servers/VirtFusionDirect/admin.php?serviceID=" + encodeURIComponent(serviceId) + "&action=impersonateServerOwner"
|
url: vfUrl(systemUrl, serviceId, "impersonateServerOwner", "admin")
|
||||||
}).done(function (response) {
|
}).done(function (response) {
|
||||||
if (response.success && response.user) {
|
if (response.success && response.user) {
|
||||||
window.open(response.url + "/_imp/in/" + response.user.id + "/-");
|
window.open(response.url + "/_imp/in/" + response.user.id + "/-");
|
||||||
@@ -335,42 +506,6 @@ function impersonateServerOwner(serviceId, systemUrl) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// =========================================================================
|
|
||||||
// Network / IP Management
|
|
||||||
// =========================================================================
|
|
||||||
|
|
||||||
function vfRemoveIP(serviceId, systemUrl, action, identifier) {
|
|
||||||
if (!confirm("Are you sure you want to remove this IP address?")) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var alertDiv = $("#vf-network-alert");
|
|
||||||
alertDiv.hide();
|
|
||||||
|
|
||||||
var paramName = action === "removeIPv4" ? "ip" : "subnet";
|
|
||||||
|
|
||||||
$.ajax({
|
|
||||||
type: "GET",
|
|
||||||
dataType: "json",
|
|
||||||
url: systemUrl + "modules/servers/VirtFusionDirect/client.php?serviceID=" + encodeURIComponent(serviceId) + "&action=" + encodeURIComponent(action) + "&" + paramName + "=" + identifier
|
|
||||||
}).done(function (response) {
|
|
||||||
if (response.success) {
|
|
||||||
alertDiv.removeClass("alert-danger").addClass("alert-success");
|
|
||||||
alertDiv.text(response.data.message || "IP address removed successfully.");
|
|
||||||
alertDiv.show();
|
|
||||||
vfServerData(serviceId, systemUrl);
|
|
||||||
} else {
|
|
||||||
alertDiv.removeClass("alert-success").addClass("alert-danger");
|
|
||||||
alertDiv.text(response.errors || "Failed to remove IP address.");
|
|
||||||
alertDiv.show();
|
|
||||||
}
|
|
||||||
}).fail(function () {
|
|
||||||
alertDiv.removeClass("alert-success").addClass("alert-danger");
|
|
||||||
alertDiv.text("An error occurred. Please try again.");
|
|
||||||
alertDiv.show();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// VNC Console
|
// VNC Console
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
@@ -387,9 +522,7 @@ function vfOpenVnc(serviceId, systemUrl) {
|
|||||||
// Open window immediately in click context to avoid popup blockers
|
// Open window immediately in click context to avoid popup blockers
|
||||||
var vncWindow = window.open("", "_blank");
|
var vncWindow = window.open("", "_blank");
|
||||||
if (!vncWindow) {
|
if (!vncWindow) {
|
||||||
alertDiv.removeClass("alert-success").addClass("alert-danger");
|
vfShowAlert(alertDiv, "danger","Popup blocked. Please allow popups for this site and try again.");
|
||||||
alertDiv.text("Popup blocked. Please allow popups for this site and try again.");
|
|
||||||
alertDiv.show();
|
|
||||||
spinner.hide();
|
spinner.hide();
|
||||||
btn.prop("disabled", false);
|
btn.prop("disabled", false);
|
||||||
return;
|
return;
|
||||||
@@ -398,7 +531,7 @@ function vfOpenVnc(serviceId, systemUrl) {
|
|||||||
$.ajax({
|
$.ajax({
|
||||||
type: "GET",
|
type: "GET",
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
url: systemUrl + "modules/servers/VirtFusionDirect/client.php?serviceID=" + encodeURIComponent(serviceId) + "&action=vnc"
|
url: vfUrl(systemUrl, serviceId, "vnc")
|
||||||
}).done(function (response) {
|
}).done(function (response) {
|
||||||
if (response.success && response.data) {
|
if (response.success && response.data) {
|
||||||
var data = response.data.data || response.data;
|
var data = response.data.data || response.data;
|
||||||
@@ -413,27 +546,76 @@ function vfOpenVnc(serviceId, systemUrl) {
|
|||||||
vncWindow.location.href = vncUrl;
|
vncWindow.location.href = vncUrl;
|
||||||
} else {
|
} else {
|
||||||
vncWindow.close();
|
vncWindow.close();
|
||||||
alertDiv.removeClass("alert-danger").addClass("alert-success");
|
vfShowAlert(alertDiv, "success","VNC session is ready. Check your VirtFusion control panel for access.");
|
||||||
alertDiv.text("VNC session is ready. Check your VirtFusion control panel for access.");
|
|
||||||
alertDiv.show();
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
vncWindow.close();
|
vncWindow.close();
|
||||||
alertDiv.removeClass("alert-success").addClass("alert-danger");
|
vfShowAlert(alertDiv, "danger","VNC console is not available.");
|
||||||
alertDiv.text(response.errors || "VNC console is not available.");
|
|
||||||
alertDiv.show();
|
|
||||||
}
|
}
|
||||||
}).fail(function () {
|
}).fail(function () {
|
||||||
vncWindow.close();
|
vncWindow.close();
|
||||||
alertDiv.removeClass("alert-success").addClass("alert-danger");
|
vfShowAlert(alertDiv, "danger","An error occurred. The server may be powered off.");
|
||||||
alertDiv.text("An error occurred. The server may be powered off.");
|
|
||||||
alertDiv.show();
|
|
||||||
}).always(function () {
|
}).always(function () {
|
||||||
spinner.hide();
|
spinner.hide();
|
||||||
btn.prop("disabled", false);
|
btn.prop("disabled", false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function vfToggleVnc(serviceId, systemUrl, enabled) {
|
||||||
|
var toggle = $("#vf-vnc-toggle");
|
||||||
|
toggle.prop("disabled", true);
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
type: "POST",
|
||||||
|
dataType: "json",
|
||||||
|
url: vfUrl(systemUrl, serviceId, "toggleVnc"),
|
||||||
|
data: { enabled: enabled ? "1" : "0" }
|
||||||
|
}).done(function (response) {
|
||||||
|
if (response.success) {
|
||||||
|
if (enabled && response.data) {
|
||||||
|
var data = response.data.data || response.data;
|
||||||
|
if (data.ip || data.host) {
|
||||||
|
$("#vf-vnc-ip").text(data.ip || data.host || "-");
|
||||||
|
$("#vf-vnc-port").text(data.port || "-");
|
||||||
|
$("#vf-vnc-details").show();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$("#vf-vnc-details").hide();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
toggle.prop("checked", !enabled);
|
||||||
|
}
|
||||||
|
}).fail(function () {
|
||||||
|
toggle.prop("checked", !enabled);
|
||||||
|
}).always(function () {
|
||||||
|
toggle.prop("disabled", false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function vfCopyVncPassword(serviceId, systemUrl) {
|
||||||
|
var confirmSpan = $("#vf-vnc-copy-confirm");
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
type: "GET",
|
||||||
|
dataType: "json",
|
||||||
|
url: vfUrl(systemUrl, serviceId, "vnc")
|
||||||
|
}).done(function (response) {
|
||||||
|
if (response.success && response.data) {
|
||||||
|
var data = response.data.data || response.data;
|
||||||
|
var password = data.password || "";
|
||||||
|
if (password) {
|
||||||
|
navigator.clipboard.writeText(password).then(function () {
|
||||||
|
confirmSpan.text("Copied!").show();
|
||||||
|
setTimeout(function () { confirmSpan.hide(); }, 2000);
|
||||||
|
}).catch(function () {
|
||||||
|
confirmSpan.text("Copy failed").show();
|
||||||
|
setTimeout(function () { confirmSpan.hide(); }, 2000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// Self Service — Credit & Usage
|
// Self Service — Credit & Usage
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
@@ -442,7 +624,7 @@ function vfLoadSelfServiceUsage(serviceId, systemUrl) {
|
|||||||
$.ajax({
|
$.ajax({
|
||||||
type: "GET",
|
type: "GET",
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
url: systemUrl + "modules/servers/VirtFusionDirect/client.php?serviceID=" + encodeURIComponent(serviceId) + "&action=selfServiceUsage"
|
url: vfUrl(systemUrl, serviceId, "selfServiceUsage")
|
||||||
}).done(function (response) {
|
}).done(function (response) {
|
||||||
if (response.success && response.data) {
|
if (response.success && response.data) {
|
||||||
var data = response.data.data || response.data;
|
var data = response.data.data || response.data;
|
||||||
@@ -481,31 +663,6 @@ function vfLoadSelfServiceUsage(serviceId, systemUrl) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function vfLoadSelfServiceReport(serviceId, systemUrl) {
|
|
||||||
$.ajax({
|
|
||||||
type: "GET",
|
|
||||||
dataType: "json",
|
|
||||||
url: systemUrl + "modules/servers/VirtFusionDirect/client.php?serviceID=" + encodeURIComponent(serviceId) + "&action=selfServiceReport"
|
|
||||||
}).done(function (response) {
|
|
||||||
if (response.success && response.data) {
|
|
||||||
var data = response.data.data || response.data;
|
|
||||||
var tbody = $("#vf-ss-usage-table");
|
|
||||||
tbody.empty();
|
|
||||||
|
|
||||||
var items = data.items || data.report || [];
|
|
||||||
if (Array.isArray(items) && items.length > 0) {
|
|
||||||
$.each(items, function (i, item) {
|
|
||||||
var desc = item.description || item.name || "Item";
|
|
||||||
var cost = item.cost !== undefined ? parseFloat(item.cost).toFixed(2) : "-";
|
|
||||||
tbody.append('<tr><td>' + $('<span>').text(desc).html() + '</td><td class="text-right">' + $('<span>').text(cost).html() + '</td></tr>');
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
tbody.append('<tr><td colspan="2" class="text-muted">No report data available</td></tr>');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function vfAddCredit(serviceId, systemUrl) {
|
function vfAddCredit(serviceId, systemUrl) {
|
||||||
var amount = $("#vf-ss-credit-amount").val();
|
var amount = $("#vf-ss-credit-amount").val();
|
||||||
var alertDiv = $("#vf-selfservice-alert");
|
var alertDiv = $("#vf-selfservice-alert");
|
||||||
@@ -513,9 +670,7 @@ function vfAddCredit(serviceId, systemUrl) {
|
|||||||
var spinner = $("#vf-ss-add-credit-spinner");
|
var spinner = $("#vf-ss-add-credit-spinner");
|
||||||
|
|
||||||
if (!amount || parseFloat(amount) <= 0) {
|
if (!amount || parseFloat(amount) <= 0) {
|
||||||
alertDiv.removeClass("alert-success").addClass("alert-danger");
|
vfShowAlert(alertDiv, "danger","Please enter a valid positive amount.");
|
||||||
alertDiv.text("Please enter a valid positive amount.");
|
|
||||||
alertDiv.show();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -524,28 +679,335 @@ function vfAddCredit(serviceId, systemUrl) {
|
|||||||
alertDiv.hide();
|
alertDiv.hide();
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
type: "GET",
|
type: "POST",
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
url: systemUrl + "modules/servers/VirtFusionDirect/client.php?serviceID=" + encodeURIComponent(serviceId) + "&action=selfServiceAddCredit&tokens=" + encodeURIComponent(amount)
|
url: vfUrl(systemUrl, serviceId, "selfServiceAddCredit"),
|
||||||
|
data: { tokens: amount }
|
||||||
}).done(function (response) {
|
}).done(function (response) {
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
alertDiv.removeClass("alert-danger").addClass("alert-success");
|
vfShowAlert(alertDiv, "success","Credit added successfully.");
|
||||||
alertDiv.text("Credit added successfully.");
|
|
||||||
alertDiv.show();
|
|
||||||
$("#vf-ss-credit-amount").val("");
|
$("#vf-ss-credit-amount").val("");
|
||||||
// Refresh usage data
|
// Refresh usage data
|
||||||
vfLoadSelfServiceUsage(serviceId, systemUrl);
|
vfLoadSelfServiceUsage(serviceId, systemUrl);
|
||||||
} else {
|
} else {
|
||||||
alertDiv.removeClass("alert-success").addClass("alert-danger");
|
vfShowAlert(alertDiv, "danger","Failed to add credit. Please try again.");
|
||||||
alertDiv.text(response.errors || "Failed to add credit.");
|
|
||||||
alertDiv.show();
|
|
||||||
}
|
}
|
||||||
}).fail(function () {
|
}).fail(function () {
|
||||||
alertDiv.removeClass("alert-success").addClass("alert-danger");
|
vfShowAlert(alertDiv, "danger","An error occurred. Please try again.");
|
||||||
alertDiv.text("An error occurred. Please try again.");
|
|
||||||
alertDiv.show();
|
|
||||||
}).always(function () {
|
}).always(function () {
|
||||||
spinner.hide();
|
spinner.hide();
|
||||||
btn.prop("disabled", false);
|
btn.prop("disabled", false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Server Password Reset
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
function vfResetServerPassword(serviceId, systemUrl) {
|
||||||
|
if (!confirm("Are you sure you want to reset the server root password? This will change the password immediately.")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var btn = $("#vf-server-password-btn");
|
||||||
|
var spinner = $("#vf-server-password-spinner");
|
||||||
|
var alertDiv = $("#vf-server-password-alert");
|
||||||
|
|
||||||
|
btn.prop("disabled", true);
|
||||||
|
spinner.show();
|
||||||
|
alertDiv.hide();
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
type: "POST",
|
||||||
|
dataType: "json",
|
||||||
|
url: vfUrl(systemUrl, serviceId, "resetServerPassword")
|
||||||
|
}).done(function (response) {
|
||||||
|
if (response.success && response.data) {
|
||||||
|
var data = response.data.data || response.data;
|
||||||
|
var password = data.password || data.newPassword || "";
|
||||||
|
if (password) {
|
||||||
|
navigator.clipboard.writeText(password).then(function () {
|
||||||
|
vfShowAlert(alertDiv, "success","New password copied to clipboard.");
|
||||||
|
}).catch(function () {
|
||||||
|
vfShowAlert(alertDiv, "warning","Password reset successful. Unable to copy to clipboard automatically.");
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
vfShowAlert(alertDiv, "success","Password reset initiated. Check your email for the new credentials.");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
vfShowAlert(alertDiv, "danger","Password reset failed. Please try again.");
|
||||||
|
}
|
||||||
|
}).fail(function () {
|
||||||
|
vfShowAlert(alertDiv, "danger","An error occurred. Please try again.");
|
||||||
|
}).always(function () {
|
||||||
|
spinner.hide();
|
||||||
|
btn.prop("disabled", false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Backup Listing
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
function vfLoadBackups(serviceId, systemUrl) {
|
||||||
|
$.ajax({
|
||||||
|
type: "GET",
|
||||||
|
dataType: "json",
|
||||||
|
url: vfUrl(systemUrl, serviceId, "backups")
|
||||||
|
}).done(function (response) {
|
||||||
|
if (response.success && response.data) {
|
||||||
|
var backups = response.data.data || response.data;
|
||||||
|
if (!Array.isArray(backups)) backups = [];
|
||||||
|
|
||||||
|
if (backups.length > 0) {
|
||||||
|
var timeline = $("#vf-backups-timeline");
|
||||||
|
timeline.empty();
|
||||||
|
|
||||||
|
$.each(backups, function (i, backup) {
|
||||||
|
var rawDate = backup.created_at || backup.date || "";
|
||||||
|
var date = rawDate;
|
||||||
|
try { if (rawDate) date = new Date(rawDate).toLocaleString(); } catch (e) {}
|
||||||
|
var size = backup.size ? (backup.size >= 1024 ? (backup.size / 1024).toFixed(2) + " GB" : backup.size + " MB") : "-";
|
||||||
|
var status = backup.status || "completed";
|
||||||
|
var dotClass = status === "completed" ? "vf-timeline-dot-success" : "vf-timeline-dot-pending";
|
||||||
|
|
||||||
|
var item = $('<div class="vf-timeline-item"></div>');
|
||||||
|
if (i >= 10) item.addClass("vf-timeline-item-hidden").hide();
|
||||||
|
item.append('<div class="vf-timeline-dot ' + dotClass + '"></div>');
|
||||||
|
item.append($('<div class="vf-timeline-content"></div>')
|
||||||
|
.append($('<div class="vf-bold"></div>').text(date))
|
||||||
|
.append($('<div class="text-muted"></div>').text("Size: " + size + " | Status: " + status))
|
||||||
|
);
|
||||||
|
timeline.append(item);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (backups.length > 10) {
|
||||||
|
$("#vf-backups-show-all").show();
|
||||||
|
}
|
||||||
|
|
||||||
|
$("#vf-backups-section").show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).always(function () {
|
||||||
|
$("#vf-backups-loader").hide();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Traffic Statistics Chart
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
function vfDrawTrafficChart(canvasId, entries) {
|
||||||
|
var canvas = document.getElementById(canvasId);
|
||||||
|
if (!canvas || !canvas.getContext) return;
|
||||||
|
|
||||||
|
var dpr = window.devicePixelRatio || 1;
|
||||||
|
var rect = canvas.parentElement.getBoundingClientRect();
|
||||||
|
canvas.width = rect.width * dpr;
|
||||||
|
canvas.height = 200 * dpr;
|
||||||
|
canvas.style.height = "200px";
|
||||||
|
canvas.style.width = "100%";
|
||||||
|
|
||||||
|
var ctx = canvas.getContext("2d");
|
||||||
|
ctx.scale(dpr, dpr);
|
||||||
|
var w = rect.width;
|
||||||
|
var h = 200;
|
||||||
|
|
||||||
|
if (!entries || entries.length === 0) {
|
||||||
|
ctx.fillStyle = "#888";
|
||||||
|
ctx.font = "13px sans-serif";
|
||||||
|
ctx.textAlign = "center";
|
||||||
|
ctx.fillText("No traffic data available", w / 2, h / 2);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var maxVal = 0;
|
||||||
|
entries.forEach(function (e) {
|
||||||
|
var total = (e.inbound || 0) + (e.outbound || 0);
|
||||||
|
if (total > maxVal) maxVal = total;
|
||||||
|
});
|
||||||
|
if (maxVal === 0) maxVal = 1;
|
||||||
|
|
||||||
|
var padding = { top: 10, right: 10, bottom: 30, left: 50 };
|
||||||
|
var chartW = w - padding.left - padding.right;
|
||||||
|
var chartH = h - padding.top - padding.bottom;
|
||||||
|
var barGroupW = chartW / entries.length;
|
||||||
|
var barW = Math.max(4, (barGroupW * 0.35));
|
||||||
|
|
||||||
|
// Y axis
|
||||||
|
ctx.strokeStyle = "#dee2e6";
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
for (var i = 0; i <= 4; i++) {
|
||||||
|
var y = padding.top + chartH - (chartH * i / 4);
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(padding.left, y);
|
||||||
|
ctx.lineTo(w - padding.right, y);
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.fillStyle = "#888";
|
||||||
|
ctx.font = "10px sans-serif";
|
||||||
|
ctx.textAlign = "right";
|
||||||
|
var labelVal = (maxVal * i / 4);
|
||||||
|
ctx.fillText(labelVal >= 1024 ? (labelVal / 1024).toFixed(1) + " TB" : labelVal.toFixed(0) + " GB", padding.left - 5, y + 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.forEach(function (e, idx) {
|
||||||
|
var inVal = e.inbound || 0;
|
||||||
|
var outVal = e.outbound || 0;
|
||||||
|
var inH = (inVal / maxVal) * chartH;
|
||||||
|
var outH = (outVal / maxVal) * chartH;
|
||||||
|
var x = padding.left + idx * barGroupW + (barGroupW - barW * 2 - 2) / 2;
|
||||||
|
|
||||||
|
ctx.fillStyle = "#337ab7";
|
||||||
|
ctx.fillRect(x, padding.top + chartH - inH, barW, inH);
|
||||||
|
|
||||||
|
ctx.fillStyle = "#28a745";
|
||||||
|
ctx.fillRect(x + barW + 2, padding.top + chartH - outH, barW, outH);
|
||||||
|
|
||||||
|
// X label
|
||||||
|
ctx.fillStyle = "#888";
|
||||||
|
ctx.font = "10px sans-serif";
|
||||||
|
ctx.textAlign = "center";
|
||||||
|
ctx.fillText(e.label || (idx + 1), padding.left + idx * barGroupW + barGroupW / 2, h - 8);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Legend
|
||||||
|
ctx.fillStyle = "#337ab7";
|
||||||
|
ctx.fillRect(padding.left, h - 15, 10, 10);
|
||||||
|
ctx.fillStyle = "#888";
|
||||||
|
ctx.font = "10px sans-serif";
|
||||||
|
ctx.textAlign = "left";
|
||||||
|
ctx.fillText("In", padding.left + 14, h - 6);
|
||||||
|
ctx.fillStyle = "#28a745";
|
||||||
|
ctx.fillRect(padding.left + 32, h - 15, 10, 10);
|
||||||
|
ctx.fillStyle = "#888";
|
||||||
|
ctx.fillText("Out", padding.left + 46, h - 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
function vfLoadTrafficStats(serviceId, systemUrl) {
|
||||||
|
$.ajax({
|
||||||
|
type: "GET",
|
||||||
|
dataType: "json",
|
||||||
|
url: vfUrl(systemUrl, serviceId, "trafficStats")
|
||||||
|
}).done(function (response) {
|
||||||
|
if (response.success && response.data) {
|
||||||
|
var data = response.data.data || response.data;
|
||||||
|
var entries = data.entries || data.traffic || [];
|
||||||
|
var used = data.used || data.totalUsed || 0;
|
||||||
|
var limit = data.limit || data.allowance || 0;
|
||||||
|
|
||||||
|
if (entries.length > 0 || used > 0) {
|
||||||
|
vfDrawTrafficChart("vf-traffic-chart", entries);
|
||||||
|
$("#vf-traffic-used").text(used >= 1024 ? (used / 1024).toFixed(2) + " TB" : used + " GB");
|
||||||
|
$("#vf-traffic-limit").text(limit > 0 ? (limit >= 1024 ? (limit / 1024).toFixed(2) + " TB" : limit + " GB") : "Unlimited");
|
||||||
|
var remaining = limit > 0 ? Math.max(0, limit - used) : 0;
|
||||||
|
$("#vf-traffic-remaining").text(limit > 0 ? (remaining >= 1024 ? (remaining / 1024).toFixed(2) + " TB" : remaining + " GB") : "-");
|
||||||
|
$("#vf-traffic-chart-section").show();
|
||||||
|
|
||||||
|
// Debounced resize redraw
|
||||||
|
var resizeTimer;
|
||||||
|
$(window).on("resize.vfTraffic", function () {
|
||||||
|
clearTimeout(resizeTimer);
|
||||||
|
resizeTimer = setTimeout(function () {
|
||||||
|
vfDrawTrafficChart("vf-traffic-chart", entries);
|
||||||
|
}, 250);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Server Naming
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
function vfGenerateFriendlyName() {
|
||||||
|
var adjectives = ["swift","bold","calm","keen","fair","brave","cool","sage","free","warm"];
|
||||||
|
var nouns = ["cloud","node","core","link","bolt","wave","star","peak","edge","dock"];
|
||||||
|
var adj = adjectives[Math.floor(Math.random() * adjectives.length)];
|
||||||
|
var noun = nouns[Math.floor(Math.random() * nouns.length)];
|
||||||
|
var num = String(Math.floor(Math.random() * 90) + 10);
|
||||||
|
return adj + "-" + noun + "-" + num;
|
||||||
|
}
|
||||||
|
|
||||||
|
function vfShowNameDropdown(serviceId, systemUrl) {
|
||||||
|
var dropdown = $("#vf-name-dropdown");
|
||||||
|
dropdown.empty();
|
||||||
|
|
||||||
|
for (var i = 0; i < 4; i++) {
|
||||||
|
var name = vfGenerateFriendlyName();
|
||||||
|
var opt = $('<div class="vf-name-option"></div>').text(name);
|
||||||
|
(function (n) {
|
||||||
|
opt.on("click", function () {
|
||||||
|
$("#vf-rename-input").val(n);
|
||||||
|
dropdown.hide();
|
||||||
|
});
|
||||||
|
})(name);
|
||||||
|
dropdown.append(opt);
|
||||||
|
}
|
||||||
|
|
||||||
|
var refreshBtn = $('<div class="vf-name-option text-muted" style="text-align:center;cursor:pointer;">↻ More options</div>');
|
||||||
|
refreshBtn.on("click", function () {
|
||||||
|
vfShowNameDropdown(serviceId, systemUrl);
|
||||||
|
});
|
||||||
|
dropdown.append(refreshBtn);
|
||||||
|
dropdown.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
function vfRenameServer(serviceId, systemUrl) {
|
||||||
|
var name = $("#vf-rename-input").val().trim().toLowerCase();
|
||||||
|
var alertDiv = $("#vf-rename-alert");
|
||||||
|
alertDiv.hide();
|
||||||
|
|
||||||
|
if (!name || !/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/.test(name)) {
|
||||||
|
vfShowAlert(alertDiv, "danger","Invalid name. Use lowercase letters, numbers, and hyphens (2-63 chars, must start/end with alphanumeric).");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var btn = $("#vf-rename-save");
|
||||||
|
btn.prop("disabled", true);
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
type: "POST",
|
||||||
|
dataType: "json",
|
||||||
|
url: vfUrl(systemUrl, serviceId, "rename"),
|
||||||
|
data: { name: name }
|
||||||
|
}).done(function (response) {
|
||||||
|
if (response.success) {
|
||||||
|
vfShowAlert(alertDiv, "success","Server renamed successfully.");
|
||||||
|
} else {
|
||||||
|
vfShowAlert(alertDiv, "danger","Rename failed. Please try again.");
|
||||||
|
}
|
||||||
|
alertDiv.show();
|
||||||
|
}).fail(function () {
|
||||||
|
vfShowAlert(alertDiv, "danger","An error occurred. Please try again.");
|
||||||
|
}).always(function () {
|
||||||
|
btn.prop("disabled", false);
|
||||||
|
setTimeout(function () { alertDiv.fadeOut(); }, 3000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Utility — Copy to Clipboard
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
function vfCopyButton(text) {
|
||||||
|
var btn = $('<button type="button" class="btn btn-sm vf-ip-copy" title="Copy"></button>');
|
||||||
|
btn.html('<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M4 2a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V2zm2-1a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1H6z"/><path d="M2 5a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1v-1h1v1a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h1v1H2z"/></svg>');
|
||||||
|
btn.on("click", function () {
|
||||||
|
var $this = $(this);
|
||||||
|
navigator.clipboard.writeText(text).then(function () {
|
||||||
|
$this.html('<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M13.485 1.929a.75.75 0 0 1 .086 1.057l-7.5 9a.75.75 0 0 1-1.1.043l-3.5-3.5a.75.75 0 0 1 1.06-1.06l2.915 2.915 6.982-8.382a.75.75 0 0 1 1.057-.073z"/></svg>');
|
||||||
|
var tooltip = $('<span class="vf-copy-tooltip">Copied!</span>');
|
||||||
|
$this.parent().append(tooltip);
|
||||||
|
setTimeout(function () {
|
||||||
|
tooltip.remove();
|
||||||
|
$this.html('<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M4 2a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V2zm2-1a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1H6z"/><path d="M2 5a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1v-1h1v1a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h1v1H2z"/></svg>');
|
||||||
|
}, 1500);
|
||||||
|
}).catch(function () {
|
||||||
|
var tooltip = $('<span class="vf-copy-tooltip" style="background:#dc3545;">Failed</span>');
|
||||||
|
$this.parent().append(tooltip);
|
||||||
|
setTimeout(function () { tooltip.remove(); }, 1500);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return btn;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<link href="{$systemURL}modules/servers/VirtFusionDirect/templates/css/module.css?v=20260207" rel="stylesheet">
|
<link href="{$systemURL}modules/servers/VirtFusionDirect/templates/css/module.css?v={$smarty.now}" rel="stylesheet">
|
||||||
<script src="{$systemURL}modules/servers/VirtFusionDirect/templates/js/module.js?v=20260207"></script>
|
<script src="{$systemURL}modules/servers/VirtFusionDirect/templates/js/module.js?v={$smarty.now}"></script>
|
||||||
|
|
||||||
{if $serviceStatus eq 'Active'}
|
{if $serviceStatus eq 'Active'}
|
||||||
|
|
||||||
@@ -12,9 +12,27 @@
|
|||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body card-body p-4">
|
<div class="panel-body card-body p-4">
|
||||||
|
<div id="vf-action-progress" style="display:none;">
|
||||||
|
<div class="spinner-border spinner-border-sm text-light"></div>
|
||||||
|
<span id="vf-action-progress-text"></span>
|
||||||
|
<span id="vf-action-progress-timer" class="ml-auto" style="margin-left:auto;"></span>
|
||||||
|
</div>
|
||||||
<div id="vf-server-info-loader-container">
|
<div id="vf-server-info-loader-container">
|
||||||
<div id="vf-server-info-loader" class="d-flex align-items-center justify-content-center">
|
<div id="vf-server-info-loader">
|
||||||
<div class="spinner-border"></div>
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="vf-skeleton vf-skeleton-line vf-skeleton-line-medium"></div>
|
||||||
|
<div class="vf-skeleton vf-skeleton-line vf-skeleton-line-short"></div>
|
||||||
|
<div class="vf-skeleton vf-skeleton-line vf-skeleton-line-medium"></div>
|
||||||
|
<div class="vf-skeleton vf-skeleton-line vf-skeleton-line-short"></div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="vf-skeleton vf-skeleton-line vf-skeleton-line-medium"></div>
|
||||||
|
<div class="vf-skeleton vf-skeleton-line vf-skeleton-line-short"></div>
|
||||||
|
<div class="vf-skeleton vf-skeleton-line vf-skeleton-line-medium"></div>
|
||||||
|
<div class="vf-skeleton vf-skeleton-line vf-skeleton-line-short"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script>vfServerData('{$serviceid}', '{$systemURL}');</script>
|
<script>vfServerData('{$serviceid}', '{$systemURL}');</script>
|
||||||
@@ -27,7 +45,15 @@
|
|||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="row p-1">
|
<div class="row p-1">
|
||||||
<div class="col-xs-4 col-4 text-right vf-bold">Name:</div>
|
<div class="col-xs-4 col-4 text-right vf-bold">Name:</div>
|
||||||
<div class="col-xs-8 col-8" id="vf-data-server-name"></div>
|
<div class="col-xs-8 col-8">
|
||||||
|
<div class="d-flex" style="display:flex; gap:6px; align-items:center;">
|
||||||
|
<input type="text" id="vf-rename-input" class="form-control form-control-sm" maxlength="63" style="max-width:200px;" placeholder="Server name">
|
||||||
|
<button id="vf-randomise-btn" onclick="vfShowNameDropdown('{$serviceid}','{$systemURL}')" type="button" class="btn btn-sm btn-outline-secondary" title="Randomise">↻</button>
|
||||||
|
<button id="vf-rename-save" onclick="vfRenameServer('{$serviceid}','{$systemURL}')" type="button" class="btn btn-sm btn-primary">Save</button>
|
||||||
|
</div>
|
||||||
|
<div id="vf-name-dropdown" style="display:none;"></div>
|
||||||
|
<div id="vf-rename-alert" class="mt-1" style="display:none;"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row p-1">
|
<div class="row p-1">
|
||||||
<div class="col-xs-4 col-4 text-right vf-bold">Hostname:</div>
|
<div class="col-xs-4 col-4 text-right vf-bold">Hostname:</div>
|
||||||
@@ -136,6 +162,27 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
<div class="col-12">
|
||||||
|
<hr>
|
||||||
|
<div id="vf-server-password-alert" class="alert" style="display:none;"></div>
|
||||||
|
<p class="vf-small text-muted">Reset the server's root password. The new password will be copied to your clipboard automatically.</p>
|
||||||
|
<button id="vf-server-password-btn" onclick="vfResetServerPassword('{$serviceid}','{$systemURL}')" type="button" class="btn btn-warning text-uppercase d-flex align-items-center">
|
||||||
|
<span id="vf-server-password-spinner" class="spinner-border spinner-border-sm vf-spinner-margin" style="display:none;"></span>
|
||||||
|
Reset Server Password
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-12" id="vf-backups-section" style="display:none;">
|
||||||
|
<hr>
|
||||||
|
<h5 class="vf-bold">Backups</h5>
|
||||||
|
<div id="vf-backups-loader"><div class="spinner-border spinner-border-sm"></div></div>
|
||||||
|
<div id="vf-backups-timeline" class="vf-timeline"></div>
|
||||||
|
<button id="vf-backups-show-all" class="btn btn-sm btn-link" style="display:none;" onclick="$('.vf-timeline-item-hidden').show(); $(this).hide();">Show all</button>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
if (typeof vfLoadBackups === 'function') {
|
||||||
|
vfLoadBackups('{$serviceid}', '{$systemURL}');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -150,16 +197,16 @@
|
|||||||
<div class="alert alert-warning">
|
<div class="alert alert-warning">
|
||||||
<strong>Warning:</strong> Rebuilding your server will erase all data on the server and reinstall the operating system. This action cannot be undone.
|
<strong>Warning:</strong> Rebuilding your server will erase all data on the server and reinstall the operating system. This action cannot be undone.
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<input type="hidden" id="vf-rebuild-os" value="">
|
||||||
<div class="col-md-6">
|
<div class="form-group mb-3">
|
||||||
<div class="form-group mb-3">
|
<label>Operating System</label>
|
||||||
<label for="vf-rebuild-os">Operating System</label>
|
<input type="text" id="vf-os-search" class="form-control vf-os-search" placeholder="Search templates...">
|
||||||
<select id="vf-rebuild-os" class="form-control">
|
|
||||||
<option value="">Loading...</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div id="vf-os-gallery-loader" class="mb-3">
|
||||||
|
<div class="vf-skeleton" style="height:120px;"></div>
|
||||||
|
</div>
|
||||||
|
<div id="vf-os-gallery" class="mb-3" style="display:none;"></div>
|
||||||
|
<div id="vf-os-details" class="mb-3" style="display:none;"></div>
|
||||||
<button id="vf-rebuild-button" onclick="vfRebuildServer('{$serviceid}','{$systemURL}')" type="button" class="btn btn-danger text-uppercase d-flex align-items-center">
|
<button id="vf-rebuild-button" onclick="vfRebuildServer('{$serviceid}','{$systemURL}')" type="button" class="btn btn-danger text-uppercase d-flex align-items-center">
|
||||||
<span id="vf-rebuild-spinner" class="spinner-border spinner-border-sm vf-spinner-margin" style="display:none;"></span>
|
<span id="vf-rebuild-spinner" class="spinner-border spinner-border-sm vf-spinner-margin" style="display:none;"></span>
|
||||||
Rebuild Server
|
Rebuild Server
|
||||||
@@ -235,6 +282,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="vf-traffic-chart-section" style="display:none;">
|
||||||
|
<hr>
|
||||||
|
<h5 class="vf-bold mb-2">Traffic Usage</h5>
|
||||||
|
<canvas id="vf-traffic-chart" style="width:100%; height:200px;"></canvas>
|
||||||
|
<div class="row mt-2 text-center">
|
||||||
|
<div class="col-4"><small class="text-muted">Used</small><div id="vf-traffic-used" class="vf-bold">-</div></div>
|
||||||
|
<div class="col-4"><small class="text-muted">Limit</small><div id="vf-traffic-limit" class="vf-bold">-</div></div>
|
||||||
|
<div class="col-4"><small class="text-muted">Remaining</small><div id="vf-traffic-remaining" class="vf-bold">-</div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
if (typeof vfLoadTrafficStats === 'function') {
|
||||||
|
vfLoadTrafficStats('{$serviceid}', '{$systemURL}');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -246,10 +308,37 @@
|
|||||||
<div class="panel-body card-body p-4">
|
<div class="panel-body card-body p-4">
|
||||||
<div id="vf-vnc-alert" class="alert" style="display: none;"></div>
|
<div id="vf-vnc-alert" class="alert" style="display: none;"></div>
|
||||||
<p>Access your server's console directly in your browser. The server must be running for VNC access.</p>
|
<p>Access your server's console directly in your browser. The server must be running for VNC access.</p>
|
||||||
<button id="vf-vnc-button" onclick="vfOpenVnc('{$serviceid}','{$systemURL}')" type="button" class="btn btn-primary text-uppercase d-flex align-items-center">
|
<div class="d-flex align-items-center mb-3" style="display:flex; gap:12px; align-items:center;">
|
||||||
<span id="vf-vnc-spinner" class="spinner-border spinner-border-sm vf-spinner-margin" style="display:none;"></span>
|
<button id="vf-vnc-button" onclick="vfOpenVnc('{$serviceid}','{$systemURL}')" type="button" class="btn btn-primary text-uppercase d-flex align-items-center">
|
||||||
Open Console
|
<span id="vf-vnc-spinner" class="spinner-border spinner-border-sm vf-spinner-margin" style="display:none;"></span>
|
||||||
</button>
|
Open Console
|
||||||
|
</button>
|
||||||
|
<label class="vf-toggle-label mb-0" style="display:flex; align-items:center; gap:6px; cursor:pointer;">
|
||||||
|
<input type="checkbox" id="vf-vnc-toggle" class="vf-toggle-input" onchange="vfToggleVnc('{$serviceid}','{$systemURL}', this.checked)">
|
||||||
|
<span class="vf-toggle-switch"></span>
|
||||||
|
<span class="vf-small">VNC Enabled</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div id="vf-vnc-details" style="display:none;">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="row p-1">
|
||||||
|
<div class="col-4 text-right vf-bold vf-small">IP:</div>
|
||||||
|
<div class="col-8 vf-small" id="vf-vnc-ip">-</div>
|
||||||
|
</div>
|
||||||
|
<div class="row p-1">
|
||||||
|
<div class="col-4 text-right vf-bold vf-small">Port:</div>
|
||||||
|
<div class="col-8 vf-small" id="vf-vnc-port">-</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="vfCopyVncPassword('{$serviceid}','{$systemURL}')">
|
||||||
|
Copy VNC Password
|
||||||
|
</button>
|
||||||
|
<span id="vf-vnc-copy-confirm" class="text-success vf-small" style="display:none;">Copied!</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
24
pint.json
Normal file
24
pint.json
Normal file
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user