Files
virtfusion-whmcs-module/modules/servers/VirtFusionDirect/lib/Cache.php
Prophet731 1ab2ef42a5 chore: full project audit cleanup, dead code removal, and documentation update
Dead code removed:
- Module.php: remove assignBackupPlan(), getSelfServiceCurrencies() (no callers)
- Cache.php: remove forgetPattern() (no callers, no-op on filesystem)
- module.js: remove vfLoadSelfServiceReport() (no UI trigger)

Stale files removed:
- .releaserc.json (orphaned, conflicts with tag-based workflow)
- .github/workflows/api-sync-check.yml (baseline never populated)
- docs/openapi-baseline.yaml (placeholder stub)
- scripts/generate-endpoint-doc.sh (broken grep patterns)

Security fixes:
- AdminHTML: cast $serverId to (int), cast $serviceId to (int)
- admin.php: add explicit break after every output() call, sanitize error msgs

File hygiene:
- Move modify.sql into modules/servers/VirtFusionDirect/ (matches README docs)
- Fix CHANGELOG.md: remove duplicate 1.0.0 entry, clean up mixed git host URLs

Documentation:
- CLAUDE.md: full rewrite with current architecture, Cache class, development
  rules (try/catch, ownership validation, HTTP methods, caching policy)
- README.md: remove stale IPv4 removal references, add new features (traffic,
  backups, VNC toggle, password reset, OS gallery, copy buttons), add Cache.php
  to file structure, remove "Primary IPv4 Protection" known issue

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 14:28:58 -05:00

174 lines
4.0 KiB
PHP

<?php
namespace WHMCS\Module\Server\VirtFusionDirect;
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);
}
}
}