Upload via GUI (335 Dateien)
This commit is contained in:
@@ -1,113 +1,204 @@
|
||||
<?php
|
||||
/**
|
||||
* Systemüberwachung: erkennt, wenn der Mail-Abruf oder die Hintergrundjobs nicht mehr laufen,
|
||||
* benachrichtigt Administratoren per E-Mail und liefert eine Liste von Problemen für die
|
||||
* Weboberfläche und die Apps (Banner).
|
||||
*/
|
||||
|
||||
/** Ab so vielen Fehlversuchen in Folge gilt ein Postfach als ausgefallen (bei 5-Minuten-Takt ≈ 15 Min.). */
|
||||
const HEALTH_FAIL_THRESHOLD = 3;
|
||||
/** Wiederholte Alarm-Mail frühestens nach so vielen Sekunden, solange der Fehler anhält. */
|
||||
const HEALTH_ALERT_REPEAT = 86400;
|
||||
|
||||
/** E-Mail-Adressen aller aktiven Administratoren. */
|
||||
function health_admin_emails(PDO $pdo): array
|
||||
{
|
||||
return $pdo->query("SELECT email FROM staff WHERE is_active = 1 AND role = 'admin'")->fetchAll(PDO::FETCH_COLUMN) ?: [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Hält das Ergebnis eines Abrufs für ein Postfach fest. $error = null bedeutet Erfolg.
|
||||
* Verschickt eine Alarm-Mail an die Administratoren, wenn der Fehler anhält, und eine
|
||||
* Entwarnung, wenn das Postfach wieder erreichbar ist.
|
||||
*/
|
||||
function health_record_mail_result(PDO $pdo, array $account, ?string $error): void
|
||||
{
|
||||
$now = db_now();
|
||||
$label = $account['label'] ?: $account['username'];
|
||||
if ($error === null) {
|
||||
$wasAlerted = !empty($account['alert_sent_at']);
|
||||
$pdo->prepare('UPDATE imap_accounts SET last_success_at = ?, last_error = NULL, fail_count = 0, alert_sent_at = NULL WHERE id = ?')->execute([$now, $account['id']]);
|
||||
if ($wasAlerted) {
|
||||
foreach (health_admin_emails($pdo) as $to) {
|
||||
send_mail($to, "DeskHub: Postfach „{$label}“ ist wieder erreichbar",
|
||||
'<p>Der Mail-Abruf für das Postfach <strong>' . e($label) . '</strong> funktioniert wieder.</p>');
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
$fails = (int) ($account['fail_count'] ?? 0) + 1;
|
||||
$pdo->prepare('UPDATE imap_accounts SET last_error = ?, fail_count = ? WHERE id = ?')->execute([mb_substr($error, 0, 500), $fails, $account['id']]);
|
||||
$lastAlert = !empty($account['alert_sent_at']) ? strtotime($account['alert_sent_at']) : 0;
|
||||
if ($fails >= HEALTH_FAIL_THRESHOLD && (time() - $lastAlert) >= HEALTH_ALERT_REPEAT) {
|
||||
$since = $account['last_success_at'] ? date('d.m.Y H:i', strtotime($account['last_success_at'])) : 'noch nie';
|
||||
$sent = false;
|
||||
foreach (health_admin_emails($pdo) as $to) {
|
||||
$sent = send_mail($to, "DeskHub: Mail-Abruf für „{$label}“ fehlgeschlagen",
|
||||
'<p>Der Mail-Abruf für das Postfach <strong>' . e($label) . '</strong> (' . e($account['host']) . ') schlägt seit '
|
||||
. $fails . ' Versuchen fehl. Eingehende Mails werden in dieser Zeit <strong>nicht</strong> als Tickets angelegt.</p>'
|
||||
. '<p>Letzter Fehler: <code>' . e($error) . '</code><br>Letzter erfolgreicher Abruf: ' . e($since) . '</p>'
|
||||
. '<p>Bitte Zugangsdaten und Server unter E-Mail-Adressen → Entferntes Postfach prüfen.</p>') || $sent;
|
||||
}
|
||||
// Auch ohne versendete Mail als „gemeldet“ markieren, damit nicht bei jedem Lauf neu versucht wird
|
||||
$pdo->prepare('UPDATE imap_accounts SET alert_sent_at = ? WHERE id = ?')->execute([$now, $account['id']]);
|
||||
}
|
||||
}
|
||||
|
||||
/** Merkt sich den Erfolg bzw. Misserfolg des letzten Mailversands (für die Statusanzeige). */
|
||||
function health_record_send_result(bool $ok, string $error = ''): void
|
||||
{
|
||||
try {
|
||||
if ($ok) { set_setting('mail_send_last_ok', db_now()); return; }
|
||||
set_setting('mail_send_last_error', db_now() . '|' . mb_substr($error, 0, 300));
|
||||
} catch (Throwable $e) {
|
||||
// Überwachung darf den Versand nie beeinträchtigen
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Liefert die aktuellen Probleme als Liste von ['level' => 'error'|'warning', 'code' => ..., 'message' => ...].
|
||||
* Leer = alles in Ordnung. Rein lesend, daher auch bei jedem Seitenaufruf aufrufbar.
|
||||
*/
|
||||
function system_health(PDO $pdo): array
|
||||
{
|
||||
$issues = [];
|
||||
// Das zuletzt geprüfte Backup lässt sich nicht wiederherstellen
|
||||
$bk = json_decode((string) get_setting('backup_last_check', ''), true);
|
||||
if (is_array($bk) && array_key_exists('ok', $bk) && !$bk['ok']) {
|
||||
$issues[] = ['level' => 'error', 'code' => 'backup_broken', 'message' => 'Das Backup ' . ($bk['file'] ?? '') . ' ist nicht wiederherstellbar: ' . implode(' ', (array) ($bk['errors'] ?? [])) . ' Bitte unter Backup ein neues erstellen.'];
|
||||
}
|
||||
$accounts = $pdo->query('SELECT * FROM imap_accounts WHERE is_active = 1')->fetchAll();
|
||||
if ($accounts) {
|
||||
if (!function_exists('imap_open')) {
|
||||
$issues[] = ['level' => 'error', 'code' => 'imap_missing', 'message' => 'Die PHP-IMAP-Erweiterung fehlt – eingehende Mails können nicht abgerufen werden.'];
|
||||
}
|
||||
foreach ($accounts as $a) {
|
||||
$label = $a['label'] ?: $a['username'];
|
||||
$fails = (int) ($a['fail_count'] ?? 0);
|
||||
if ($fails >= HEALTH_FAIL_THRESHOLD) {
|
||||
$since = !empty($a['last_success_at']) ? ' Letzter Erfolg: ' . date('d.m.Y H:i', strtotime($a['last_success_at'])) . '.' : '';
|
||||
$issues[] = ['level' => 'error', 'code' => 'mailbox_down', 'message' => "Postfach „{$label}“ ist nicht erreichbar ({$fails} Fehlversuche in Folge).{$since} " . ($a['last_error'] ?? '')];
|
||||
} elseif ($fails > 0) {
|
||||
$issues[] = ['level' => 'warning', 'code' => 'mailbox_flaky', 'message' => "Postfach „{$label}“: letzter Abruf fehlgeschlagen. " . ($a['last_error'] ?? '')];
|
||||
}
|
||||
}
|
||||
// Der Abruf selbst läuft nicht mehr (Cronjob/Seitenaufrufe bleiben aus)
|
||||
$last = get_setting('cron_fetch_emails_last_run', '');
|
||||
if (get_setting('pseudo_cron_enabled', '1') === '1' || $last !== '') {
|
||||
if ($last !== '' && (time() - strtotime($last)) > 1200) {
|
||||
$issues[] = ['level' => 'warning', 'code' => 'fetch_stale', 'message' => 'Der Mail-Abruf wurde seit ' . cron_last_run_label($last) . ' nicht ausgeführt. Läuft der Cronjob (cron/fetch_emails.php)?'];
|
||||
}
|
||||
}
|
||||
}
|
||||
$lastErr = (string) get_setting('mail_send_last_error', '');
|
||||
if ($lastErr !== '') {
|
||||
[$at, $msg] = array_pad(explode('|', $lastErr, 2), 2, '');
|
||||
$ok = (string) get_setting('mail_send_last_ok', '');
|
||||
if ($at !== '' && strtotime($at) > time() - 86400 && ($ok === '' || strtotime($ok) < strtotime($at))) {
|
||||
$issues[] = ['level' => 'warning', 'code' => 'send_failed', 'message' => 'Der letzte E-Mail-Versand ist fehlgeschlagen (' . date('d.m.Y H:i', strtotime($at)) . '): ' . $msg];
|
||||
}
|
||||
}
|
||||
return $issues;
|
||||
}
|
||||
<?php
|
||||
/**
|
||||
* Systemüberwachung: erkennt, wenn der Mail-Abruf oder die Hintergrundjobs nicht mehr laufen,
|
||||
* benachrichtigt Administratoren per E-Mail und liefert eine Liste von Problemen für die
|
||||
* Weboberfläche und die Apps (Banner).
|
||||
*/
|
||||
|
||||
/** Ab so vielen Fehlversuchen in Folge gilt ein Postfach als ausgefallen (bei 5-Minuten-Takt ≈ 15 Min.). */
|
||||
const HEALTH_FAIL_THRESHOLD = 3;
|
||||
/** Wiederholte Alarm-Mail frühestens nach so vielen Sekunden, solange der Fehler anhält. */
|
||||
const HEALTH_ALERT_REPEAT = 86400;
|
||||
|
||||
/** E-Mail-Adressen aller aktiven Administratoren. */
|
||||
function health_admin_emails(PDO $pdo): array
|
||||
{
|
||||
return $pdo->query("SELECT email FROM staff WHERE is_active = 1 AND role = 'admin'")->fetchAll(PDO::FETCH_COLUMN) ?: [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Hält das Ergebnis eines Abrufs für ein Postfach fest. $error = null bedeutet Erfolg.
|
||||
* Verschickt eine Alarm-Mail an die Administratoren, wenn der Fehler anhält, und eine
|
||||
* Entwarnung, wenn das Postfach wieder erreichbar ist.
|
||||
*/
|
||||
function health_record_mail_result(PDO $pdo, array $account, ?string $error): void
|
||||
{
|
||||
$now = db_now();
|
||||
$label = $account['label'] ?: $account['username'];
|
||||
if ($error === null) {
|
||||
$wasAlerted = !empty($account['alert_sent_at']);
|
||||
$pdo->prepare('UPDATE imap_accounts SET last_success_at = ?, last_error = NULL, fail_count = 0, alert_sent_at = NULL WHERE id = ?')->execute([$now, $account['id']]);
|
||||
if ($wasAlerted) {
|
||||
foreach (health_admin_emails($pdo) as $to) {
|
||||
send_mail($to, "DeskHub: Postfach „{$label}“ ist wieder erreichbar",
|
||||
'<p>Der Mail-Abruf für das Postfach <strong>' . e($label) . '</strong> funktioniert wieder.</p>');
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
$fails = (int) ($account['fail_count'] ?? 0) + 1;
|
||||
$pdo->prepare('UPDATE imap_accounts SET last_error = ?, fail_count = ? WHERE id = ?')->execute([mb_substr($error, 0, 500), $fails, $account['id']]);
|
||||
$lastAlert = !empty($account['alert_sent_at']) ? strtotime($account['alert_sent_at']) : 0;
|
||||
if ($fails >= HEALTH_FAIL_THRESHOLD && (time() - $lastAlert) >= HEALTH_ALERT_REPEAT) {
|
||||
$since = $account['last_success_at'] ? date('d.m.Y H:i', strtotime($account['last_success_at'])) : 'noch nie';
|
||||
$sent = false;
|
||||
foreach (health_admin_emails($pdo) as $to) {
|
||||
$sent = send_mail($to, "DeskHub: Mail-Abruf für „{$label}“ fehlgeschlagen",
|
||||
'<p>Der Mail-Abruf für das Postfach <strong>' . e($label) . '</strong> (' . e($account['host']) . ') schlägt seit '
|
||||
. $fails . ' Versuchen fehl. Eingehende Mails werden in dieser Zeit <strong>nicht</strong> als Tickets angelegt.</p>'
|
||||
. '<p>Letzter Fehler: <code>' . e($error) . '</code><br>Letzter erfolgreicher Abruf: ' . e($since) . '</p>'
|
||||
. '<p>Bitte Zugangsdaten und Server unter E-Mail-Adressen → Entferntes Postfach prüfen.</p>') || $sent;
|
||||
}
|
||||
// Auch ohne versendete Mail als „gemeldet“ markieren, damit nicht bei jedem Lauf neu versucht wird
|
||||
$pdo->prepare('UPDATE imap_accounts SET alert_sent_at = ? WHERE id = ?')->execute([$now, $account['id']]);
|
||||
}
|
||||
}
|
||||
|
||||
/** Merkt sich den Erfolg bzw. Misserfolg des letzten Mailversands (für die Statusanzeige). */
|
||||
function health_record_send_result(bool $ok, string $error = ''): void
|
||||
{
|
||||
try {
|
||||
if ($ok) { set_setting('mail_send_last_ok', db_now()); return; }
|
||||
set_setting('mail_send_last_error', db_now() . '|' . mb_substr($error, 0, 300));
|
||||
} catch (Throwable $e) {
|
||||
// Überwachung darf den Versand nie beeinträchtigen
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Liefert die aktuellen Probleme als Liste von ['level' => 'error'|'warning', 'code' => ..., 'message' => ...].
|
||||
* Leer = alles in Ordnung. Rein lesend, daher auch bei jedem Seitenaufruf aufrufbar.
|
||||
*/
|
||||
function system_health(PDO $pdo): array
|
||||
{
|
||||
$issues = [];
|
||||
// Das zuletzt geprüfte Backup lässt sich nicht wiederherstellen
|
||||
$bk = json_decode((string) get_setting('backup_last_check', ''), true);
|
||||
if (is_array($bk) && array_key_exists('ok', $bk) && !$bk['ok']) {
|
||||
$issues[] = ['level' => 'error', 'code' => 'backup_broken', 'message' => 'Das Backup ' . ($bk['file'] ?? '') . ' ist nicht wiederherstellbar: ' . implode(' ', (array) ($bk['errors'] ?? [])) . ' Bitte unter Backup ein neues erstellen.'];
|
||||
}
|
||||
$accounts = $pdo->query('SELECT * FROM imap_accounts WHERE is_active = 1')->fetchAll();
|
||||
if ($accounts) {
|
||||
if (!function_exists('imap_open')) {
|
||||
$issues[] = ['level' => 'error', 'code' => 'imap_missing', 'message' => 'Die PHP-IMAP-Erweiterung fehlt – eingehende Mails können nicht abgerufen werden.'];
|
||||
}
|
||||
foreach ($accounts as $a) {
|
||||
$label = $a['label'] ?: $a['username'];
|
||||
$fails = (int) ($a['fail_count'] ?? 0);
|
||||
if ($fails >= HEALTH_FAIL_THRESHOLD) {
|
||||
$since = !empty($a['last_success_at']) ? ' Letzter Erfolg: ' . date('d.m.Y H:i', strtotime($a['last_success_at'])) . '.' : '';
|
||||
$issues[] = ['level' => 'error', 'code' => 'mailbox_down', 'message' => "Postfach „{$label}“ ist nicht erreichbar ({$fails} Fehlversuche in Folge).{$since} " . ($a['last_error'] ?? '')];
|
||||
} elseif ($fails > 0) {
|
||||
$issues[] = ['level' => 'warning', 'code' => 'mailbox_flaky', 'message' => "Postfach „{$label}“: letzter Abruf fehlgeschlagen. " . ($a['last_error'] ?? '')];
|
||||
}
|
||||
}
|
||||
// Der Abruf selbst läuft nicht mehr (Cronjob/Seitenaufrufe bleiben aus)
|
||||
$last = get_setting('cron_fetch_emails_last_run', '');
|
||||
if (get_setting('pseudo_cron_enabled', '1') === '1' || $last !== '') {
|
||||
if ($last !== '' && (time() - strtotime($last)) > 1200) {
|
||||
$issues[] = ['level' => 'warning', 'code' => 'fetch_stale', 'message' => 'Der Mail-Abruf wurde seit ' . cron_last_run_label($last) . ' nicht ausgeführt. Läuft der Cronjob (cron/fetch_emails.php)?'];
|
||||
}
|
||||
}
|
||||
}
|
||||
$lastErr = (string) get_setting('mail_send_last_error', '');
|
||||
if ($lastErr !== '') {
|
||||
[$at, $msg] = array_pad(explode('|', $lastErr, 2), 2, '');
|
||||
$ok = (string) get_setting('mail_send_last_ok', '');
|
||||
if ($at !== '' && strtotime($at) > time() - 86400 && ($ok === '' || strtotime($ok) < strtotime($at))) {
|
||||
$issues[] = ['level' => 'warning', 'code' => 'send_failed', 'message' => 'Der letzte E-Mail-Versand ist fehlgeschlagen (' . date('d.m.Y H:i', strtotime($at)) . '): ' . $msg];
|
||||
}
|
||||
}
|
||||
return $issues;
|
||||
}
|
||||
|
||||
// ── Fehler-Mail bei schweren PHP-Fehlern (wie bei WordPress) ───────────────────
|
||||
|
||||
/** Fehlerarten, die eine Seite abbrechen. */
|
||||
const ERROR_ALERT_FATAL = [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR, E_RECOVERABLE_ERROR];
|
||||
/** Dieselbe Meldung frühestens nach so vielen Sekunden erneut mailen. */
|
||||
const ERROR_ALERT_REPEAT = 3600;
|
||||
/** Höchstens so viele Fehler-Mails pro Stunde insgesamt (schützt das Postfach bei einem Fehler auf jeder Seite). */
|
||||
const ERROR_ALERT_MAX_PER_HOUR = 10;
|
||||
|
||||
/** Empfänger: eigene Adresse aus den Einstellungen, sonst Admin-Adresse, sonst alle aktiven Administratoren. */
|
||||
function error_alert_recipients(PDO $pdo): array
|
||||
{
|
||||
$own = trim((string) get_setting('error_alert_email', ''));
|
||||
if ($own !== '') return array_values(array_filter(array_map('trim', preg_split('/[,;\s]+/', $own) ?: []), fn($m) => filter_var($m, FILTER_VALIDATE_EMAIL)));
|
||||
$admin = trim((string) get_setting('admin_email', '') ?: (string) (config()['site']['admin_email'] ?? ''));
|
||||
if (filter_var($admin, FILTER_VALIDATE_EMAIL) && $admin !== 'support@example.com') return [$admin];
|
||||
return health_admin_emails($pdo);
|
||||
}
|
||||
|
||||
/** Betreff und Text der Mail. Nur Pfad der Adresse (keine Parameter, dort können Zugangscodes stehen), keine Formular- oder Cookie-Daten. */
|
||||
function error_alert_compose(array $err, array $context = []): array
|
||||
{
|
||||
$root = str_replace('\\', '/', dirname(__DIR__)) . '/';
|
||||
$file = str_replace($root, '', str_replace('\\', '/', (string) ($err['file'] ?? '')));
|
||||
$msg = mb_substr(str_replace($root, '', str_replace('\\', '/', (string) ($err['message'] ?? ''))), 0, 1500);
|
||||
$site = (string) get_setting('display_name', 'DeskHub');
|
||||
$path = (string) ($context['path'] ?? (parse_url((string) ($_SERVER['REQUEST_URI'] ?? ''), PHP_URL_PATH) ?: '(Hintergrundaufruf)'));
|
||||
$headline = !empty($err['headline']) ? e((string) $err['headline']) : 'Auf <strong>' . e($site) . '</strong> ist ein schwerer Fehler aufgetreten. Die aufgerufene Seite konnte nicht angezeigt werden.';
|
||||
$html = '<p>' . $headline . '</p>'
|
||||
. '<p><strong>Fehler:</strong><br><code>' . nl2br(e($msg)) . '</code></p>'
|
||||
. '<p><strong>Datei:</strong> ' . e($file) . ((int) ($err['line'] ?? 0) > 0 ? ', Zeile ' . (int) $err['line'] : '') . '<br>'
|
||||
. '<strong>Seite:</strong> ' . e($path) . '<br>'
|
||||
. '<strong>Zeit:</strong> ' . e(date('d.m.Y H:i:s')) . '<br>'
|
||||
. '<strong>Version:</strong> DeskHub ' . e((string) (defined('DESKHUB_VERSION') ? DESKHUB_VERSION : '?')) . ', PHP ' . e(PHP_VERSION) . '</p>'
|
||||
. '<p style="color:#666;font-size:12px">Dieselbe Meldung wird höchstens einmal pro Stunde gesendet. Ausschalten oder Empfänger ändern: Einstellungen → Allgemein.</p>';
|
||||
return [!empty($err['subject']) ? "{$site}: " . (string) $err['subject'] : "{$site}: Schwerer Fehler auf der Webseite", $html];
|
||||
}
|
||||
|
||||
/**
|
||||
* Meldet einen Fehler, der die Seite nicht abbricht, aber Aufmerksamkeit braucht (zum Beispiel ein Plugin, das Fehler wirft, oder ein
|
||||
* fehlgeschlagener Hintergrundjob). Gleiche Ein-/Ausschalter, Empfänger und Drosselung wie bei schweren Fehlern.
|
||||
*/
|
||||
function error_alert_notice(string $subject, string $headline, string $where, string $message): void
|
||||
{
|
||||
try {
|
||||
error_alert_dispatch(get_db(), ['type' => E_USER_WARNING, 'message' => $message, 'file' => $where, 'line' => 0, 'subject' => $subject, 'headline' => $headline]);
|
||||
} catch (Throwable $e) {
|
||||
error_log('error_alert_notice: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft Ein/Aus und Drosselung und verschickt die Mail. $send ist nur für Tests austauschbar (Standard: send_mail).
|
||||
* Liefert die Zahl der Empfänger, an die gesendet wurde.
|
||||
*/
|
||||
function error_alert_dispatch(PDO $pdo, array $err, ?callable $send = null): int
|
||||
{
|
||||
if (get_setting('error_alert', '1') !== '1') return 0;
|
||||
$fp = sha1(preg_replace('/\d+/', '#', (string) ($err['file'] ?? '') . ':' . substr((string) ($err['message'] ?? ''), 0, 200)));
|
||||
$now = time();
|
||||
$log = json_decode((string) get_setting('error_alert_log', '[]'), true);
|
||||
$log = array_values(array_filter(is_array($log) ? $log : [], fn($r) => is_array($r) && $now - (int) ($r[0] ?? 0) < 86400));
|
||||
$lastHour = array_filter($log, fn($r) => $now - (int) $r[0] < 3600);
|
||||
if (count($lastHour) >= ERROR_ALERT_MAX_PER_HOUR) return 0;
|
||||
foreach ($lastHour as $r) if (($r[1] ?? '') === $fp && $now - (int) $r[0] < ERROR_ALERT_REPEAT) return 0;
|
||||
// erst merken, dann senden: Schlägt das Senden selbst fehl, folgt keine Endlosschleife
|
||||
$log[] = [$now, $fp];
|
||||
set_setting('error_alert_log', json_encode($log));
|
||||
[$subject, $html] = error_alert_compose($err);
|
||||
$sent = 0;
|
||||
foreach (error_alert_recipients($pdo) as $to) {
|
||||
if (($send ?? $GLOBALS['error_alert_send'] ?? 'send_mail')($to, $subject, $html)) $sent++;
|
||||
}
|
||||
return $sent;
|
||||
}
|
||||
|
||||
/** Wird am Ende jeder Anfrage aufgerufen: war es ein schwerer Fehler, geht eine Mail an den Administrator. */
|
||||
function error_alert_shutdown(): void
|
||||
{
|
||||
static $running = false;
|
||||
$err = error_get_last();
|
||||
if ($running || !$err || !in_array($err['type'], ERROR_ALERT_FATAL, true)) return;
|
||||
$running = true;
|
||||
try {
|
||||
error_alert_dispatch(get_db(), $err);
|
||||
} catch (Throwable $e) {
|
||||
error_log('error_alert: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
register_shutdown_function('error_alert_shutdown');
|
||||
|
||||
Reference in New Issue
Block a user