Files
nexus/modules/pihole/pages/api.php
Lars Gebhardt-Kusche f94dd83b68
All checks were successful
Deploy / deploy-staging (push) Successful in 5s
Deploy / deploy-production (push) Has been skipped
ysdsd
2026-04-27 01:50:16 +02:00

1126 lines
42 KiB
PHP

<?php
require_auth();
header('Content-Type: application/json; charset=utf-8');
$action = (string)($_GET['action'] ?? '');
$instances = module_fn('pihole', 'instances');
$listsPrimaryOnly = module_fn('pihole', 'lists_primary_only');
$body = file_get_contents('php://input');
$payload = [];
if ($body !== false && $body !== '') {
$decoded = json_decode($body, true);
if (is_array($decoded)) {
$payload = $decoded;
}
}
$respond = function (array $data, int $status = 200): void {
http_response_code($status);
echo json_encode($data, JSON_UNESCAPED_UNICODE);
exit;
};
$debugPush = static function (string $label, array $payload = []): void {
module_debug_push('pihole', array_merge(['label' => $label], $payload));
};
app()->session()->start();
$sessionFingerprint = static function (array $instance): string {
return sha1(json_encode([
'id' => (string)($instance['id'] ?? ''),
'url' => (string)($instance['url'] ?? ''),
'password' => (string)($instance['password'] ?? ''),
], JSON_UNESCAPED_UNICODE));
};
$getSessionBucket = static function (): array {
$bucket = $_SESSION['pihole_api_sessions'] ?? [];
return is_array($bucket) ? $bucket : [];
};
$readSessionCache = static function (array $instance) use ($getSessionBucket, $sessionFingerprint): ?array {
$bucket = $getSessionBucket();
$key = (string)($instance['id'] ?? '');
if ($key === '' || !isset($bucket[$key]) || !is_array($bucket[$key])) {
return null;
}
$row = $bucket[$key];
if (($row['fingerprint'] ?? '') !== $sessionFingerprint($instance)) {
return null;
}
return $row;
};
$writeSessionCache = static function (array $instance, string $sid, int $validity = 300, ?string $csrf = null) use ($sessionFingerprint): void {
$key = (string)($instance['id'] ?? '');
if ($key === '' || $sid === '') {
return;
}
if (!isset($_SESSION['pihole_api_sessions']) || !is_array($_SESSION['pihole_api_sessions'])) {
$_SESSION['pihole_api_sessions'] = [];
}
$_SESSION['pihole_api_sessions'][$key] = [
'fingerprint' => $sessionFingerprint($instance),
'sid' => $sid,
'csrf' => $csrf ?? '',
'validity' => $validity > 0 ? $validity : 300,
'expires_at' => time() + max(30, $validity - 15),
'updated_at' => time(),
];
};
$touchSessionCache = static function (array $instance, ?int $validity = null) use ($readSessionCache, $writeSessionCache): void {
$cached = $readSessionCache($instance);
if (!$cached || empty($cached['sid'])) {
return;
}
$writeSessionCache(
$instance,
(string)$cached['sid'],
$validity ?? (int)($cached['validity'] ?? 300),
(string)($cached['csrf'] ?? '')
);
};
$clearSessionCache = static function (array $instance): void {
$key = (string)($instance['id'] ?? '');
if ($key === '' || !isset($_SESSION['pihole_api_sessions']) || !is_array($_SESSION['pihole_api_sessions'])) {
return;
}
unset($_SESSION['pihole_api_sessions'][$key]);
};
$normalizeApiPath = function (string $baseUrl, string $apiPath): string {
$base = rtrim($baseUrl, '/');
$path = $apiPath;
if ($path === '') {
$path = '/admin/api.php';
}
if ($path[0] !== '/') {
$path = '/' . $path;
}
return $base . $path;
};
$httpRequest = function (string $method, string $url, array $headers, ?string $body, bool $verify, int $timeout): array {
$raw = '';
$httpCode = 0;
$error = '';
if (function_exists('curl_init')) {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => $timeout,
CURLOPT_CONNECTTIMEOUT => $timeout,
CURLOPT_SSL_VERIFYPEER => $verify,
CURLOPT_SSL_VERIFYHOST => $verify ? 2 : 0,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => $headers,
]);
if ($body !== null) {
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
}
$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' => $method,
'timeout' => $timeout,
'header' => implode("\r\n", $headers),
'content' => $body ?? '',
],
'ssl' => [
'verify_peer' => $verify,
'verify_peer_name' => $verify,
],
]);
$raw = (string)@file_get_contents($url, false, $ctx);
$httpCode = 200;
if ($raw === '') {
$error = 'HTTP request failed';
}
}
if ($error !== '') {
return ['ok' => false, 'error' => $error, 'http_code' => $httpCode, 'url' => $url];
}
$data = json_decode($raw, true);
if (!is_array($data)) {
return ['ok' => false, 'error' => 'invalid_json', 'http_code' => $httpCode, 'raw' => $raw, 'url' => $url];
}
if ($httpCode >= 400) {
$apiError = is_array($data['error'] ?? null) ? $data['error'] : [];
return [
'ok' => false,
'error' => (string)($apiError['message'] ?? $apiError['key'] ?? ('HTTP ' . $httpCode)),
'error_key' => (string)($apiError['key'] ?? ''),
'hint' => $apiError['hint'] ?? null,
'http_code' => $httpCode,
'data' => $data,
'url' => $url,
];
}
if (isset($data['error'])) {
$apiError = is_array($data['error']) ? $data['error'] : [];
return [
'ok' => false,
'error' => (string)($apiError['message'] ?? $apiError['key'] ?? 'api_error'),
'error_key' => (string)($apiError['key'] ?? ''),
'hint' => $apiError['hint'] ?? null,
'http_code' => $httpCode,
'data' => $data,
'url' => $url,
];
}
return ['ok' => true, 'data' => $data, 'http_code' => $httpCode, 'url' => $url];
};
$v5Request = function (array $instance, array $params) use ($normalizeApiPath, $httpRequest): array {
if (!empty($instance['password']) && !isset($params['auth'])) {
$params['auth'] = $instance['password'];
}
$url = $normalizeApiPath((string)$instance['url'], (string)$instance['api_path']);
$qs = http_build_query($params, '', '&', PHP_QUERY_RFC3986);
$full = $qs !== '' ? $url . '?' . $qs : $url;
$timeout = (int)($instance['timeout'] ?? 8);
if ($timeout <= 0) {
$timeout = 8;
}
$verify = !empty($instance['verify_tls']);
return $httpRequest('GET', $full, ['Accept: application/json'], null, $verify, $timeout);
};
$v6Logout = function (array $instance, string $sid) use ($httpRequest): void {
if ($sid === '') {
return;
}
$base = rtrim((string)$instance['url'], '/');
$url = $base . '/api/auth?sid=' . rawurlencode($sid);
$timeout = (int)($instance['timeout'] ?? 8);
if ($timeout <= 0) {
$timeout = 8;
}
$verify = !empty($instance['verify_tls']);
$httpRequest('DELETE', $url, ['Accept: application/json', 'X-FTL-SID: ' . $sid], null, $verify, $timeout);
};
$v6Auth = function (array $instance) use ($httpRequest, $readSessionCache, $writeSessionCache, $clearSessionCache, $v6Logout): array {
$cached = $readSessionCache($instance);
if ($cached && !empty($cached['sid']) && (int)($cached['expires_at'] ?? 0) > time()) {
return [
'ok' => true,
'http_code' => 200,
'url' => rtrim((string)$instance['url'], '/') . '/api/auth',
'data' => ['session' => ['sid' => (string)$cached['sid'], 'validity' => (int)($cached['validity'] ?? 300)]],
'sid' => (string)$cached['sid'],
'cached' => true,
];
}
$base = rtrim((string)$instance['url'], '/');
$url = $base . '/api/auth';
$timeout = (int)($instance['timeout'] ?? 8);
if ($timeout <= 0) {
$timeout = 8;
}
$verify = !empty($instance['verify_tls']);
$payload = ['password' => (string)($instance['password'] ?? '')];
$body = json_encode($payload, JSON_UNESCAPED_UNICODE);
$res = $httpRequest('POST', $url, ['Accept: application/json', 'Content-Type: application/json'], $body, $verify, $timeout);
if (!$res['ok']) {
$clearSessionCache($instance);
return $res;
}
$data = (array)($res['data'] ?? []);
$session = (array)($data['session'] ?? []);
$sid = (string)($session['sid'] ?? '');
$validity = (int)($session['validity'] ?? 300);
$csrf = (string)($session['csrf'] ?? '');
if ($cached && !empty($cached['sid']) && $cached['sid'] !== $sid) {
$v6Logout($instance, (string)$cached['sid']);
}
if ($sid !== '') {
$writeSessionCache($instance, $sid, $validity, $csrf);
}
$res['sid'] = $sid;
$res['cached'] = false;
return $res;
};
$v6Request = function (array $instance, string $path, string $method, array $payload, string $sid) use ($httpRequest, $clearSessionCache, $touchSessionCache): array {
$base = rtrim((string)$instance['url'], '/');
$path = ltrim($path, '/');
$url = $base . '/api/' . $path;
$timeout = (int)($instance['timeout'] ?? 8);
if ($timeout <= 0) {
$timeout = 8;
}
$verify = !empty($instance['verify_tls']);
$headers = ['Accept: application/json'];
if ($sid !== '') {
$headers[] = 'sid: ' . $sid;
$headers[] = 'X-FTL-SID: ' . $sid;
}
$body = null;
if ($method !== 'GET') {
if ($sid !== '' && !isset($payload['sid'])) {
$payload['sid'] = $sid;
}
$body = json_encode($payload, JSON_UNESCAPED_UNICODE);
$headers[] = 'Content-Type: application/json';
}
$result = $httpRequest($method, $url, $headers, $body, $verify, $timeout);
$httpCode = (int)($result['http_code'] ?? 0);
if ($sid !== '') {
if (($result['ok'] ?? false) === true) {
$touchSessionCache($instance);
} elseif (in_array($httpCode, [401, 403], true)) {
$clearSessionCache($instance);
}
}
return $result;
};
$v6RequestAny = function (array $instance, array $paths, string $method, array $payload, string $sid) use ($v6Request): array {
$last = ['ok' => false, 'error' => 'no_path', 'http_code' => 0, 'url' => ''];
foreach ($paths as $path) {
$result = $v6Request($instance, (string)$path, $method, $payload, $sid);
if (($result['ok'] ?? false) === true) {
return $result;
}
$last = $result;
$httpCode = (int)($result['http_code'] ?? 0);
if (!in_array($httpCode, [0, 400, 404], true)) {
return $result;
}
}
return $last;
};
$detectApi = function (array $instance) use ($v6Auth, $v6RequestAny, $v5Request, $clearSessionCache): array {
$sid = '';
$authRes = null;
if (!empty($instance['password'])) {
$authRes = $v6Auth($instance);
if (($authRes['ok'] ?? false) && !empty($authRes['sid'])) {
$sid = (string)$authRes['sid'];
$probe = $v6RequestAny($instance, ['dns/blocking', 'stats/summary', 'summary'], 'GET', [], $sid);
if (!(empty($authRes['cached'])) && in_array((int)($probe['http_code'] ?? 0), [401, 403], true)) {
$clearSessionCache($instance);
$authRes = $v6Auth($instance);
if (($authRes['ok'] ?? false) && !empty($authRes['sid'])) {
$sid = (string)$authRes['sid'];
$probe = $v6RequestAny($instance, ['dns/blocking', 'stats/summary', 'summary'], 'GET', [], $sid);
}
}
return ['version' => 6, 'sid' => $sid, 'probe' => $probe, 'auth' => $authRes];
}
$httpCode = (int)($authRes['http_code'] ?? 0);
if (in_array($httpCode, [401, 403], true)) {
return ['version' => 6, 'sid' => '', 'probe' => $authRes, 'auth' => $authRes];
}
}
$probe = $v6RequestAny($instance, ['dns/blocking', 'stats/summary', 'summary'], 'GET', [], $sid);
if ($probe['ok'] || in_array((int)($probe['http_code'] ?? 0), [401, 403], true)) {
return ['version' => 6, 'sid' => $sid, 'probe' => $probe, 'auth' => $authRes];
}
$legacy = $v5Request($instance, ['summaryRaw' => 1]);
if ($legacy['ok']) {
return ['version' => 5, 'sid' => '', 'probe' => $legacy];
}
return ['version' => 0, 'sid' => '', 'probe' => $probe, 'legacy' => $legacy, 'auth' => $authRes];
};
$debugResult = static function (string $label, string $instanceId, array $instance, array $result) use ($debugPush): void {
$payload = [
'instance_id' => $instanceId,
'instance_name' => (string)($instance['name'] ?? $instanceId),
'url' => (string)($instance['url'] ?? ''),
'ok' => (bool)($result['ok'] ?? false),
'http_code' => (int)($result['http_code'] ?? 0),
'error' => (string)($result['error'] ?? ''),
'error_key' => (string)($result['error_key'] ?? ''),
'hint' => $result['hint'] ?? null,
'request_url' => (string)($result['url'] ?? ''),
];
if (isset($result['data']) && is_array($result['data'])) {
$payload['response'] = $result['data'];
} elseif (isset($result['raw'])) {
$payload['raw'] = (string)$result['raw'];
}
$debugPush($label, $payload);
};
$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);
}
$debugPush('dashboard.request', [
'action' => $action,
'instance_count' => count($instances),
]);
$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 = [];
$makeError = function (string $scope, array $result): array {
return [
'scope' => $scope,
'error' => $result['error'] ?? 'error',
'error_key' => $result['error_key'] ?? '',
'hint' => $result['hint'] ?? null,
'http_code' => $result['http_code'] ?? 0,
'url' => $result['url'] ?? '',
];
};
$now = time();
$from = $now - 3600;
foreach ($instances as $id => $instance) {
$errors = [];
$summaryData = null;
$topData = null;
$queryTypesData = null;
$querySourcesData = null;
$forwardDestData = null;
$recentData = null;
$updates = ['available' => false, 'details' => []];
$versionsData = null;
$apiInfo = $detectApi($instance);
$debugPush('api.detect.result', [
'instance_id' => $id,
'instance_name' => (string)($instance['name'] ?? $id),
'version' => (int)($apiInfo['version'] ?? 0),
'sid_present' => !empty($apiInfo['sid']),
]);
if ($apiInfo['version'] === 6) {
$sid = (string)($apiInfo['sid'] ?? '');
$summary = $apiInfo['probe'];
$debugResult('dashboard.summary', $id, $instance, $summary);
if (!$summary['ok']) {
$errors[] = $makeError('summary', $summary);
}
$blocking = $v6RequestAny($instance, ['dns/blocking'], 'GET', [], $sid);
$debugResult('dashboard.blocking', $id, $instance, $blocking);
if (!$blocking['ok']) {
$errors[] = $makeError('blocking', $blocking);
}
$queries = $v6RequestAny($instance, [
'queries',
'queries/all',
'queries?from=' . $from . '&until=' . $now,
'queries/all?from=' . $from . '&until=' . $now,
], 'GET', [], $sid);
$debugResult('dashboard.queries', $id, $instance, $queries);
if (!$queries['ok']) {
$errors[] = $makeError('queries', $queries);
}
$upstreams = $v6RequestAny($instance, ['stats/upstreams', 'upstreams'], 'GET', [], $sid);
$debugResult('dashboard.upstreams', $id, $instance, $upstreams);
if (!$upstreams['ok']) {
$errors[] = $makeError('upstreams', $upstreams);
}
if ($summary['ok'] && is_array($summary['data'])) {
$sum = $summary['data'];
$queriesBlock = (array)($sum['queries'] ?? []);
$clientsBlock = (array)($sum['clients'] ?? []);
$status = 'unknown';
if ($blocking['ok'] && is_array($blocking['data'])) {
$blockingState = $blocking['data']['blocking'] ?? null;
if ($blockingState === true || $blockingState === 'enabled') {
$status = 'enabled';
} elseif ($blockingState === false || $blockingState === 'disabled') {
$status = 'disabled';
}
}
$summaryData = [
'dns_queries_today' => (int)($queriesBlock['total'] ?? 0),
'ads_blocked_today' => (int)($queriesBlock['blocked'] ?? 0),
'unique_clients' => (int)($clientsBlock['active'] ?? $clientsBlock['total'] ?? 0),
'unique_domains' => (int)($queriesBlock['unique_domains'] ?? 0),
'queries_forwarded' => (int)($queriesBlock['forwarded'] ?? 0),
'queries_cached' => (int)($queriesBlock['cached'] ?? 0),
'status' => $status,
];
if ($summaryData['dns_queries_today'] > 0) {
$summaryData['ads_percentage_today'] = round(
$summaryData['ads_blocked_today'] / $summaryData['dns_queries_today'] * 100,
2
);
} else {
$summaryData['ads_percentage_today'] = 0;
}
$queryTypesData = (array)($queriesBlock['types'] ?? []);
}
if ($queries['ok'] && is_array($queries['data'])) {
$queryList = [];
if (isset($queries['data']['queries']) && is_array($queries['data']['queries'])) {
$queryList = $queries['data']['queries'];
} elseif (isset($queries['data']['data']) && is_array($queries['data']['data'])) {
$queryList = $queries['data']['data'];
} elseif (array_is_list($queries['data'])) {
$queryList = $queries['data'];
}
$topAds = [];
$topQueries = [];
$sources = [];
$recent = [];
foreach ($queryList as $entry) {
if (!is_array($entry)) {
continue;
}
$domainRaw = $entry['domain'] ?? '';
$domain = '';
if (is_array($domainRaw)) {
$domain = (string)($domainRaw['name'] ?? $domainRaw['domain'] ?? '');
} else {
$domain = (string)$domainRaw;
}
$clientRaw = $entry['client'] ?? '';
$client = '';
if (is_array($clientRaw)) {
$name = (string)($clientRaw['name'] ?? '');
$ip = (string)($clientRaw['ip'] ?? '');
$client = $name !== '' ? ($name . ' (' . $ip . ')') : ($ip !== '' ? $ip : 'unknown');
} else {
$client = (string)$clientRaw;
}
if ($client !== '') {
$sources[$client] = ($sources[$client] ?? 0) + 1;
}
if ($domain !== '') {
$topQueries[$domain] = ($topQueries[$domain] ?? 0) + 1;
}
$statusVal = strtoupper((string)($entry['status'] ?? ''));
$isBlocked = str_contains($statusVal, 'GRAVITY')
|| str_contains($statusVal, 'BLOCK')
|| str_contains($statusVal, 'DENY')
|| str_contains($statusVal, 'CNAME');
if ($isBlocked && $domain !== '') {
$topAds[$domain] = ($topAds[$domain] ?? 0) + 1;
$recent[] = ['domain' => $domain, 'instance' => $instance['name']];
}
}
$topData = ['top_ads' => $topAds, 'top_queries' => $topQueries];
$querySourcesData = $sources;
$recentData = $recent;
}
if ($upstreams['ok'] && is_array($upstreams['data'])) {
$upList = [];
if (isset($upstreams['data']['upstreams']) && is_array($upstreams['data']['upstreams'])) {
$upList = $upstreams['data']['upstreams'];
} elseif (isset($upstreams['data']['data']) && is_array($upstreams['data']['data'])) {
$upList = $upstreams['data']['data'];
} elseif (array_is_list($upstreams['data'])) {
$upList = $upstreams['data'];
}
$forwardDestData = [];
foreach ($upList as $item) {
if (!is_array($item)) {
continue;
}
$label = (string)($item['name'] ?? $item['ip'] ?? '');
if ($label === '' && isset($item['port'])) {
$label = 'upstream:' . (string)$item['port'];
}
if ($label === '') {
continue;
}
$forwardDestData[$label] = (int)($item['count'] ?? 0);
}
}
} elseif ($apiInfo['version'] === 5) {
$summary = $apiInfo['probe'];
$topItems = $v5Request($instance, ['topItems' => 50]);
$queryTypes = $v5Request($instance, ['getQueryTypes' => 1]);
$querySources = $v5Request($instance, ['getQuerySources' => 1]);
$forwardDest = $v5Request($instance, ['getForwardDestinations' => 1]);
$recentBlocked = $v5Request($instance, ['recentBlocked' => 30]);
$versions = $v5Request($instance, ['versions' => 1]);
$debugResult('dashboard.summary', $id, $instance, $summary);
$debugResult('dashboard.topItems', $id, $instance, $topItems);
$debugResult('dashboard.queryTypes', $id, $instance, $queryTypes);
$debugResult('dashboard.querySources', $id, $instance, $querySources);
$debugResult('dashboard.forwardDestinations', $id, $instance, $forwardDest);
$debugResult('dashboard.recentBlocked', $id, $instance, $recentBlocked);
$debugResult('dashboard.versions', $id, $instance, $versions);
if (!$summary['ok']) {
$errors[] = $makeError('summary', $summary);
}
if (!$topItems['ok']) {
$errors[] = $makeError('topItems', $topItems);
}
if (!$queryTypes['ok']) {
$errors[] = $makeError('queryTypes', $queryTypes);
}
if (!$querySources['ok']) {
$errors[] = $makeError('querySources', $querySources);
}
if (!$forwardDest['ok']) {
$errors[] = $makeError('forwardDestinations', $forwardDest);
}
if (!$recentBlocked['ok']) {
$errors[] = $makeError('recentBlocked', $recentBlocked);
}
if (!$versions['ok']) {
$errors[] = $makeError('versions', $versions);
}
$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);
} else {
$probe = $apiInfo['probe'] ?? ['error' => 'unknown'];
if (is_array($probe)) {
$debugResult('dashboard.probe', $id, $instance, $probe);
}
$errors[] = $makeError('probe', is_array($probe) ? $probe : ['error' => 'unknown']);
}
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;
$debugPush('dashboard.response', [
'aggregate_summary' => $aggregate['summary'],
'instance_ids' => array_keys($instancePayloads),
]);
$respond([
'ok' => true,
'ts' => time(),
'instances' => $instancePayloads,
'aggregate' => $aggregate,
]);
}
if ($action === 'test') {
require_admin();
$target = (string)($payload['instance'] ?? '');
if ($target === '') {
$respond(['ok' => false, 'error' => 'missing_instance'], 400);
}
if (!isset($instances[$target])) {
$respond(['ok' => false, 'error' => 'invalid_instance'], 400);
}
$instance = $instances[$target];
$debugPush('test.request', [
'instance_id' => $target,
'instance_name' => (string)($instance['name'] ?? $target),
'url' => (string)($instance['url'] ?? ''),
]);
$apiInfo = $detectApi($instance);
$debugPush('test.detect.result', [
'instance_id' => $target,
'version' => (int)($apiInfo['version'] ?? 0),
'sid_present' => !empty($apiInfo['sid']),
]);
if (!empty($apiInfo['probe']) && is_array($apiInfo['probe'])) {
$debugResult('test.probe', $target, $instance, $apiInfo['probe']);
}
if ($apiInfo['version'] === 6) {
$result = $apiInfo['probe'];
if ($result['ok']) {
$respond([
'ok' => true,
'status' => 'ok',
'message' => 'Verbindung OK. API v6 antwortet.',
]);
}
$httpCode = (int)($result['http_code'] ?? 0);
$error = (string)($result['error'] ?? 'error');
$status = 'error';
$message = 'Unbekannter Fehler.';
if ($httpCode === 0) {
$status = 'unreachable';
$message = 'Host nicht erreichbar oder kein HTTP-Response.';
} elseif ($httpCode === 401 || $httpCode === 403) {
$status = 'auth';
$message = 'Passwort oder App-Passwort falsch oder nicht berechtigt.';
} elseif ($error === 'invalid_json') {
$status = 'invalid';
$message = 'API antwortet nicht mit JSON. URL pruefen.';
} else {
$status = 'error';
$message = 'API Fehler: ' . $error . ' (HTTP ' . $httpCode . ')';
}
$respond([
'ok' => false,
'status' => $status,
'message' => $message,
'http_code' => $httpCode,
'error' => $error,
'url' => (string)($result['url'] ?? ''),
]);
}
$result = $v5Request($instance, ['summaryRaw' => 1]);
if ($result['ok']) {
$respond([
'ok' => true,
'status' => 'ok',
'message' => 'Verbindung OK. API v5 antwortet.',
]);
}
$httpCode = (int)($result['http_code'] ?? 0);
$error = (string)($result['error'] ?? 'error');
$status = 'error';
$message = 'Unbekannter Fehler.';
if ($httpCode === 0) {
$status = 'unreachable';
$message = 'Host nicht erreichbar oder kein HTTP-Response.';
} elseif ($httpCode === 401 || $httpCode === 403) {
$status = 'auth';
$message = 'Passwort oder Legacy-API-Token falsch oder nicht berechtigt.';
} elseif ($error === 'invalid_json') {
$status = 'invalid';
$message = 'API antwortet nicht mit JSON. URL oder API-Pfad pruefen.';
} else {
$status = 'error';
$message = 'API Fehler: ' . $error . ' (HTTP ' . $httpCode . ')';
}
$respond([
'ok' => false,
'status' => $status,
'message' => $message,
'http_code' => $httpCode,
'error' => $error,
'url' => (string)($result['url'] ?? ''),
]);
}
if ($action === 'disable') {
require_admin();
$debugPush('disable.request', ['payload' => $payload]);
$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) {
$apiInfo = $detectApi($instance);
if ($apiInfo['version'] === 6) {
$sid = (string)($apiInfo['sid'] ?? '');
$result = $v6Request($instance, 'dns/blocking', 'POST', [
'blocking' => false,
'timer' => $minutes * 60,
], $sid);
} elseif ($apiInfo['version'] === 5) {
$result = $v5Request($instance, ['disable' => $minutes * 60]);
} else {
$result = ['ok' => false, 'error' => 'unknown_api', 'http_code' => 0, 'url' => ''];
}
$results[$id] = $result;
$debugResult('disable.result', $id, $instance, $result);
}
$respond(['ok' => true, 'results' => $results]);
}
if ($action === 'enable') {
require_admin();
$debugPush('enable.request', ['payload' => $payload]);
$target = (string)($payload['instance'] ?? 'all');
$targets = $pickInstances($target);
if (!$targets) {
$respond(['ok' => false, 'error' => 'invalid_instance'], 400);
}
$results = [];
foreach ($targets as $id => $instance) {
$apiInfo = $detectApi($instance);
if ($apiInfo['version'] === 6) {
$sid = (string)($apiInfo['sid'] ?? '');
$result = $v6Request($instance, 'dns/blocking', 'POST', [
'blocking' => true,
], $sid);
} elseif ($apiInfo['version'] === 5) {
$result = $v5Request($instance, ['enable' => 1]);
} else {
$result = ['ok' => false, 'error' => 'unknown_api', 'http_code' => 0, 'url' => ''];
}
$results[$id] = $result;
$debugResult('enable.result', $id, $instance, $result);
}
$respond(['ok' => true, 'results' => $results]);
}
if ($action === 'gravity') {
require_admin();
$debugPush('gravity.request', ['payload' => $payload]);
$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) {
$apiInfo = $detectApi($instance);
if ($apiInfo['version'] === 6) {
$sid = (string)($apiInfo['sid'] ?? '');
$result = $v6Request($instance, 'action/gravity', 'POST', [], $sid);
} elseif ($apiInfo['version'] === 5) {
$result = $v5Request($instance, ['updateGravity' => 1]);
} else {
$result = ['ok' => false, 'error' => 'unknown_api', 'http_code' => 0, 'url' => ''];
}
$results[$id] = $result;
$debugResult('gravity.result', $id, $instance, $result);
}
$respond(['ok' => true, 'results' => $results]);
}
if ($action === 'domain_add') {
require_admin();
$debugPush('domain_add.request', ['payload' => $payload]);
$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) {
$apiInfo = $detectApi($instance);
if ($apiInfo['version'] === 6) {
$result = ['ok' => false, 'error' => 'not_supported_v6', 'http_code' => 400, 'url' => ''];
} elseif ($apiInfo['version'] === 5) {
$result = $v5Request($instance, $params);
} else {
$result = ['ok' => false, 'error' => 'unknown_api', 'http_code' => 0, 'url' => ''];
}
$results[$id] = $result;
$debugResult('domain_add.result', $id, $instance, $result);
}
$respond(['ok' => true, 'results' => $results]);
}
if ($action === 'adlist_add') {
require_admin();
$debugPush('adlist_add.request', ['payload' => $payload]);
$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) {
$apiInfo = $detectApi($instance);
if ($apiInfo['version'] === 6) {
$result = ['ok' => false, 'error' => 'not_supported_v6', 'http_code' => 400, 'url' => ''];
} elseif ($apiInfo['version'] === 5) {
$result = $v5Request($instance, ['list' => 'adlist', 'add' => $url]);
} else {
$result = ['ok' => false, 'error' => 'unknown_api', 'http_code' => 0, 'url' => ''];
}
$results[$id] = $result;
$debugResult('adlist_add.result', $id, $instance, $result);
}
$respond(['ok' => true, 'results' => $results]);
}
if ($action === 'update') {
require_admin();
$debugPush('update.request', ['payload' => $payload]);
$target = (string)($payload['instance'] ?? 'primary');
$targets = $pickInstances($target);
if (!$targets) {
$respond(['ok' => false, 'error' => 'invalid_instance'], 400);
}
$results = [];
foreach ($targets as $id => $instance) {
$apiInfo = $detectApi($instance);
if ($apiInfo['version'] === 6) {
$result = ['ok' => false, 'error' => 'not_supported_v6', 'http_code' => 400, 'url' => ''];
} elseif ($apiInfo['version'] === 5) {
$result = $v5Request($instance, ['update' => 1]);
} else {
$result = ['ok' => false, 'error' => 'unknown_api', 'http_code' => 0, 'url' => ''];
}
$results[$id] = $result;
$debugResult('update.result', $id, $instance, $result);
}
$respond(['ok' => true, 'results' => $results]);
}
$respond(['ok' => false, 'error' => 'unknown_action'], 400);