ysdsd
This commit is contained in:
@@ -163,6 +163,41 @@
|
|||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pihole-page.is-busy {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pihole-busy-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 24px;
|
||||||
|
background: rgba(10, 14, 24, 0.28);
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
z-index: 80;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pihole-busy-overlay[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pihole-busy-card {
|
||||||
|
min-width: min(320px, 92vw);
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 18px 20px;
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
background: var(--panel);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pihole-busy-card span {
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
.pihole-instance-card {
|
.pihole-instance-card {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
background: var(--panel-2);
|
background: var(--panel-2);
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
let refreshTimer = null;
|
let refreshTimer = null;
|
||||||
let loadInFlight = false;
|
let loadInFlight = false;
|
||||||
|
let actionInFlight = false;
|
||||||
|
|
||||||
const apiCall = async (action, payload = {}) => {
|
const apiCall = async (action, payload = {}) => {
|
||||||
const res = await fetch(`/module/pihole/api?action=${encodeURIComponent(action)}`,
|
const res = await fetch(`/module/pihole/api?action=${encodeURIComponent(action)}`,
|
||||||
@@ -46,6 +47,39 @@
|
|||||||
el.textContent = value;
|
el.textContent = value;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setActionLock = (locked, message = 'Bitte warten ...') => {
|
||||||
|
actionInFlight = locked;
|
||||||
|
page.classList.toggle('is-busy', locked);
|
||||||
|
|
||||||
|
let overlay = page.querySelector('[data-pihole-busy-overlay]');
|
||||||
|
if (!overlay) {
|
||||||
|
overlay = document.createElement('div');
|
||||||
|
overlay.className = 'pihole-busy-overlay';
|
||||||
|
overlay.dataset.piholeBusyOverlay = '1';
|
||||||
|
overlay.hidden = true;
|
||||||
|
overlay.innerHTML = '<div class="pihole-busy-card"><strong>Aktion wird ausgefuehrt</strong><span data-pihole-busy-text></span></div>';
|
||||||
|
page.appendChild(overlay);
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = overlay.querySelector('[data-pihole-busy-text]');
|
||||||
|
if (text) {
|
||||||
|
text.textContent = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
overlay.hidden = !locked;
|
||||||
|
|
||||||
|
page.querySelectorAll('button, input, select, textarea').forEach((el) => {
|
||||||
|
const formControl = el;
|
||||||
|
if (locked) {
|
||||||
|
formControl.dataset.piholeWasDisabled = formControl.disabled ? 'true' : 'false';
|
||||||
|
formControl.disabled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
formControl.disabled = formControl.dataset.piholeWasDisabled === 'true';
|
||||||
|
delete formControl.dataset.piholeWasDisabled;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const statusLabel = (status) => {
|
const statusLabel = (status) => {
|
||||||
if (status === 'enabled') return 'Aktiv';
|
if (status === 'enabled') return 'Aktiv';
|
||||||
if (status === 'disabled') return 'Deaktiviert';
|
if (status === 'disabled') return 'Deaktiviert';
|
||||||
@@ -188,6 +222,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const actionLabel = action === 'enable'
|
||||||
|
? 'Pi-hole wird aktiviert ...'
|
||||||
|
: action === 'disable' || action === 'disable-custom'
|
||||||
|
? 'Pi-hole wird deaktiviert ...'
|
||||||
|
: action === 'gravity'
|
||||||
|
? 'Listen werden aktualisiert ...'
|
||||||
|
: 'Aktion wird ausgefuehrt ...';
|
||||||
|
setActionLock(true, actionLabel);
|
||||||
|
|
||||||
if (action === 'enable') {
|
if (action === 'enable') {
|
||||||
await apiCall('enable', payload);
|
await apiCall('enable', payload);
|
||||||
} else if (action === 'disable' || action === 'disable-custom') {
|
} else if (action === 'disable' || action === 'disable-custom') {
|
||||||
@@ -206,6 +249,8 @@
|
|||||||
await loadDashboard();
|
await loadDashboard();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert(`Aktion fehlgeschlagen: ${err.message}`);
|
alert(`Aktion fehlgeschlagen: ${err.message}`);
|
||||||
|
} finally {
|
||||||
|
setActionLock(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -250,7 +295,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const loadDashboard = async () => {
|
const loadDashboard = async () => {
|
||||||
if (loadInFlight) return;
|
if (loadInFlight || actionInFlight) return;
|
||||||
loadInFlight = true;
|
loadInFlight = true;
|
||||||
try {
|
try {
|
||||||
const data = await apiCall('dashboard');
|
const data = await apiCall('dashboard');
|
||||||
|
|||||||
@@ -25,6 +25,79 @@ $debugPush = static function (string $label, array $payload = []): void {
|
|||||||
module_debug_push('pihole', array_merge(['label' => $label], $payload));
|
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 {
|
$normalizeApiPath = function (string $baseUrl, string $apiPath): string {
|
||||||
$base = rtrim($baseUrl, '/');
|
$base = rtrim($baseUrl, '/');
|
||||||
$path = $apiPath;
|
$path = $apiPath;
|
||||||
@@ -138,7 +211,34 @@ $v5Request = function (array $instance, array $params) use ($normalizeApiPath, $
|
|||||||
return $httpRequest('GET', $full, ['Accept: application/json'], null, $verify, $timeout);
|
return $httpRequest('GET', $full, ['Accept: application/json'], null, $verify, $timeout);
|
||||||
};
|
};
|
||||||
|
|
||||||
$v6Auth = function (array $instance) use ($httpRequest): array {
|
$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'], '/');
|
$base = rtrim((string)$instance['url'], '/');
|
||||||
$url = $base . '/api/auth';
|
$url = $base . '/api/auth';
|
||||||
$timeout = (int)($instance['timeout'] ?? 8);
|
$timeout = (int)($instance['timeout'] ?? 8);
|
||||||
@@ -150,16 +250,26 @@ $v6Auth = function (array $instance) use ($httpRequest): array {
|
|||||||
$body = json_encode($payload, JSON_UNESCAPED_UNICODE);
|
$body = json_encode($payload, JSON_UNESCAPED_UNICODE);
|
||||||
$res = $httpRequest('POST', $url, ['Accept: application/json', 'Content-Type: application/json'], $body, $verify, $timeout);
|
$res = $httpRequest('POST', $url, ['Accept: application/json', 'Content-Type: application/json'], $body, $verify, $timeout);
|
||||||
if (!$res['ok']) {
|
if (!$res['ok']) {
|
||||||
|
$clearSessionCache($instance);
|
||||||
return $res;
|
return $res;
|
||||||
}
|
}
|
||||||
$data = (array)($res['data'] ?? []);
|
$data = (array)($res['data'] ?? []);
|
||||||
$session = (array)($data['session'] ?? []);
|
$session = (array)($data['session'] ?? []);
|
||||||
$sid = (string)($session['sid'] ?? '');
|
$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['sid'] = $sid;
|
||||||
|
$res['cached'] = false;
|
||||||
return $res;
|
return $res;
|
||||||
};
|
};
|
||||||
|
|
||||||
$v6Request = function (array $instance, string $path, string $method, array $payload, string $sid) use ($httpRequest): array {
|
$v6Request = function (array $instance, string $path, string $method, array $payload, string $sid) use ($httpRequest, $clearSessionCache, $touchSessionCache): array {
|
||||||
$base = rtrim((string)$instance['url'], '/');
|
$base = rtrim((string)$instance['url'], '/');
|
||||||
$path = ltrim($path, '/');
|
$path = ltrim($path, '/');
|
||||||
$url = $base . '/api/' . $path;
|
$url = $base . '/api/' . $path;
|
||||||
@@ -183,7 +293,17 @@ $v6Request = function (array $instance, string $path, string $method, array $pay
|
|||||||
$headers[] = 'Content-Type: application/json';
|
$headers[] = 'Content-Type: application/json';
|
||||||
}
|
}
|
||||||
|
|
||||||
return $httpRequest($method, $url, $headers, $body, $verify, $timeout);
|
$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 {
|
$v6RequestAny = function (array $instance, array $paths, string $method, array $payload, string $sid) use ($v6Request): array {
|
||||||
@@ -202,7 +322,7 @@ $v6RequestAny = function (array $instance, array $paths, string $method, array $
|
|||||||
return $last;
|
return $last;
|
||||||
};
|
};
|
||||||
|
|
||||||
$detectApi = function (array $instance) use ($v6Auth, $v6RequestAny, $v5Request): array {
|
$detectApi = function (array $instance) use ($v6Auth, $v6RequestAny, $v5Request, $clearSessionCache): array {
|
||||||
$sid = '';
|
$sid = '';
|
||||||
$authRes = null;
|
$authRes = null;
|
||||||
if (!empty($instance['password'])) {
|
if (!empty($instance['password'])) {
|
||||||
@@ -211,6 +331,14 @@ $detectApi = function (array $instance) use ($v6Auth, $v6RequestAny, $v5Request)
|
|||||||
$sid = (string)$authRes['sid'];
|
$sid = (string)$authRes['sid'];
|
||||||
|
|
||||||
$probe = $v6RequestAny($instance, ['dns/blocking', 'stats/summary', 'summary'], 'GET', [], $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];
|
return ['version' => 6, 'sid' => $sid, 'probe' => $probe, 'auth' => $authRes];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user