settings('pihole'); $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, ?int $timeoutOverride = null) 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 = $timeoutOverride ?? (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, ?int $timeoutOverride = null) use ($httpRequest, $clearSessionCache, $touchSessionCache): array { $base = rtrim((string)$instance['url'], '/'); $path = ltrim($path, '/'); $url = $base . '/api/' . $path; $timeout = $timeoutOverride ?? (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, ?int $timeoutOverride = null) 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, $timeoutOverride); 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, ['stats/summary', 'summary'], 'GET', [], $sid); if (!($probe['ok'] ?? false)) { $versionProbe = $v6RequestAny($instance, ['dns/blocking'], 'GET', [], $sid); if (($versionProbe['ok'] ?? false) || in_array((int)($versionProbe['http_code'] ?? 0), [401, 403], true)) { $probe = $versionProbe; } } 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, ['stats/summary', 'summary'], 'GET', [], $sid); if (!($probe['ok'] ?? false)) { $versionProbe = $v6RequestAny($instance, ['dns/blocking'], 'GET', [], $sid); if (($versionProbe['ok'] ?? false) || in_array((int)($versionProbe['http_code'] ?? 0), [401, 403], true)) { $probe = $versionProbe; } } } } 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, ['stats/summary', 'summary', 'dns/blocking'], '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; }; $resolveActionTimeout = static function () use ($moduleSettings): int { $timeout = (int)($moduleSettings['action_timeout_sec'] ?? 120); return $timeout > 0 ? $timeout : 120; }; $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]); $actionTimeout = $resolveActionTimeout(); $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, $actionTimeout); } elseif ($apiInfo['version'] === 5) { $result = $v5Request($instance, ['updateGravity' => 1], $actionTimeout); } 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]); $actionTimeout = $resolveActionTimeout(); $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], $actionTimeout); } 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);