query('SELECT first_fail_at, verify_tls, last_status, last_title FROM plugin_ueberwachung_monitors LIMIT 1'); } catch (Throwable $e) { try { plugin_run_migrations(get_db(), UEBERWACHUNG); } catch (Throwable $e2) { error_log('ueberwachung schema: ' . $e2->getMessage()); } } } function ueberwachung_config(): array { $mail = trim((string) plugin_setting(UEBERWACHUNG, 'alert_email', '')); return [ 'threshold' => max(1, min(10, (int) plugin_setting(UEBERWACHUNG, 'threshold', '2'))), 'department_id' => (int) plugin_setting(UEBERWACHUNG, 'department_id', '0'), 'priority_id' => (int) plugin_setting(UEBERWACHUNG, 'priority_id', '0'), 'create_tickets' => plugin_setting(UEBERWACHUNG, 'create_tickets', '1') === '1', 'auto_resolve' => plugin_setting(UEBERWACHUNG, 'auto_resolve', '1') === '1', 'mail' => plugin_setting(UEBERWACHUNG, 'mail', '1') === '1', 'alert_email' => $mail !== '' ? $mail : (string) (get_setting('admin_email', '') ?: (config()['site']['admin_email'] ?? '')), ]; } function ueberwachung_types(): array { return ['http' => 'Webseite (HTTP/HTTPS)', 'port' => 'Port (TCP)', 'tls' => 'Zertifikat eines Dienstes (TLS-Port)']; } /** Prüft die Eingaben eines Überwachungsziels. @return array{ok:bool,error?:string,data?:array} */ function ueberwachung_validate(array $in): array { $name = trim((string) ($in['name'] ?? '')); $type = (string) ($in['type'] ?? 'http'); $target = trim((string) ($in['target'] ?? '')); if ($name === '' || mb_strlen($name) > 120) return ['ok' => false, 'error' => 'Bitte einen Namen angeben (höchstens 120 Zeichen).']; if (!isset(ueberwachung_types()[$type])) return ['ok' => false, 'error' => 'Unbekannte Art der Prüfung.']; if ($type === 'http') { $p = parse_url($target); if (!$p || !in_array($p['scheme'] ?? '', ['http', 'https'], true) || empty($p['host']) || preg_match('/[\s\x00-\x1F]/', $target)) return ['ok' => false, 'error' => 'Bitte eine Adresse mit http:// oder https:// angeben.']; } else { if (!preg_match('/^([A-Za-z0-9.-]{1,253}|\[[0-9a-fA-F:]+\]):(\d{1,5})$/', $target, $m) || (int) $m[2] < 1 || (int) $m[2] > 65535) return ['ok' => false, 'error' => 'Bitte Host und Port im Format host:port angeben (zum Beispiel mail.example.de:993).']; } return ['ok' => true, 'data' => [ 'name' => $name, 'type' => $type, 'target' => mb_substr($target, 0, 500), 'expect_text' => $type === 'http' ? mb_substr(trim((string) ($in['expect_text'] ?? '')), 0, 200) : '', 'interval_min' => max(1, min(1440, (int) ($in['interval_min'] ?? 5))), 'timeout_sec' => max(2, min(30, (int) ($in['timeout_sec'] ?? 8))), 'cert_warn_days' => max(1, min(90, (int) ($in['cert_warn_days'] ?? 14))), 'verify_tls' => array_key_exists('verify_tls', $in) ? (!empty($in['verify_tls']) ? 1 : 0) : 1, 'is_active' => array_key_exists('is_active', $in) ? (!empty($in['is_active']) ? 1 : 0) : 1, ]]; } function ueberwachung_save(PDO $pdo, array $in, int $id = 0): array { $v = ueberwachung_validate($in); if (!$v['ok']) return $v; $d = $v['data']; if ($id === 0) { $pdo->prepare('INSERT INTO plugin_ueberwachung_monitors (name, type, target, expect_text, interval_min, timeout_sec, cert_warn_days, verify_tls, is_active, created_at) VALUES (?,?,?,?,?,?,?,?,?,?)') ->execute([$d['name'], $d['type'], $d['target'], $d['expect_text'], $d['interval_min'], $d['timeout_sec'], $d['cert_warn_days'], $d['verify_tls'], $d['is_active'], db_now()]); return ['ok' => true, 'id' => (int) $pdo->lastInsertId()]; } $pdo->prepare('UPDATE plugin_ueberwachung_monitors SET name=?, type=?, target=?, expect_text=?, interval_min=?, timeout_sec=?, cert_warn_days=?, verify_tls=?, is_active=? WHERE id=?') ->execute([$d['name'], $d['type'], $d['target'], $d['expect_text'], $d['interval_min'], $d['timeout_sec'], $d['cert_warn_days'], $d['verify_tls'], $d['is_active'], $id]); return ['ok' => true, 'id' => $id]; } function ueberwachung_delete(PDO $pdo, int $id): void { $pdo->prepare('DELETE FROM plugin_ueberwachung_events WHERE monitor_id = ?')->execute([$id]); $pdo->prepare('DELETE FROM plugin_ueberwachung_monitors WHERE id = ?')->execute([$id]); } // ── Prüfung ──────────────────────────────────────────────────────────────── /** Eine Prüfung ausführen. @return array{ok:bool,error:?string,ms:int,cert_expires:?int} (für Tests über $GLOBALS['ueberwachung_probe'] austauschbar) */ function ueberwachung_probe(array $m): array { if (isset($GLOBALS['ueberwachung_probe']) && is_callable($GLOBALS['ueberwachung_probe'])) return ($GLOBALS['ueberwachung_probe'])($m); $timeout = max(2, (int) $m['timeout_sec']); $start = microtime(true); $ms = fn() => (int) round((microtime(true) - $start) * 1000); $verifyTls = !isset($m['verify_tls']) || (int) $m['verify_tls'] === 1; $ssl = ['capture_peer_cert' => true, 'verify_peer' => $verifyTls, 'verify_peer_name' => $verifyTls]; $certInfo = []; $certTs = function ($ctx) use (&$certInfo): ?int { $params = stream_context_get_params($ctx); $cert = $params['options']['ssl']['peer_certificate'] ?? null; if (!$cert) return null; $info = @openssl_x509_parse($cert); if (!$info) return null; $certInfo = ['issuer' => (string) ($info['issuer']['O'] ?? $info['issuer']['CN'] ?? ''), 'cert_subject' => (string) ($info['subject']['CN'] ?? '')]; return (int) $info['validTo_time_t']; }; if ($m['type'] === 'http' && function_exists('curl_init') && empty($GLOBALS['ueberwachung_no_curl'])) return ueberwachung_probe_curl($m, $timeout); if ($m['type'] === 'http') { $ctx = stream_context_create(['http' => ['method' => 'GET', 'timeout' => $timeout, 'ignore_errors' => true, 'follow_location' => 1, 'max_redirects' => 5, 'header' => "User-Agent: DeskHub-Ueberwachung\r\nAccept: */*\r\n"], 'ssl' => $ssl]); $old = ini_set('default_socket_timeout', (string) $timeout); $warnings = []; set_error_handler(function (int $no, string $str) use (&$warnings): bool { $warnings[] = $str; return true; }); try { $fh = fopen((string) $m['target'], 'r', false, $ctx); } finally { restore_error_handler(); } if ($old !== false) ini_set('default_socket_timeout', (string) $old); if (!$fh) { return ['ok' => false, 'error' => ueberwachung_describe_error(implode(' | ', $warnings)), 'ms' => $ms(), 'cert_expires' => null]; } stream_set_timeout($fh, $timeout); $body = (string) stream_get_contents($fh, 65536); $meta = stream_get_meta_data($fh); fclose($fh); $status = 0; foreach ((array) ($meta['wrapper_data'] ?? []) as $h) if (is_string($h) && preg_match('#^HTTP/\S+\s+(\d+)#', $h, $mm)) $status = (int) $mm[1]; $cert = $certTs($ctx); $extra = $certInfo + ['status' => $status, 'title' => ueberwachung_page_title($body)]; if ($status < 200 || $status >= 400) return ['ok' => false, 'error' => 'HTTP-Status ' . ($status ?: 'unbekannt'), 'ms' => $ms(), 'cert_expires' => $cert] + $extra; if ($m['expect_text'] !== '' && !str_contains($body, (string) $m['expect_text'])) return ['ok' => false, 'error' => 'Der erwartete Text fehlt auf der Seite.', 'ms' => $ms(), 'cert_expires' => $cert] + $extra; if ($m['expect_text'] === '' && ($why = ueberwachung_default_page($extra['title'], $body))) return ['ok' => false, 'error' => $why, 'ms' => $ms(), 'cert_expires' => $cert] + $extra; return ['ok' => true, 'error' => null, 'ms' => $ms(), 'cert_expires' => $cert] + $extra; } [$host, $port] = [preg_replace('/:\d+$/', '', (string) $m['target']), (int) substr((string) strrchr((string) $m['target'], ':'), 1)]; $isTls = $m['type'] === 'tls'; $ctx = stream_context_create(['ssl' => $ssl + ['peer_name' => trim($host, '[]')]]); $fh = @stream_socket_client(($isTls ? 'ssl://' : 'tcp://') . $host . ':' . $port, $errno, $errstr, $timeout, STREAM_CLIENT_CONNECT, $ctx); if (!$fh) return ['ok' => false, 'error' => ueberwachung_describe_error((string) $errstr ?: 'connection refused'), 'ms' => $ms(), 'cert_expires' => null]; $cert = $isTls ? $certTs($ctx) : null; fclose($fh); return ['ok' => true, 'error' => null, 'ms' => $ms(), 'cert_expires' => $cert] + $certInfo; } /** Kennt dieses PHP keine vertrauenswürdigen Zertifizierungsstellen? Dann scheitert jede HTTPS-Prüfung mit „Zertifikat ungültig“, auch bei gesunden Seiten. */ function ueberwachung_ca_bundle_missing(): bool { // Mit open_basedir (bei Plesk üblich) lassen sich die Dateien nicht prüfen: dann nichts behaupten if ((string) ini_get('open_basedir') !== '') return false; foreach ([(string) ini_get('curl.cainfo'), (string) ini_get('openssl.cafile'), '/etc/ssl/certs/ca-certificates.crt', '/etc/pki/tls/certs/ca-bundle.crt', '/etc/ssl/cert.pem'] as $file) { if ($file !== '' && is_file($file)) return false; } $loc = function_exists('openssl_get_cert_locations') ? openssl_get_cert_locations() : []; if (!empty($loc['default_cert_file']) && is_file($loc['default_cert_file'])) return false; $dir = (string) ($loc['default_cert_dir'] ?? ''); return !($dir !== '' && is_dir($dir) && (glob($dir . '/*') ?: [])); } /** Prüfung einer Webseite mit cURL: genaue Fehlertexte, HTTP-Status und Zertifikatsdaten. */ function ueberwachung_probe_curl(array $m, int $timeout): array { $verify = !isset($m['verify_tls']) || (int) $m['verify_tls'] === 1; $body = ''; $ch = curl_init((string) $m['target']); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => true, CURLOPT_MAXREDIRS => 5, CURLOPT_TIMEOUT => $timeout, CURLOPT_CONNECTTIMEOUT => min($timeout, 10), CURLOPT_USERAGENT => 'DeskHub-Ueberwachung', CURLOPT_HTTPHEADER => ['Accept: */*'], CURLOPT_SSL_VERIFYPEER => $verify, CURLOPT_SSL_VERIFYHOST => $verify ? 2 : 0, CURLOPT_CERTINFO => true, CURLOPT_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS, // nur die ersten 64 KB lesen (reicht für den erwarteten Text) CURLOPT_WRITEFUNCTION => function ($c, string $data) use (&$body): int { $body .= $data; return strlen($body) > 65536 ? 0 : strlen($data); }, ]); $start = microtime(true); $ok = curl_exec($ch); $errno = curl_errno($ch); $error = curl_error($ch); $status = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE); $ms = (int) round((microtime(true) - $start) * 1000); $cert = null; $extra = []; $info = curl_getinfo($ch, CURLINFO_CERTINFO); if (is_array($info) && !empty($info[0]['Expire date'])) { $ts = strtotime((string) $info[0]['Expire date']); if ($ts) $cert = $ts; $issuer = (string) ($info[0]['Issuer'] ?? ''); $subject = (string) ($info[0]['Subject'] ?? ''); $extra = ['issuer' => preg_match('/O\s*=\s*([^,\/]+)/', $issuer, $x) ? trim($x[1]) : (preg_match('/CN\s*=\s*([^,\/]+)/', $issuer, $x) ? trim($x[1]) : ''), 'cert_subject' => preg_match('/CN\s*=\s*([^,\/]+)/', $subject, $x) ? trim($x[1]) : '']; } curl_close($ch); $extra += ['status' => $status]; // Zertifikatsfehler: ohne Prüfung nachsehen, ob die Seite antwortet und was für ein Zertifikat sie hat if ($ok === false && $verify && preg_match('/certificate|ssl|tls/i', $error)) { $r2 = ueberwachung_probe_curl(array_merge($m, ['verify_tls' => 0]), $timeout); $msg = ueberwachung_describe_error($error); if (!empty($r2['cert_expires']) && $r2['cert_expires'] < time()) $msg = 'Zertifikat/TLS-Fehler (Zertifikat abgelaufen): Die gesicherte Verbindung kam nicht zustande'; if (ueberwachung_ca_bundle_missing()) $msg .= ' – möglicherweise fehlt auf diesem Server die Liste vertrauenswürdiger Zertifizierungsstellen (CA-Bundle). Das Ziel kann in Ordnung sein: Zertifikatsprüfung für dieses Ziel abschalten oder curl.cainfo/openssl.cafile in der PHP-Konfiguration setzen'; elseif (!empty($r2['status'])) $msg .= ' (die Seite antwortet ohne Prüfung mit HTTP ' . $r2['status'] . ')'; return ['ok' => false, 'error' => $msg, 'ms' => $ms, 'cert_expires' => $r2['cert_expires'] ?? null] + array_intersect_key($r2, ['issuer' => 1, 'cert_subject' => 1, 'status' => 1]); } // Abbruch durch unsere 64-KB-Grenze (Fehler 23) ist kein Fehler if ($ok === false && !($errno === 23 && strlen($body) > 65536 && $status > 0)) { return ['ok' => false, 'error' => ueberwachung_describe_error($error !== '' ? $error : 'curl error ' . $errno), 'ms' => $ms, 'cert_expires' => $cert] + $extra; } $extra['title'] = ueberwachung_page_title($body); if ($status < 200 || $status >= 400) return ['ok' => false, 'error' => 'HTTP-Status ' . ($status ?: 'unbekannt'), 'ms' => $ms, 'cert_expires' => $cert] + $extra; if ($m['expect_text'] !== '' && !str_contains($body, (string) $m['expect_text'])) return ['ok' => false, 'error' => 'Der erwartete Text fehlt auf der Seite.', 'ms' => $ms, 'cert_expires' => $cert] + $extra; if ($m['expect_text'] === '' && ($why = ueberwachung_default_page($extra['title'], $body))) return ['ok' => false, 'error' => $why, 'ms' => $ms, 'cert_expires' => $cert] + $extra; return ['ok' => true, 'error' => null, 'ms' => $ms, 'cert_expires' => $cert] + $extra; } function ueberwachung_event(PDO $pdo, int $monitorId, string $kind, string $message): void { $pdo->prepare('INSERT INTO plugin_ueberwachung_events (monitor_id, kind, message, created_at) VALUES (?,?,?,?)')->execute([$monitorId, $kind, mb_substr($message, 0, 400), db_now()]); } /** Mail an den Administrator (für Tests über $GLOBALS['ueberwachung_mail'] austauschbar). */ function ueberwachung_alert(string $subject, string $html): void { $cfg = ueberwachung_config(); if (!$cfg['mail']) return; foreach (preg_split('/[,;\s]+/', $cfg['alert_email'], -1, PREG_SPLIT_NO_EMPTY) ?: [] as $to) { if (!filter_var($to, FILTER_VALIDATE_EMAIL)) continue; try { if (isset($GLOBALS['ueberwachung_mail']) && is_callable($GLOBALS['ueberwachung_mail'])) ($GLOBALS['ueberwachung_mail'])($to, $subject, $html); else send_mail($to, $subject, $html); } catch (Throwable $e) { error_log('ueberwachung mail: ' . $e->getMessage()); } } } function ueberwachung_duration(int $seconds): string { if ($seconds < 90) return $seconds . ' Sekunden'; if ($seconds < 5400) return round($seconds / 60) . ' Minuten'; if ($seconds < 172800) return round($seconds / 3600, 1) . ' Stunden'; return round($seconds / 86400, 1) . ' Tage'; } /** * Legt ein Ticket ohne Ticket-Regeln, Absender-Sperre und Antwort-Mail an. Das ist der Rückfall, wenn das normale Anlegen scheitert: * Eine Störungsmeldung darf nicht daran hängen, dass eine Regel („Spam ablehnen“) oder die Sperrliste die eigene Adresse trifft. */ function ueberwachung_create_direct(PDO $pdo, string $email, string $subject, string $message, ?int $deptId, ?int $prioId): int { $deptId = active_department_id($pdo, $deptId ?: (int) get_setting('default_department_id', '1')); if (!$deptId) throw new RuntimeException('Es ist keine Abteilung eingerichtet.'); if (!$prioId) $prioId = (int) $pdo->query('SELECT id FROM priorities ORDER BY sort_order LIMIT 1')->fetchColumn(); $p = $pdo->prepare('SELECT * FROM priorities WHERE id = ?'); $p->execute([$prioId]); $priority = $p->fetch(); if (!$priority) throw new RuntimeException('Die Priorität gibt es nicht.'); $sla = ['first_response_due_at' => null, 'resolution_due_at' => null]; try { $sla = calc_sla_dates($priority, null, $deptId, null); } catch (Throwable $e) { /* ohne Fristen */ } $number = generate_ticket_number($pdo); $now = db_now(); $pdo->prepare('INSERT INTO tickets (ticket_number, subject, requester_name, requester_email, department_id, priority_id, status, source, requester_ip, first_response_due_at, resolution_due_at, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)') ->execute([$number, mb_substr($subject, 0, 250), 'Überwachung', $email, $deptId, $prioId, 'open', 'api', '0.0.0.0', $sla['first_response_due_at'] ?? null, $sla['resolution_due_at'] ?? null, $now, $now]); $id = (int) $pdo->lastInsertId(); $pdo->prepare("INSERT INTO ticket_threads (ticket_id, author_type, message, is_internal_note, created_at) VALUES (?, 'customer', ?, 0, ?)")->execute([$id, $message, $now]); log_activity($pdo, $id, null, 'Ticket von der Überwachung angelegt'); apply_tag_to_ticket($pdo, $id, 'Überwachung'); $d = $pdo->prepare('SELECT name FROM departments WHERE id = ?'); $d->execute([$deptId]); try { notify_staff_new_ticket(['ticket_number' => $number, 'subject' => $subject, 'requester_name' => 'Überwachung', 'requester_email' => $email, 'department_id' => $deptId, 'department_name' => $d->fetchColumn() ?: null]); } catch (Throwable $e) { /* Benachrichtigung ist nachrangig */ } try { webhook_dispatch($pdo, 'ticket.created', $id); } catch (Throwable $e) { /* dito */ } return $id; } /** Legt ein Ticket an (erst auf dem normalen Weg, sonst direkt). Merkt sich den letzten Fehler für die Seite. @return int|null Ticket-ID */ function ueberwachung_open_ticket(PDO $pdo, string $subject, string $message): ?int { $cfg = ueberwachung_config(); if (!$cfg['create_tickets']) return null; $first = preg_split('/[,;\s]+/', $cfg['alert_email'], -1, PREG_SPLIT_NO_EMPTY)[0] ?? ''; $email = filter_var($first, FILTER_VALIDATE_EMAIL) ? $first : 'ueberwachung@system.invalid'; $why = ''; try { $r = public_ticket_create($pdo, [ 'name' => 'Überwachung', 'email' => $email, 'subject' => mb_substr($subject, 0, 250), 'message' => $message, 'departmentId' => $cfg['department_id'] ?: null, 'priorityId' => $cfg['priority_id'] ?: null, ], '0.0.0.0', ['activity' => 'Ticket von der Überwachung angelegt', 'tag' => 'Überwachung']); plugin_set_setting(UEBERWACHUNG, 'ticket_error', ''); return (int) $r['id']; } catch (Throwable $e) { $why = $e->getMessage(); error_log('ueberwachung ticket: ' . $why); } // Ist das Ticket trotz Fehler (zum Beispiel bei der Benachrichtigung) schon angelegt worden, nicht doppelt anlegen $dup = $pdo->prepare("SELECT id FROM tickets WHERE subject = ? AND requester_name = 'Überwachung' AND created_at >= ? ORDER BY id DESC LIMIT 1"); $dup->execute([mb_substr($subject, 0, 250), date('Y-m-d H:i:s', time() - 120)]); if ($id = $dup->fetchColumn()) { plugin_set_setting(UEBERWACHUNG, 'ticket_error', db_now() . ': ' . $why . ' (Ticket wurde trotzdem angelegt)'); return (int) $id; } try { $id = ueberwachung_create_direct($pdo, $email, $subject, $message, $cfg['department_id'] ?: null, $cfg['priority_id'] ?: null); plugin_set_setting(UEBERWACHUNG, 'ticket_error', db_now() . ': Das Ticket musste am normalen Weg vorbei angelegt werden, weil dort abgelehnt wurde: ' . $why); return $id; } catch (Throwable $e) { error_log('ueberwachung ticket (direkt): ' . $e->getMessage()); plugin_set_setting(UEBERWACHUNG, 'ticket_error', db_now() . ': Ticket konnte nicht angelegt werden: ' . $why . ' / ' . $e->getMessage()); return null; } } /** Setzt das Ticket auf „Gelöst“ und vermerkt die Entwarnung (nur wenn es noch offen ist und die Einstellung es erlaubt). */ function ueberwachung_resolve_ticket(PDO $pdo, ?int $ticketId, string $note): void { if (!$ticketId) return; $s = $pdo->prepare('SELECT status FROM tickets WHERE id = ?'); $s->execute([$ticketId]); $status = $s->fetchColumn(); if ($status === false) return; $pdo->prepare("INSERT INTO ticket_threads (ticket_id, author_type, message, is_internal_note, created_at) VALUES (?, 'system', ?, 1, ?)")->execute([$ticketId, $note, db_now()]); log_activity($pdo, $ticketId, null, 'Überwachung: ' . $note); if (!ueberwachung_config()['auto_resolve'] || in_array($status, ['resolved', 'closed'], true)) return; $now = db_now(); $pdo->prepare("UPDATE tickets SET status = 'resolved', resolved_at = ?, updated_at = ? WHERE id = ?")->execute([$now, $now, $ticketId]); webhook_dispatch($pdo, 'ticket.status_changed', $ticketId, ['previousStatus' => (string) $status]); } /** Wertet ein Prüfergebnis aus: Zustand, Ticket, Mail, Entwarnung. */ function ueberwachung_apply(PDO $pdo, array $m, array $res): void { $cfg = ueberwachung_config(); $now = db_now(); $id = (int) $m['id']; $certStr = $res['cert_expires'] ? date('Y-m-d H:i:s', (int) $res['cert_expires']) : ($m['cert_expires'] ?? null); $upd = $pdo->prepare('UPDATE plugin_ueberwachung_monitors SET last_check_at = ?, last_ms = ?, cert_expires = ?, last_status = ?, last_title = ? WHERE id = ?'); $upd->execute([$now, $res['ms'], $certStr, !empty($res['status']) ? (int) $res['status'] : null, isset($res['title']) && $res['title'] !== '' ? mb_substr((string) $res['title'], 0, 150) : null, $id]); if ($res['ok']) { $wasDown = $m['state'] === 'down'; $pdo->prepare("UPDATE plugin_ueberwachung_monitors SET state = 'up', fails = 0, last_ok_at = ?, last_error = NULL, down_since = NULL, first_fail_at = NULL, ticket_id = ? WHERE id = ?")->execute([$now, $wasDown ? null : $m['ticket_id'], $id]); if ($wasDown) { $dur = $m['down_since'] ? ueberwachung_duration(max(0, time() - (int) strtotime((string) $m['down_since']))) : 'unbekannt'; ueberwachung_event($pdo, $id, 'up', 'Wieder erreichbar nach ' . $dur . '.'); ueberwachung_resolve_ticket($pdo, $m['ticket_id'] ? (int) $m['ticket_id'] : null, ueberwachung_up_note($m, $dur)); ueberwachung_alert(ueberwachung_subject('Entwarnung', $m) . ' ist wieder erreichbar', '

' . e($m['name']) . ' ist wieder erreichbar.

' . ueberwachung_table([['Dienst', (string) $m['name']], ['Adresse', (string) $m['target']], ['Ausfall', ueberwachung_dt((string) ($m['first_fail_at'] ?? $m['down_since'] ?? '')) . ' bis ' . date('d.m.Y H:i:s')], ['Dauer', $dur], ['Antwortzeit jetzt', (int) $res['ms'] . ' ms']])); } } else { $fails = (int) $m['fails'] + 1; $firstFail = ($fails === 1 || empty($m['first_fail_at'])) ? $now : (string) $m['first_fail_at']; $pdo->prepare('UPDATE plugin_ueberwachung_monitors SET fails = ?, last_error = ?, first_fail_at = ? WHERE id = ?')->execute([$fails, mb_substr((string) $res['error'], 0, 300), $firstFail, $id]); if ($fails >= $cfg['threshold'] && $m['state'] !== 'down') { $body = ueberwachung_down_body($m, $res, $fails, $firstFail); $ticket = ueberwachung_open_ticket($pdo, ueberwachung_subject('Ausfall', $m), $body); $pdo->prepare("UPDATE plugin_ueberwachung_monitors SET state = 'down', down_since = ?, ticket_id = ? WHERE id = ?")->execute([$firstFail, $ticket, $id]); ueberwachung_event($pdo, $id, 'down', 'Ausfall: ' . $res['error'] . ($ticket ? '' : ($cfg['create_tickets'] ? ' (Ticket konnte nicht angelegt werden, siehe Hinweis oben)' : ' (Tickets sind ausgeschaltet)'))); ueberwachung_alert(ueberwachung_subject('Ausfall', $m), $body); } elseif ($fails < $cfg['threshold'] && $m['state'] === 'unknown') { $pdo->prepare("UPDATE plugin_ueberwachung_monitors SET state = 'up' WHERE id = ? AND state = 'unknown'")->execute([$id]); } } // Zertifikat if ($res['cert_expires']) { $days = (int) floor(((int) $res['cert_expires'] - time()) / 86400); $warn = (int) $m['cert_warn_days']; if ($days <= $warn && !$m['cert_ticket_id']) { $text = $days < 0 ? 'Das Zertifikat von ' . $m['name'] . ' ist seit ' . abs($days) . ' Tagen abgelaufen.' : 'Das Zertifikat von ' . $m['name'] . ' läuft in ' . $days . ' Tagen ab (' . date('d.m.Y', (int) $res['cert_expires']) . ').'; $certBody = ueberwachung_cert_body($m, $res, $days); $ticket = ueberwachung_open_ticket($pdo, ueberwachung_subject('Zertifikat', $m), $certBody); $pdo->prepare('UPDATE plugin_ueberwachung_monitors SET cert_ticket_id = ? WHERE id = ?')->execute([$ticket ?: -1, $id]); ueberwachung_event($pdo, $id, 'cert', $text); ueberwachung_alert(ueberwachung_subject($days < 0 ? 'Zertifikat abgelaufen' : 'Zertifikat läuft bald ab', $m), $certBody); } elseif ($days > $warn && $m['cert_ticket_id']) { $pdo->prepare('UPDATE plugin_ueberwachung_monitors SET cert_ticket_id = NULL WHERE id = ?')->execute([$id]); ueberwachung_event($pdo, $id, 'cert_ok', 'Zertifikat erneuert, gültig bis ' . date('d.m.Y', (int) $res['cert_expires']) . '.'); ueberwachung_resolve_ticket($pdo, (int) $m['cert_ticket_id'] > 0 ? (int) $m['cert_ticket_id'] : null, 'Zertifikat erneuert (' . $m['name'] . '), gültig bis ' . date('d.m.Y', (int) $res['cert_expires']) . '.'); ueberwachung_alert('Zertifikat erneuert: ' . $m['name'], '

Das Zertifikat von ' . e($m['name']) . ' wurde erneuert und ist gültig bis ' . e(date('d.m.Y', (int) $res['cert_expires'])) . '.

'); } } } /** Prüft ein Ziel jetzt. */ function ueberwachung_check(PDO $pdo, int $id): bool { $s = $pdo->prepare('SELECT * FROM plugin_ueberwachung_monitors WHERE id = ?'); $s->execute([$id]); $m = $s->fetch(); if (!$m) return false; try { $res = ueberwachung_probe($m); } catch (Throwable $e) { $res = ['ok' => false, 'error' => 'Prüfung fehlgeschlagen: ' . mb_substr($e->getMessage(), 0, 150), 'ms' => 0, 'cert_expires' => null]; } ueberwachung_apply($pdo, $m, $res); return true; } /** Wartezeit bis zur nächsten Prüfung in Sekunden: der eingestellte Abstand, nach einem ersten Fehlschlag (Ausfall noch nicht gemeldet) nur eine Minute. */ function ueberwachung_next_wait(array $m): int { if ((int) ($m['fails'] ?? 0) > 0 && ($m['state'] ?? '') !== 'down') return 45; return max(45, (int) $m['interval_min'] * 60 - 15); } /** Hintergrundlauf: fällige Ziele prüfen (höchstens 6 pro Lauf, damit der Lauf kurz bleibt). */ function ueberwachung_tick(PDO $pdo): int { plugin_set_setting(UEBERWACHUNG, 'last_tick', (string) time()); $due = []; foreach ($pdo->query('SELECT id, interval_min, last_check_at, fails, state FROM plugin_ueberwachung_monitors WHERE is_active = 1 ORDER BY last_check_at')->fetchAll() as $m) { if ($m['last_check_at'] === null || time() - (int) strtotime((string) $m['last_check_at']) >= ueberwachung_next_wait($m)) $due[] = (int) $m['id']; if (count($due) >= 6) break; } foreach ($due as $id) ueberwachung_check($pdo, $id); if ($due && time() - (int) plugin_setting(UEBERWACHUNG, 'last_prune', '0') > 86400) { plugin_set_setting(UEBERWACHUNG, 'last_prune', (string) time()); $pdo->prepare('DELETE FROM plugin_ueberwachung_events WHERE created_at < ?')->execute([date('Y-m-d H:i:s', time() - 90 * 86400)]); } return count($due); } plugin_add_hook('plugin.uninstall.' . UEBERWACHUNG, function () { try { get_db()->exec('DROP TABLE IF EXISTS plugin_ueberwachung_events'); get_db()->exec('DROP TABLE IF EXISTS plugin_ueberwachung_monitors'); } catch (Throwable $e) { } });