Files
amneziavpnphp/inc/ProtocolService.php
2026-01-23 17:55:40 +03:00

407 lines
15 KiB
PHP

<?php
class ProtocolService
{
/**
* Get all protocols with additional metadata
*/
public static function getAllProtocolsWithStats(): array
{
try {
$pdo = DB::conn();
$stmt = $pdo->query('
SELECT p.*,
COUNT(DISTINCT sp.server_id) as server_count,
COUNT(DISTINCT pt.id) as template_count,
COUNT(DISTINCT pv.id) as variable_count,
COUNT(DISTINCT ag.id) as ai_generation_count,
MAX(ag.created_at) as last_ai_generation
FROM protocols p
LEFT JOIN server_protocols sp ON p.id = sp.protocol_id
LEFT JOIN protocol_templates pt ON p.id = pt.protocol_id
LEFT JOIN protocol_variables pv ON p.id = pv.protocol_id
LEFT JOIN ai_generations ag ON p.id = ag.protocol_id
GROUP BY p.id
ORDER BY p.name ASC
');
return $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (Exception $e) {
error_log("Error in ProtocolService::getAllProtocolsWithStats: " . $e->getMessage());
throw new Exception('Failed to get protocols with stats');
}
}
/**
* Get protocol with all related data (templates, variables, AI history)
*/
public static function getProtocolWithDetails(int $protocolId): array
{
try {
$pdo = DB::conn();
// Get protocol
$stmt = $pdo->prepare('SELECT * FROM protocols WHERE id = ?');
$stmt->execute([$protocolId]);
$protocol = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$protocol) {
throw new Exception('Protocol not found');
}
// Get templates
$stmt = $pdo->prepare('SELECT * FROM protocol_templates WHERE protocol_id = ? ORDER BY is_default DESC, template_name ASC');
$stmt->execute([$protocolId]);
$protocol['templates'] = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Get variables
$stmt = $pdo->prepare('SELECT * FROM protocol_variables WHERE protocol_id = ? ORDER BY variable_name ASC');
$stmt->execute([$protocolId]);
$protocol['variables'] = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Get AI generation history (last 10)
$stmt = $pdo->prepare('
SELECT ag.*, p.name as protocol_name
FROM ai_generations ag
LEFT JOIN protocols p ON ag.protocol_id = p.id
WHERE ag.protocol_id = ?
ORDER BY ag.created_at DESC
LIMIT 10
');
$stmt->execute([$protocolId]);
$protocol['ai_history'] = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Get server usage
$stmt = $pdo->prepare('
SELECT sp.*, vs.name as server_name, vs.host as server_host
FROM server_protocols sp
JOIN vpn_servers vs ON sp.server_id = vs.id
WHERE sp.protocol_id = ?
ORDER BY sp.applied_at DESC
');
$stmt->execute([$protocolId]);
$protocol['server_usage'] = $stmt->fetchAll(PDO::FETCH_ASSOC);
return $protocol;
} catch (Exception $e) {
error_log("Error in ProtocolService::getProtocolWithDetails: " . $e->getMessage());
throw new Exception('Failed to get protocol details');
}
}
/**
* Validate protocol data before saving
*/
public static function validateProtocolData(array $data): array
{
$errors = [];
// Validate name
if (empty($data['name'])) {
$errors[] = 'Protocol name is required';
} elseif (strlen($data['name']) > 255) {
$errors[] = 'Protocol name must be less than 255 characters';
}
// Validate slug
if (empty($data['slug'])) {
$errors[] = 'Protocol slug is required';
} elseif (!preg_match('/^[a-z0-9_-]+$/i', $data['slug'])) {
$errors[] = 'Slug may contain only letters, numbers, dashes, and underscores';
} elseif (strlen($data['slug']) > 100) {
$errors[] = 'Protocol slug must be less than 100 characters';
}
// Validate description length
if (isset($data['description']) && strlen($data['description']) > 65535) {
$errors[] = 'Description is too long';
}
// Validate install script
if (isset($data['install_script']) && strlen($data['install_script']) > 16777215) { // MEDIUMTEXT limit
$errors[] = 'Installation script is too long';
}
// Validate output template
if (isset($data['output_template']) && strlen($data['output_template']) > 16777215) { // MEDIUMTEXT limit
$errors[] = 'Output template is too long';
}
// Validate ubuntu_compatible
if (isset($data['ubuntu_compatible']) && !is_bool($data['ubuntu_compatible']) && !in_array($data['ubuntu_compatible'], [0, 1, '0', '1'])) {
$errors[] = 'Ubuntu compatible must be a boolean value';
}
// Validate is_active
if (isset($data['is_active']) && !is_bool($data['is_active']) && !in_array($data['is_active'], [0, 1, '0', '1'])) {
$errors[] = 'Active status must be a boolean value';
}
// Validate QR code template
if (isset($data['qr_code_template']) && strlen($data['qr_code_template']) > 16777215) {
$errors[] = 'QR code template is too long';
}
// Validate QR code format
if (isset($data['qr_code_format']) && !in_array($data['qr_code_format'], ['raw', 'amnezia_compressed'])) {
$errors[] = 'Invalid QR code format';
}
return $errors;
}
/**
* Check if slug is unique
*/
public static function isSlugUnique(string $slug, ?int $excludeId = null): bool
{
try {
$pdo = DB::conn();
if ($excludeId) {
$stmt = $pdo->prepare('SELECT COUNT(*) FROM protocols WHERE slug = ? AND id != ?');
$stmt->execute([$slug, $excludeId]);
} else {
$stmt = $pdo->prepare('SELECT COUNT(*) FROM protocols WHERE slug = ?');
$stmt->execute([$slug]);
}
return (int) $stmt->fetchColumn() === 0;
} catch (Exception $e) {
error_log("Error in ProtocolService::isSlugUnique: " . $e->getMessage());
return false;
}
}
/**
* Check if protocol can be deleted
*/
public static function canDeleteProtocol(int $protocolId): array
{
try {
$pdo = DB::conn();
// Check if protocol is used by any servers
$stmt = $pdo->prepare('SELECT COUNT(*) FROM server_protocols WHERE protocol_id = ?');
$stmt->execute([$protocolId]);
$serverCount = (int) $stmt->fetchColumn();
$canDelete = $serverCount === 0;
$reason = '';
if (!$canDelete) {
$reason = "Protocol is currently used by $serverCount server(s)";
}
return [
'can_delete' => $canDelete,
'reason' => $reason,
'server_count' => $serverCount
];
} catch (Exception $e) {
error_log("Error in ProtocolService::canDeleteProtocol: " . $e->getMessage());
return [
'can_delete' => false,
'reason' => 'Database error occurred',
'server_count' => 0
];
}
}
/**
* Generate protocol template with variables
*/
public static function generateProtocolOutput(array $protocol, array $variables): string
{
try {
$template = $protocol['output_template'] ?? '';
if (empty($template)) {
return '';
}
foreach ($variables as $key => $value) {
$template = str_replace('{{' . $key . '}}', $value ?? '', $template);
}
$template = preg_replace('/(\w+:\/\/[^\/:]+):(?=\/|\?|$)/', '$1', $template);
$template = preg_replace('/(@[^\/:]+):(?=\/|\?|$)/', '$1', $template);
$template = preg_replace('/(\w+:\/\/)@(?=[^\/]{1})/', '$1', $template);
$template = preg_replace('/\{\{[^}]+\}\}/', '', $template);
// Check for unreplaced variables
if (preg_match('/\{\{([^}]+)\}\}/', $template, $matches)) {
error_log("Unreplaced variables in protocol template: " . implode(', ', $matches));
}
return $template;
} catch (Exception $e) {
error_log("Error in ProtocolService::generateProtocolOutput: " . $e->getMessage());
return '';
}
}
/**
* Generate QR code payload from template
*/
public static function generateQrCodePayload(array $protocol, array $variables): string
{
try {
$template = $protocol['qr_code_template'] ?? '';
$format = $protocol['qr_code_format'] ?? 'amnezia_compressed';
if (empty($template)) {
return '';
}
// Render template using the same logic as output template
// We temporarily wrap it to use the existing method
$rendered = self::generateProtocolOutput(['output_template' => $template], $variables);
if ($format === 'amnezia_compressed') {
require_once __DIR__ . '/QrUtil.php';
return QrUtil::encodeOldPayloadFromJson($rendered);
}
// For 'raw' and 'text' formats, return rendered template directly
return $rendered;
} catch (Exception $e) {
error_log("Error in ProtocolService::generateQrCodePayload: " . $e->getMessage());
return '';
}
}
/**
* Get protocol statistics for dashboard
*/
public static function getProtocolStatistics(): array
{
try {
$pdo = DB::conn();
// Total protocols
$stmt = $pdo->query('SELECT COUNT(*) FROM protocols');
$totalProtocols = (int) $stmt->fetchColumn();
// Active protocols
$stmt = $pdo->query('SELECT COUNT(*) FROM protocols WHERE is_active = 1');
$activeProtocols = (int) $stmt->fetchColumn();
// Ubuntu compatible protocols
$stmt = $pdo->query('SELECT COUNT(*) FROM protocols WHERE ubuntu_compatible = 1');
$ubuntuCompatibleProtocols = (int) $stmt->fetchColumn();
// Protocols with AI generations
$stmt = $pdo->query('
SELECT COUNT(DISTINCT protocol_id)
FROM ai_generations
WHERE protocol_id IS NOT NULL
');
$protocolsWithAI = (int) $stmt->fetchColumn();
// Recent AI generations
$stmt = $pdo->query('
SELECT COUNT(*)
FROM ai_generations
WHERE created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)
');
$recentAIGenerations = (int) $stmt->fetchColumn();
// Server usage by protocol
$stmt = $pdo->query('
SELECT p.name, COUNT(sp.server_id) as server_count
FROM protocols p
LEFT JOIN server_protocols sp ON p.id = sp.protocol_id
GROUP BY p.id, p.name
ORDER BY server_count DESC
LIMIT 10
');
$serverUsageByProtocol = $stmt->fetchAll(PDO::FETCH_ASSOC);
return [
'total_protocols' => $totalProtocols,
'active_protocols' => $activeProtocols,
'ubuntu_compatible_protocols' => $ubuntuCompatibleProtocols,
'protocols_with_ai' => $protocolsWithAI,
'recent_ai_generations' => $recentAIGenerations,
'server_usage_by_protocol' => $serverUsageByProtocol,
'ai_usage_percentage' => $totalProtocols > 0 ? round(($protocolsWithAI / $totalProtocols) * 100, 2) : 0
];
} catch (Exception $e) {
error_log("Error in ProtocolService::getProtocolStatistics: " . $e->getMessage());
return [
'total_protocols' => 0,
'active_protocols' => 0,
'ubuntu_compatible_protocols' => 0,
'protocols_with_ai' => 0,
'recent_ai_generations' => 0,
'server_usage_by_protocol' => [],
'ai_usage_percentage' => 0
];
}
}
/**
* Get AI generation statistics
*/
public static function getAIGenerationStatistics(): array
{
try {
$pdo = DB::conn();
// Total AI generations
$stmt = $pdo->query('SELECT COUNT(*) FROM ai_generations');
$totalGenerations = (int) $stmt->fetchColumn();
// AI generations this month
$stmt = $pdo->query('
SELECT COUNT(*)
FROM ai_generations
WHERE MONTH(created_at) = MONTH(NOW()) AND YEAR(created_at) = YEAR(NOW())
');
$thisMonthGenerations = (int) $stmt->fetchColumn();
// AI generations by model
$stmt = $pdo->query('
SELECT model_used, COUNT(*) as count
FROM ai_generations
GROUP BY model_used
ORDER BY count DESC
');
$generationsByModel = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Ubuntu compatible generations
$stmt = $pdo->query('
SELECT COUNT(*)
FROM ai_generations
WHERE ubuntu_compatible = 1
');
$ubuntuCompatibleGenerations = (int) $stmt->fetchColumn();
return [
'total_generations' => $totalGenerations,
'this_month_generations' => $thisMonthGenerations,
'generations_by_model' => $generationsByModel,
'ubuntu_compatible_generations' => $ubuntuCompatibleGenerations,
'ubuntu_compatible_percentage' => $totalGenerations > 0 ? round(($ubuntuCompatibleGenerations / $totalGenerations) * 100, 2) : 0
];
} catch (Exception $e) {
error_log("Error in ProtocolService::getAIGenerationStatistics: " . $e->getMessage());
return [
'total_generations' => 0,
'this_month_generations' => 0,
'generations_by_model' => [],
'ubuntu_compatible_generations' => 0,
'ubuntu_compatible_percentage' => 0
];
}
}
}