Files
nexus/modules/pihole/pages/instances.php
Lars Gebhardt-Kusche f64975b5f7
All checks were successful
Deploy / deploy-staging (push) Successful in 6s
Deploy / deploy-production (push) Has been skipped
fsdfdsf
2026-04-27 02:04:27 +02:00

417 lines
16 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
$moduleName = 'pihole';
$assets = app()->assets();
$assets->addStyle('/module/pihole/asset?file=pihole.css');
$assets->addScript('/module/pihole/asset?file=pihole_instances.js', 'footer', true);
require_admin();
$settings = modules()->settings($moduleName);
$notice = null;
$error = null;
$testResults = [];
$httpRequest = static function (string $method, string $url, array $headers, ?string $body, bool $verify, int $timeout): array {
$raw = '';
$httpCode = 0;
$requestError = '';
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)) {
$requestError = 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);
if ($raw === '') {
$requestError = 'HTTP request failed';
} else {
$httpCode = 200;
}
}
if ($requestError !== '') {
return ['ok' => false, 'http_code' => $httpCode, 'error' => $requestError, 'raw' => $raw, 'url' => $url];
}
$decoded = json_decode($raw, true);
if (!is_array($decoded)) {
return ['ok' => false, 'http_code' => $httpCode, 'error' => 'invalid_json', 'raw' => $raw, 'url' => $url];
}
return ['ok' => true, 'http_code' => $httpCode, 'data' => $decoded, 'url' => $url];
};
$runConnectionTest = static function (array $instance, array $settings) use ($httpRequest): array {
$verifyTls = !isset($settings['verify_tls']) || $settings['verify_tls'] === '1' || $settings['verify_tls'] === 1 || $settings['verify_tls'] === true;
$timeout = (int)($settings['api_timeout_sec'] ?? 8);
if ($timeout <= 0) {
$timeout = 8;
}
$url = rtrim((string)($instance['url'] ?? ''), '/');
$password = trim((string)($instance['password'] ?? ''));
$v6AuthUrl = $url . '/api/auth';
$v6Payload = json_encode(['password' => $password], JSON_UNESCAPED_UNICODE);
$v6Auth = $httpRequest('POST', $v6AuthUrl, ['Accept: application/json', 'Content-Type: application/json'], $v6Payload, $verifyTls, $timeout);
if ($v6Auth['ok']) {
$session = (array)(($v6Auth['data']['session'] ?? []) ?: []);
$sid = trim((string)($session['sid'] ?? ''));
if ($sid !== '') {
$probe = $httpRequest('GET', $url . '/api/stats/summary', ['Accept: application/json', 'X-FTL-SID: ' . $sid], null, $verifyTls, $timeout);
if ($probe['ok']) {
return [
'ok' => true,
'status' => 'ok',
'message' => 'Verbindung OK. API v6 antwortet.',
'version' => 6,
'details' => ['auth' => $v6Auth, 'probe' => $probe],
];
}
return [
'ok' => false,
'status' => 'error',
'message' => 'v6 Auth OK, aber Stats-Endpoint antwortet nicht sauber.',
'version' => 6,
'details' => ['auth' => $v6Auth, 'probe' => $probe],
];
}
}
$legacyUrl = $url . '/admin/api.php?summaryRaw';
if ($password !== '') {
$legacyUrl .= '&auth=' . rawurlencode($password);
}
$v5Probe = $httpRequest('GET', $legacyUrl, ['Accept: application/json'], null, $verifyTls, $timeout);
if ($v5Probe['ok']) {
return [
'ok' => true,
'status' => 'ok',
'message' => 'Verbindung OK. API v5 antwortet.',
'version' => 5,
'details' => ['auth' => $v6Auth, 'probe' => $v5Probe],
];
}
$status = 'error';
$message = 'Unbekannter Fehler.';
$httpCode = (int)($v6Auth['http_code'] ?? $v5Probe['http_code'] ?? 0);
$requestError = (string)($v6Auth['error'] ?? $v5Probe['error'] ?? 'error');
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 ($requestError === 'invalid_json') {
$status = 'invalid';
$message = 'API antwortet nicht mit JSON. URL pruefen.';
} else {
$message = 'API Fehler: ' . $requestError . ' (HTTP ' . $httpCode . ')';
}
return [
'ok' => false,
'status' => $status,
'message' => $message,
'details' => ['auth' => $v6Auth, 'probe' => $v5Probe],
];
};
$loadInstances = function (array $settings): array {
$normalizeSecret = static function (array $row): string {
$password = trim((string)($row['password'] ?? ''));
if ($password !== '') {
return $password;
}
return trim((string)($row['token'] ?? ''));
};
$instances = [];
$rawJson = trim((string)($settings['instances_json'] ?? ''));
if ($rawJson !== '') {
$decoded = json_decode($rawJson, true);
if (is_array($decoded)) {
foreach ($decoded as $row) {
if (!is_array($row)) {
continue;
}
$id = trim((string)($row['id'] ?? ''));
$url = trim((string)($row['url'] ?? ''));
if ($id === '' || $url === '') {
continue;
}
$instances[$id] = [
'id' => $id,
'name' => trim((string)($row['name'] ?? '')) ?: $id,
'url' => $url,
'password' => $normalizeSecret($row),
'is_primary' => !empty($row['is_primary']),
];
}
}
}
if (!$instances) {
foreach (['primary', 'secondary'] as $key) {
$url = trim((string)($settings[$key . '_url'] ?? ''));
if ($url === '') {
continue;
}
$instances[$key] = [
'id' => $key,
'name' => trim((string)($settings[$key . '_name'] ?? '')) ?: ($key === 'primary' ? 'Primaer' : 'Sekundaer'),
'url' => $url,
'password' => trim((string)($settings[$key . '_token'] ?? '')),
'is_primary' => $key === 'primary',
];
}
}
return $instances;
};
$instances = $loadInstances($settings);
$sanitizeId = function (string $id): string {
$id = preg_replace('/[^a-zA-Z0-9_-]/', '', $id);
return trim((string)$id);
};
$saveInstances = function (array $settings, array $instances): void {
$payload = $settings;
$payload['instances_json'] = json_encode(array_values($instances), JSON_UNESCAPED_UNICODE);
modules()->saveSettings('pihole', $payload);
};
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$deleteId = trim((string)($_POST['delete_id'] ?? ''));
$testId = trim((string)($_POST['test_id'] ?? ''));
$currentId = trim((string)($_POST['current_id'] ?? ''));
$instanceId = trim((string)($_POST['instance_id'] ?? ''));
$name = trim((string)($_POST['name'] ?? ''));
$url = trim((string)($_POST['url'] ?? ''));
$password = trim((string)($_POST['password'] ?? ''));
$isPrimary = isset($_POST['is_primary']);
if ($testId !== '') {
if (isset($instances[$testId])) {
module_debug_push('pihole', [
'label' => 'connection.test.start',
'instance_id' => $testId,
'instance_name' => (string)($instances[$testId]['name'] ?? $testId),
'url' => (string)($instances[$testId]['url'] ?? ''),
]);
$result = $runConnectionTest($instances[$testId], $settings);
$testResults[$testId] = $result;
module_debug_push('pihole', [
'label' => 'connection.test.result',
'instance_id' => $testId,
'instance_name' => (string)($instances[$testId]['name'] ?? $testId),
'result' => $result,
]);
$notice = $result['message'] ?? null;
} else {
$error = 'Test-Instanz nicht gefunden.';
}
} elseif ($deleteId !== '') {
if (isset($instances[$deleteId])) {
unset($instances[$deleteId]);
$notice = 'Instanz geloescht.';
}
} else {
$instanceId = $sanitizeId($instanceId);
if ($instanceId === '' || $url === '') {
$error = 'Bitte ID und URL angeben.';
} elseif ($currentId === '' && isset($instances[$instanceId])) {
$error = 'Die Instanz-ID ist bereits vergeben. Bitte eine eindeutige ID verwenden.';
} elseif ($currentId !== '' && $currentId !== $instanceId && isset($instances[$instanceId])) {
$error = 'Die neue Instanz-ID ist bereits vergeben. Bitte eine eindeutige ID verwenden.';
} else {
$existingPassword = '';
if ($currentId !== '' && isset($instances[$currentId])) {
$existingPassword = (string)($instances[$currentId]['password'] ?? '');
}
$passwordToStore = $password !== '' ? $password : $existingPassword;
if ($currentId !== '' && $currentId !== $instanceId) {
unset($instances[$currentId]);
}
$instances[$instanceId] = [
'id' => $instanceId,
'name' => $name !== '' ? $name : $instanceId,
'url' => $url,
'password' => $passwordToStore,
'is_primary' => $isPrimary,
];
if ($isPrimary) {
foreach ($instances as $id => &$row) {
$row['is_primary'] = ($id === $instanceId);
}
unset($row);
$settings['primary_id'] = $instanceId;
}
$notice = $currentId !== '' ? 'Instanz aktualisiert.' : 'Instanz gespeichert.';
}
}
if (!$error) {
$saveInstances($settings, $instances);
$settings = modules()->settings($moduleName);
$instances = $loadInstances($settings);
}
}
$primaryId = trim((string)($settings['primary_id'] ?? ''));
if ($primaryId === '') {
foreach ($instances as $id => $row) {
if (!empty($row['is_primary'])) {
$primaryId = $id;
break;
}
}
}
?>
<?= module_shell_header('pihole', [
'title' => 'Pi-hole Instanzen',
'description' => 'Pi-hole Instanzen hinzufuegen, bearbeiten und loeschen.',
]) ?>
<div class="module-flow">
<section class="module-box">
<div class="module-box-head">
<div>
<h2 class="module-box-title">Instanzen</h2>
<p>Pi-hole Instanzen hinzufuegen, bearbeiten und loeschen.</p>
</div>
<div style="display:flex; gap:10px; flex-wrap:wrap;">
<button class="module-button module-button--primary" type="button" data-instance-new>+ Neue Instanz</button>
</div>
</div>
<?php if ($error): ?>
<div class="setup-db-message setup-db-message--error" style="margin-top:16px;">
<?= e($error) ?>
</div>
<?php elseif ($notice): ?>
<div class="setup-db-message setup-db-message--success" style="margin-top:16px;">
<?= e($notice) ?>
</div>
<?php endif; ?>
<div class="module-box-grid module-box-grid--panels" style="margin-top:16px;">
<?php if (!$instances): ?>
<div class="module-box-soft">Keine Instanzen vorhanden.</div>
<?php else: ?>
<?php foreach ($instances as $instance): ?>
<div class="module-box-soft pihole-instance-card"
data-instance-id="<?= e((string)$instance['id']) ?>"
data-name="<?= e((string)($instance['name'] ?? '')) ?>"
data-url="<?= e((string)($instance['url'] ?? '')) ?>"
data-primary="<?= !empty($instance['is_primary']) ? '1' : '0' ?>">
<div class="pihole-instance-header">
<div>
<strong><?= e((string)($instance['name'] ?? '')) ?></strong>
<div class="muted">ID: <?= e((string)($instance['id'] ?? '')) ?></div>
<div class="muted">URL: <?= e((string)($instance['url'] ?? '')) ?></div>
</div>
<?php if (!empty($instance['is_primary']) || $instance['id'] === $primaryId): ?>
<span class="pihole-status">Primaer</span>
<?php endif; ?>
</div>
<div class="pihole-card-actions">
<button class="module-button module-button--secondary module-button--small" type="button" data-instance-edit>Bearbeiten</button>
<form method="post">
<input type="hidden" name="test_id" value="<?= e((string)($instance['id'] ?? '')) ?>">
<button class="module-button module-button--secondary module-button--small" type="submit" data-instance-test>Test Verbindung</button>
</form>
<form method="post" onsubmit="return confirm('Instanz wirklich loeschen?')">
<input type="hidden" name="delete_id" value="<?= e((string)($instance['id'] ?? '')) ?>">
<button class="module-button module-button--secondary module-button--small" type="submit">Loeschen</button>
</form>
</div>
<div class="pihole-test-result<?= !empty($testResults[$instance['id']]['status']) ? ' is-' . e((string)$testResults[$instance['id']]['status']) : '' ?>" data-instance-result><?= e((string)($testResults[$instance['id']]['message'] ?? '')) ?></div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
</section>
</div>
<div class="modal" data-instance-modal aria-hidden="true">
<div class="modal-card pihole-modal-card" role="dialog" aria-modal="true" aria-labelledby="pihole-instance-modal-title">
<div class="modal-header">
<div>
<strong id="pihole-instance-modal-title" data-instance-modal-title>Neue Instanz</strong>
<div class="muted">Pi-hole Host anlegen oder bestehende Instanz bearbeiten.</div>
</div>
<button class="icon-button" type="button" data-instance-close aria-label="Modal schliessen">×</button>
</div>
<form method="post" class="pihole-instance-form" data-instance-form>
<input type="hidden" name="current_id" value="">
<div class="form-grid pihole-instance-form-grid">
<label class="form-field pihole-form-field-wide">
<span class="muted">ID</span>
<input type="text" name="instance_id" placeholder="z.B. pihole-main" required>
<small class="muted">Die ID muss eindeutig sein und wird nur intern verwendet.</small>
</label>
<label class="form-field">
<span class="muted">Name</span>
<input type="text" name="name" placeholder="z.B. Pi-hole Main" required>
</label>
<label class="form-field">
<span class="muted">URL</span>
<input type="text" name="url" placeholder="http://pi-hole.local" required>
</label>
<label class="form-field pihole-form-field-wide">
<span class="muted">Passwort / App-Passwort</span>
<input type="password" name="password" placeholder="Pi-hole Passwort oder App-Passwort" autocomplete="new-password">
<small class="muted">Beim Bearbeiten leer lassen, um das gespeicherte Passwort unveraendert zu lassen.</small>
</label>
<label class="pihole-checkbox-field">
<input type="checkbox" name="is_primary" value="1">
<span>Als Primaer verwenden</span>
</label>
</div>
<div class="pihole-modal-actions">
<button class="cta-button" type="submit" data-instance-submit>Speichern</button>
<button class="nav-link" type="button" data-instance-cancel>Abbrechen</button>
</div>
</form>
</div>
</div>
<?= module_shell_footer() ?>