true, CURLOPT_TIMEOUT => $timeout, CURLOPT_CONNECTTIMEOUT => $timeout, CURLOPT_SSL_VERIFYPEER => $verify, CURLOPT_SSL_VERIFYHOST => $verify ? 2 : 0, ]); $raw = (string)curl_exec($ch); if ($raw === '' && curl_errno($ch)) { $error = curl_error($ch); } $httpCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); } else { $ctx = stream_context_create([ 'http' => [ 'method' => 'GET', 'timeout' => $timeout, ], 'ssl' => [ 'verify_peer' => $verify, 'verify_peer_name' => $verify, ], ]); $raw = (string)@file_get_contents($full, false, $ctx); $httpCode = 200; if ($raw === '') { $error = 'HTTP request failed'; } } if ($error !== '') { return ['ok' => false, 'error' => $error, 'http_code' => $httpCode, 'url' => $full]; } $data = json_decode($raw, true); if (!is_array($data)) { return ['ok' => false, 'error' => 'invalid_json', 'http_code' => $httpCode, 'raw' => $raw, 'url' => $full]; } return ['ok' => true, 'data' => $data, 'http_code' => $httpCode, 'url' => $full]; }; $resolvePrimaryId = function () use ($instances): ?string { foreach ($instances as $id => $row) { if (!empty($row['is_primary'])) { return $id; } } $first = array_key_first($instances); return $first !== null ? (string)$first : null; }; $pickInstances = function (string $target) use ($instances, $resolvePrimaryId): array { if ($target === 'all') { return $instances; } if ($target === 'primary') { $primaryId = $resolvePrimaryId(); if ($primaryId !== null && isset($instances[$primaryId])) { return [$primaryId => $instances[$primaryId]]; } } if (isset($instances[$target])) { return [$target => $instances[$target]]; } return []; }; $aggregateTopList = function (array $items, array &$bucket): void { foreach ($items as $label => $count) { if (!is_string($label)) { continue; } $bucket[$label] = ($bucket[$label] ?? 0) + (int)$count; } }; $aggregateMap = function (array $map, array &$bucket): void { foreach ($map as $label => $count) { if (!is_string($label)) { continue; } $bucket[$label] = ($bucket[$label] ?? 0) + (int)$count; } }; $extractQueryTypes = function (array $data): array { if (isset($data['querytypes']) && is_array($data['querytypes'])) { return $data['querytypes']; } return $data; }; $extractForwardDestinations = function (array $data): array { if (isset($data['forward_destinations']) && is_array($data['forward_destinations'])) { return $data['forward_destinations']; } return $data; }; $extractSources = function (array $data): array { if (isset($data['query_sources']) && is_array($data['query_sources'])) { return $data['query_sources']; } return $data; }; $parseUpdates = function (?array $versions): array { if (!$versions) { return ['available' => false, 'details' => []]; } $details = []; $available = false; foreach (['core', 'web', 'FTL'] as $key) { $current = $versions[$key . '_current'] ?? null; $latest = $versions[$key . '_latest'] ?? null; $updateFlag = $versions[$key . '_update'] ?? null; $needs = false; if (is_string($updateFlag)) { $needs = $updateFlag === 'true' || $updateFlag === '1'; } elseif (is_bool($updateFlag)) { $needs = $updateFlag; } if ($current && $latest && $current !== $latest) { $needs = true; } $details[$key] = [ 'current' => $current, 'latest' => $latest, 'update' => $needs, ]; if ($needs) { $available = true; } } return ['available' => $available, 'details' => $details]; }; if ($action === 'dashboard') { if (empty($instances)) { $respond(['ok' => false, 'error' => 'no_instances'], 400); } $aggregate = [ 'summary' => [ 'dns_queries_today' => 0, 'ads_blocked_today' => 0, 'unique_clients' => 0, 'unique_domains' => 0, 'queries_forwarded' => 0, 'queries_cached' => 0, 'status' => 'unknown', ], 'top_ads' => [], 'top_queries' => [], 'query_types' => [], 'query_sources' => [], 'forward_destinations' => [], 'recent_blocked' => [], 'updates' => ['available' => false, 'details' => []], ]; $instancePayloads = []; $statuses = []; foreach ($instances as $id => $instance) { $errors = []; $summary = $apiRequest($instance, ['summaryRaw' => 1]); if (!$summary['ok']) { $errors[] = ['scope' => 'summary', 'error' => $summary['error'] ?? 'error']; } $topItems = $apiRequest($instance, ['topItems' => 50]); if (!$topItems['ok']) { $errors[] = ['scope' => 'topItems', 'error' => $topItems['error'] ?? 'error']; } $queryTypes = $apiRequest($instance, ['getQueryTypes' => 1]); if (!$queryTypes['ok']) { $errors[] = ['scope' => 'queryTypes', 'error' => $queryTypes['error'] ?? 'error']; } $querySources = $apiRequest($instance, ['getQuerySources' => 1]); if (!$querySources['ok']) { $errors[] = ['scope' => 'querySources', 'error' => $querySources['error'] ?? 'error']; } $forwardDest = $apiRequest($instance, ['getForwardDestinations' => 1]); if (!$forwardDest['ok']) { $errors[] = ['scope' => 'forwardDestinations', 'error' => $forwardDest['error'] ?? 'error']; } $recentBlocked = $apiRequest($instance, ['recentBlocked' => 30]); if (!$recentBlocked['ok']) { $errors[] = ['scope' => 'recentBlocked', 'error' => $recentBlocked['error'] ?? 'error']; } $versions = $apiRequest($instance, ['versions' => 1]); if (!$versions['ok']) { $errors[] = ['scope' => 'versions', 'error' => $versions['error'] ?? 'error']; } $summaryData = $summary['ok'] ? $summary['data'] : null; $topData = $topItems['ok'] ? $topItems['data'] : null; $queryTypesData = $queryTypes['ok'] ? $extractQueryTypes($queryTypes['data']) : null; $querySourcesData = $querySources['ok'] ? $extractSources($querySources['data']) : null; $forwardDestData = $forwardDest['ok'] ? $extractForwardDestinations($forwardDest['data']) : null; $recentData = $recentBlocked['ok'] ? $recentBlocked['data'] : null; $versionsData = $versions['ok'] ? $versions['data'] : null; $updates = $parseUpdates(is_array($versionsData) ? $versionsData : null); if (is_array($summaryData)) { $aggregate['summary']['dns_queries_today'] += (int)($summaryData['dns_queries_today'] ?? 0); $aggregate['summary']['ads_blocked_today'] += (int)($summaryData['ads_blocked_today'] ?? 0); $aggregate['summary']['unique_clients'] += (int)($summaryData['unique_clients'] ?? 0); $aggregate['summary']['unique_domains'] += (int)($summaryData['unique_domains'] ?? 0); $aggregate['summary']['queries_forwarded'] += (int)($summaryData['queries_forwarded'] ?? 0); $aggregate['summary']['queries_cached'] += (int)($summaryData['queries_cached'] ?? 0); $status = (string)($summaryData['status'] ?? 'unknown'); $statuses[] = $status; } if (is_array($topData)) { if (!empty($topData['top_ads']) && is_array($topData['top_ads'])) { $aggregateTopList($topData['top_ads'], $aggregate['top_ads']); } if (!empty($topData['top_queries']) && is_array($topData['top_queries'])) { $aggregateTopList($topData['top_queries'], $aggregate['top_queries']); } } if (is_array($queryTypesData)) { $aggregateMap($queryTypesData, $aggregate['query_types']); } if (is_array($querySourcesData)) { $aggregateMap($querySourcesData, $aggregate['query_sources']); } if (is_array($forwardDestData)) { $aggregateMap($forwardDestData, $aggregate['forward_destinations']); } if (is_array($recentData)) { foreach ($recentData as $entry) { if (is_string($entry)) { $aggregate['recent_blocked'][] = ['domain' => $entry, 'instance' => $instance['name']]; } elseif (is_array($entry) && isset($entry['domain'])) { $aggregate['recent_blocked'][] = ['domain' => (string)$entry['domain'], 'instance' => $instance['name']]; } } } if (!empty($updates['available'])) { $aggregate['updates']['available'] = true; } $aggregate['updates']['details'][$id] = $updates; $instancePayloads[$id] = [ 'meta' => [ 'id' => $id, 'name' => $instance['name'], 'url' => $instance['url'], 'is_primary' => !empty($instance['is_primary']), ], 'summary' => $summaryData, 'top_items' => $topData, 'query_types' => $queryTypesData, 'query_sources' => $querySourcesData, 'forward_destinations' => $forwardDestData, 'recent_blocked' => $recentData, 'versions' => $versionsData, 'updates' => $updates, 'errors' => $errors, ]; } if ($aggregate['summary']['dns_queries_today'] > 0) { $aggregate['summary']['ads_percentage_today'] = round( $aggregate['summary']['ads_blocked_today'] / $aggregate['summary']['dns_queries_today'] * 100, 2 ); } else { $aggregate['summary']['ads_percentage_today'] = 0; } $status = 'unknown'; if ($statuses) { $allEnabled = count(array_filter($statuses, fn($s) => $s === 'enabled')) === count($statuses); $allDisabled = count(array_filter($statuses, fn($s) => $s === 'disabled')) === count($statuses); if ($allEnabled) { $status = 'enabled'; } elseif ($allDisabled) { $status = 'disabled'; } else { $status = 'partial'; } } $aggregate['summary']['status'] = $status; $respond([ 'ok' => true, 'ts' => time(), 'instances' => $instancePayloads, 'aggregate' => $aggregate, ]); } if ($action === 'disable') { require_admin(); $minutes = (int)($payload['minutes'] ?? 0); $target = (string)($payload['instance'] ?? 'all'); if ($minutes <= 0) { $respond(['ok' => false, 'error' => 'invalid_minutes'], 400); } $targets = $pickInstances($target); if (!$targets) { $respond(['ok' => false, 'error' => 'invalid_instance'], 400); } $results = []; foreach ($targets as $id => $instance) { $result = $apiRequest($instance, ['disable' => $minutes * 60]); $results[$id] = $result; } $respond(['ok' => true, 'results' => $results]); } if ($action === 'enable') { require_admin(); $target = (string)($payload['instance'] ?? 'all'); $targets = $pickInstances($target); if (!$targets) { $respond(['ok' => false, 'error' => 'invalid_instance'], 400); } $results = []; foreach ($targets as $id => $instance) { $result = $apiRequest($instance, ['enable' => 1]); $results[$id] = $result; } $respond(['ok' => true, 'results' => $results]); } if ($action === 'gravity') { require_admin(); $target = $listsPrimaryOnly ? 'primary' : (string)($payload['instance'] ?? 'primary'); $targets = $pickInstances($target); if (!$targets) { $respond(['ok' => false, 'error' => 'invalid_instance'], 400); } $results = []; foreach ($targets as $id => $instance) { $result = $apiRequest($instance, ['updateGravity' => 1]); $results[$id] = $result; } $respond(['ok' => true, 'results' => $results]); } if ($action === 'domain_add') { require_admin(); $domain = trim((string)($payload['domain'] ?? '')); $type = (string)($payload['type'] ?? 'block'); if ($domain === '') { $respond(['ok' => false, 'error' => 'missing_domain'], 400); } $target = $listsPrimaryOnly ? 'primary' : (string)($payload['instance'] ?? 'primary'); $targets = $pickInstances($target); if (!$targets) { $respond(['ok' => false, 'error' => 'invalid_instance'], 400); } $params = $type === 'allow' ? ['list' => 'white', 'add' => $domain] : ['list' => 'black', 'add' => $domain]; $results = []; foreach ($targets as $id => $instance) { $result = $apiRequest($instance, $params); $results[$id] = $result; } $respond(['ok' => true, 'results' => $results]); } if ($action === 'adlist_add') { require_admin(); $url = trim((string)($payload['url'] ?? '')); if ($url === '') { $respond(['ok' => false, 'error' => 'missing_url'], 400); } $target = $listsPrimaryOnly ? 'primary' : (string)($payload['instance'] ?? 'primary'); $targets = $pickInstances($target); if (!$targets) { $respond(['ok' => false, 'error' => 'invalid_instance'], 400); } $results = []; foreach ($targets as $id => $instance) { $result = $apiRequest($instance, ['list' => 'adlist', 'add' => $url]); $results[$id] = $result; } $respond(['ok' => true, 'results' => $results]); } if ($action === 'update') { require_admin(); $target = (string)($payload['instance'] ?? 'primary'); $targets = $pickInstances($target); if (!$targets) { $respond(['ok' => false, 'error' => 'invalid_instance'], 400); } $results = []; foreach ($targets as $id => $instance) { $result = $apiRequest($instance, ['update' => 1]); $results[$id] = $result; } $respond(['ok' => true, 'results' => $results]); } $respond(['ok' => false, 'error' => 'unknown_action'], 400);