433 lines
28 KiB
PHP
433 lines
28 KiB
PHP
<?php
|
||
// Website- und Server-Überwachung. Alle Funktionen beginnen mit ueberwachung_.
|
||
|
||
const UEBERWACHUNG = 'ueberwachung';
|
||
|
||
require_once __DIR__ . '/meldung.php';
|
||
|
||
/**
|
||
* Selbstheilung: Fehlt nach einem Update eine Datenbank-Änderung des Plugins (ältere DeskHub-Dateien spielen sie nicht von selbst ein),
|
||
* wird sie hier nachgeholt, statt dass das Plugin mit Datenbankfehlern abgeschaltet wird.
|
||
*/
|
||
function ueberwachung_ensure_schema(bool $force = false): void
|
||
{
|
||
static $done = false;
|
||
if ($done && !$force) return;
|
||
$done = true;
|
||
try {
|
||
get_db()->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', '<p><strong>' . e($m['name']) . '</strong> ist wieder erreichbar.</p>' . 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'], '<p>Das Zertifikat von <strong>' . e($m['name']) . '</strong> wurde erneuert und ist gültig bis ' . e(date('d.m.Y', (int) $res['cert_expires'])) . '.</p>');
|
||
}
|
||
}
|
||
}
|
||
|
||
/** 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) {
|
||
}
|
||
});
|