Upload via GUI (258 Dateien)
This commit is contained in:
@@ -0,0 +1,350 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../includes/bootstrap.php';
|
||||
require_once __DIR__ . '/../includes/backup.php';
|
||||
require_once __DIR__ . '/../includes/backup_restore.php';
|
||||
$staff = require_admin();
|
||||
$pdo = get_db();
|
||||
|
||||
// ── Backup-Setting Helfer ────────────────────────────────────────────────────
|
||||
function bget(string $key): ?string {
|
||||
global $pdo;
|
||||
$s = $pdo->prepare('SELECT value FROM backup_settings WHERE key_name=?');
|
||||
$s->execute([$key]);
|
||||
$r = $s->fetch();
|
||||
return $r ? $r['value'] : null;
|
||||
}
|
||||
function bset_save(string $key, ?string $value): void {
|
||||
global $pdo;
|
||||
$sql = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME) === 'sqlite'
|
||||
? 'INSERT INTO backup_settings (key_name, value, updated_at) VALUES (?,?,?)
|
||||
ON CONFLICT(key_name) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at'
|
||||
: 'INSERT INTO backup_settings (key_name, value, updated_at) VALUES (?,?,?)
|
||||
ON DUPLICATE KEY UPDATE value=VALUES(value), updated_at=VALUES(updated_at)';
|
||||
$pdo->prepare($sql)->execute([$key, $value, db_now()]);
|
||||
}
|
||||
|
||||
// ── POST-Handler ─────────────────────────────────────────────────────────────
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
verify_csrf();
|
||||
$action = $_POST['action'] ?? '';
|
||||
|
||||
if ($action === 'create_backup') {
|
||||
$result = create_backup();
|
||||
if ($result['ok']) {
|
||||
// FTP-Upload wenn konfiguriert
|
||||
$ftpHost = bget('ftp_host');
|
||||
$ftpUser = bget('ftp_user');
|
||||
$ftpPass = bget('ftp_pass');
|
||||
$ftpPath = bget('ftp_path') ?: '/';
|
||||
$ftpPort = (int)(bget('ftp_port') ?: 21);
|
||||
$ftpTls = bget('ftp_tls') === '1';
|
||||
$ftpMsg = '';
|
||||
if ($ftpHost && $ftpUser && function_exists('ftp_connect')) {
|
||||
$ftp = $ftpTls ? @ftp_ssl_connect($ftpHost, $ftpPort, 10) : @ftp_connect($ftpHost, $ftpPort, 10);
|
||||
if ($ftp && @ftp_login($ftp, $ftpUser, $ftpPass)) {
|
||||
ftp_pasv($ftp, true);
|
||||
$remote = rtrim($ftpPath, '/') . '/' . $result['filename'];
|
||||
$ftpMsg = ftp_put($ftp, $remote, $result['path'], FTP_BINARY)
|
||||
? ' – FTP-Upload erfolgreich.' : ' – FTP-Upload fehlgeschlagen.';
|
||||
ftp_close($ftp);
|
||||
} else {
|
||||
$ftpMsg = ' – FTP-Verbindung fehlgeschlagen.';
|
||||
}
|
||||
}
|
||||
// Lokale Rotation
|
||||
$keep = (int)(bget('rotate_keep') ?: 0);
|
||||
if ($keep > 0) {
|
||||
$bdir = realpath(__DIR__ . '/../backups');
|
||||
$files = array_merge(glob($bdir.'/backup_*.zip')?:[], glob($bdir.'/backup_*.tar.gz')?:[]);
|
||||
usort($files, fn($a,$b) => filemtime($b)-filemtime($a));
|
||||
foreach (array_slice($files, $keep) as $old) @unlink($old);
|
||||
}
|
||||
$verified = backup_verify_and_record($result['path']);
|
||||
audit_log($staff['id'], "Manuelles Backup: {$result['filename']} ({$result['size']} MB)$ftpMsg");
|
||||
flash($verified['ok'] ? 'success' : 'error', ($verified['ok'] ? "✅ Backup erstellt und geprüft: {$result['filename']} ({$result['size']} MB){$ftpMsg}"
|
||||
: "Backup erstellt, aber NICHT wiederherstellbar: " . implode(' ', $verified['errors'])));
|
||||
} else {
|
||||
flash('error', 'Backup fehlgeschlagen: ' . $result['error']);
|
||||
}
|
||||
redirect('/staff/backup.php');
|
||||
}
|
||||
|
||||
if ($action === 'verify_backup') {
|
||||
$path = backup_path_for((string) ($_POST['filename'] ?? ''));
|
||||
if (!$path) { flash('error', 'Backup nicht gefunden.'); redirect('/staff/backup.php'); }
|
||||
$v = backup_verify_and_record($path);
|
||||
$i = $v['info'];
|
||||
if ($v['ok']) {
|
||||
flash('success', '✅ ' . basename($path) . ' ist vollständig und wiederherstellbar (' . strtoupper((string) ($i['driver'] ?? '')) . '-Datenbank, ' . (int) ($i['tables'] ?? 0) . ' Tabellen'
|
||||
. (isset($i['tickets']) ? ', ' . $i['tickets'] . ' Tickets' : '') . ', ' . (int) ($i['files'] ?? 0) . ' Dateien).' . ($v['warnings'] ? ' Hinweis: ' . implode(' ', $v['warnings']) : ''));
|
||||
} else {
|
||||
flash('error', basename($path) . ' ist NICHT wiederherstellbar: ' . implode(' ', $v['errors']));
|
||||
}
|
||||
redirect('/staff/backup.php');
|
||||
}
|
||||
|
||||
if ($action === 'restore_backup') {
|
||||
$path = backup_path_for((string) ($_POST['filename'] ?? ''));
|
||||
if (!$path) { flash('error', 'Backup nicht gefunden.'); redirect('/staff/backup.php'); }
|
||||
if (trim((string) ($_POST['confirm'] ?? '')) !== 'WIEDERHERSTELLEN') {
|
||||
flash('error', 'Zur Bestätigung bitte WIEDERHERSTELLEN eintippen. Es wurde nichts verändert.');
|
||||
redirect('/staff/backup.php');
|
||||
}
|
||||
$dirs = array_values(array_intersect(array_keys(backup_restorable_dirs()), (array) ($_POST['dirs'] ?? [])));
|
||||
$res = backup_restore($pdo, $path, ['database' => !empty($_POST['database']), 'dirs' => $dirs]);
|
||||
if ($res['ok']) {
|
||||
audit_log($staff['id'], 'Backup wiederhergestellt: ' . basename($path) . " ({$res['tables']} Tabellen, {$res['files']} Dateien, Sicherheits-Backup: " . ($res['safety'] ?? '–') . ')');
|
||||
flash('success', '✅ Wiederhergestellt: ' . basename($path) . " ({$res['tables']} Tabellen, {$res['rows']} Datensätze, {$res['files']} Dateien). Der Stand von vorher steht im Sicherheits-Backup " . ($res['safety'] ?? '–') . '. ' . implode(' ', $res['notes'])
|
||||
. ' Falls du abgemeldet wirst: Der Mitarbeiter-Stand entspricht jetzt dem Backup, bitte neu anmelden.');
|
||||
} else {
|
||||
flash('error', $res['error']);
|
||||
}
|
||||
redirect('/staff/backup.php');
|
||||
}
|
||||
|
||||
if ($action === 'delete_backup') {
|
||||
$file = basename($_POST['filename'] ?? '');
|
||||
$bdir = realpath(__DIR__ . '/../backups');
|
||||
$path = $bdir . '/' . $file;
|
||||
if ($file && $bdir && file_exists($path)
|
||||
&& strpos(realpath($path), $bdir) === 0
|
||||
&& preg_match('/^backup_[\d\-_]+\.(zip|tar\.gz)$/', $file)) {
|
||||
unlink($path);
|
||||
audit_log($staff['id'], "Backup gelöscht: $file");
|
||||
flash('success', "Backup $file gelöscht.");
|
||||
} else {
|
||||
flash('error', 'Backup nicht gefunden.');
|
||||
}
|
||||
redirect('/staff/backup.php');
|
||||
}
|
||||
|
||||
if ($action === 'save_schedule') {
|
||||
bset_save('schedule', $_POST['schedule'] ?? 'disabled');
|
||||
bset_save('rotate_keep', (string)(int)($_POST['rotate_keep'] ?? 0));
|
||||
audit_log($staff['id'], 'Backup-Zeitplan gespeichert');
|
||||
flash('success', 'Zeitplan gespeichert.');
|
||||
redirect('/staff/backup.php#zeitplan');
|
||||
}
|
||||
|
||||
if ($action === 'save_ftp') {
|
||||
bset_save('ftp_host', trim($_POST['ftp_host'] ?? '') ?: null);
|
||||
bset_save('ftp_port', trim($_POST['ftp_port'] ?? '21'));
|
||||
bset_save('ftp_user', trim($_POST['ftp_user'] ?? '') ?: null);
|
||||
bset_save('ftp_tls', !empty($_POST['ftp_tls']) ? '1' : '0');
|
||||
bset_save('ftp_path', trim($_POST['ftp_path'] ?? '/'));
|
||||
bset_save('ftp_keep', (string)(int)($_POST['ftp_keep'] ?? 5));
|
||||
if (!empty($_POST['ftp_pass'])) {
|
||||
bset_save('ftp_pass', $_POST['ftp_pass']);
|
||||
}
|
||||
audit_log($staff['id'], 'FTP-Backup-Einstellungen gespeichert');
|
||||
flash('success', 'FTP-Einstellungen gespeichert.');
|
||||
redirect('/staff/backup.php#ftp');
|
||||
}
|
||||
|
||||
if ($action === 'test_ftp') {
|
||||
$host = bget('ftp_host'); $user = bget('ftp_user'); $pass = bget('ftp_pass');
|
||||
$port = (int)(bget('ftp_port') ?: 21); $tls = bget('ftp_tls') === '1';
|
||||
if (!$host || !$user) { flash('error', 'FTP nicht konfiguriert.'); }
|
||||
elseif (!function_exists('ftp_connect')) { flash('error', 'PHP FTP-Erweiterung nicht verfügbar.'); }
|
||||
else {
|
||||
$ftp = $tls ? @ftp_ssl_connect($host, $port, 10) : @ftp_connect($host, $port, 10);
|
||||
if ($ftp && @ftp_login($ftp, $user, $pass)) {
|
||||
ftp_close($ftp);
|
||||
flash('success', "✅ FTP-Verbindung zu $host erfolgreich.");
|
||||
} else {
|
||||
flash('error', "FTP-Verbindung zu $host fehlgeschlagen.");
|
||||
}
|
||||
}
|
||||
redirect('/staff/backup.php#ftp');
|
||||
}
|
||||
}
|
||||
|
||||
// ── Daten laden ───────────────────────────────────────────────────────────────
|
||||
$backupDir = realpath(__DIR__ . '/../backups') ?: (dirname(__DIR__) . '/backups');
|
||||
$backupFiles = [];
|
||||
if (is_dir($backupDir)) {
|
||||
foreach (array_merge(glob($backupDir.'/backup_*.zip')?:[], glob($backupDir.'/backup_*.tar.gz')?:[]) as $f) {
|
||||
$backupFiles[] = ['name'=>basename($f),'size'=>round(filesize($f)/1024/1024,2),'date'=>filemtime($f)];
|
||||
}
|
||||
usort($backupFiles, fn($a,$b) => $b['date']-$a['date']);
|
||||
}
|
||||
$totalSize = array_sum(array_column($backupFiles, 'size'));
|
||||
$lastAutoRun = get_setting('cron_process_backups_last_run');
|
||||
$lastAutoBkp = bget('last_auto_backup');
|
||||
|
||||
$pageTitle = 'Backup';
|
||||
$activePage = 'backup';
|
||||
require __DIR__ . '/_header.php';
|
||||
?>
|
||||
|
||||
<div class="topbar">
|
||||
<h1>💾 Backup</h1>
|
||||
<form method="post" id="backup-form">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="action" value="create_backup">
|
||||
<button type="submit" class="btn">💾 Backup jetzt erstellen</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Statistiken -->
|
||||
<div class="stat-grid" style="margin-bottom:20px">
|
||||
<div class="stat-card"><div class="num"><?= count($backupFiles) ?></div><div class="lbl">Backups vorhanden</div></div>
|
||||
<div class="stat-card"><div class="num"><?= $totalSize ?> MB</div><div class="lbl">Gesamtgröße</div></div>
|
||||
<div class="stat-card"><div class="num"><?= $backupFiles ? date('d.m.Y', $backupFiles[0]['date']) : '–' ?></div><div class="lbl">Letztes Backup</div></div>
|
||||
<div class="stat-card"><div class="num"><?= $lastAutoBkp ? date('d.m.Y', strtotime($lastAutoBkp)) : '–' ?></div><div class="lbl">Letztes Auto-Backup</div></div>
|
||||
</div>
|
||||
|
||||
<!-- Backup-Liste -->
|
||||
<div class="card" style="margin-bottom:20px">
|
||||
<h3>Vorhandene Backups</h3>
|
||||
<?php $lastCheck = json_decode((string) get_setting('backup_last_check', ''), true); if (is_array($lastCheck)): ?>
|
||||
<p class="muted" style="font-size:.82rem; margin-top:-6px">Letzte Prüfung: <?= e(date('d.m.Y H:i', strtotime($lastCheck['at']))) ?> – <?= e($lastCheck['file']) ?>:
|
||||
<strong style="color:<?= $lastCheck['ok'] ? '#15803d' : '#dc2626' ?>"><?= $lastCheck['ok'] ? 'wiederherstellbar' : 'nicht wiederherstellbar' ?></strong></p>
|
||||
<?php endif; ?>
|
||||
<?php if (!$backupFiles): ?>
|
||||
<div class="empty-state">Noch keine Backups vorhanden.</div>
|
||||
<?php else: ?>
|
||||
<table>
|
||||
<thead><tr><th>Dateiname</th><th>Größe</th><th>Erstellt</th><th style="width:160px"></th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($backupFiles as $b): ?>
|
||||
<tr>
|
||||
<td style="font-family:monospace; font-size:.82rem"><?= e($b['name']) ?></td>
|
||||
<td class="muted"><?= $b['size'] ?> MB</td>
|
||||
<td class="muted"><?= date('d.m.Y H:i', $b['date']) ?></td>
|
||||
<td style="display:flex; gap:6px; flex-wrap:wrap; align-items:flex-start">
|
||||
<a href="/staff/backup_download.php?file=<?= urlencode($b['name']) ?>" class="btn secondary" style="padding:5px 10px; font-size:.78rem">⬇️ Download</a>
|
||||
<?php if (str_ends_with($b['name'], '.zip')): ?>
|
||||
<form method="post" style="margin:0"><?= csrf_field() ?><input type="hidden" name="action" value="verify_backup"><input type="hidden" name="filename" value="<?= e($b['name']) ?>">
|
||||
<button type="submit" class="btn secondary" style="padding:5px 10px; font-size:.78rem" title="Prüft, ob sich das Backup wiederherstellen lässt (verändert nichts)">✔️ Prüfen</button></form>
|
||||
<details style="position:relative"><summary class="btn secondary" style="padding:5px 10px; font-size:.78rem; list-style:none; cursor:pointer">♻️ Wiederherstellen</summary>
|
||||
<form method="post" style="position:absolute; right:0; z-index:5; width:340px; padding:14px; background:var(--card-bg,#fff); border:1px solid var(--border); border-radius:10px; box-shadow:0 8px 24px rgba(0,0,0,.18)">
|
||||
<?= csrf_field() ?><input type="hidden" name="action" value="restore_backup"><input type="hidden" name="filename" value="<?= e($b['name']) ?>">
|
||||
<p style="font-size:.82rem; margin:0 0 8px"><strong>Achtung:</strong> Der aktuelle Stand der Datenbank wird durch den Stand dieses Backups ersetzt. Vorher wird automatisch ein Sicherheits-Backup erstellt. Programmdateien und die Konfiguration bleiben unverändert.</p>
|
||||
<label style="display:block; font-weight:400"><input type="checkbox" name="database" value="1" checked style="width:auto"> Datenbank</label>
|
||||
<?php foreach (backup_restorable_dirs() as $dk => $dl): ?><label style="display:block; font-weight:400"><input type="checkbox" name="dirs[]" value="<?= e($dk) ?>" checked style="width:auto"> <?= e($dl) ?></label><?php endforeach; ?>
|
||||
<label style="margin-top:8px; font-size:.8rem">Zur Bestätigung <code>WIEDERHERSTELLEN</code> eintippen</label>
|
||||
<input type="text" name="confirm" autocomplete="off" required>
|
||||
<button type="submit" class="btn" style="margin-top:8px; background:var(--danger)">Jetzt wiederherstellen</button>
|
||||
</form></details>
|
||||
<?php endif; ?>
|
||||
<form method="post" onsubmit="return confirm('Backup löschen?')" style="margin:0">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="action" value="delete_backup">
|
||||
<input type="hidden" name="filename" value="<?= e($b['name']) ?>">
|
||||
<button type="submit" class="btn secondary" style="padding:5px 10px; font-size:.78rem; border-color:var(--danger); color:var(--danger)">🗑️</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div style="display:grid; grid-template-columns:1fr 1fr; gap:20px; margin-bottom:20px">
|
||||
|
||||
<!-- Zeitplan -->
|
||||
<div class="card" id="zeitplan">
|
||||
<h3>⏰ Automatischer Zeitplan</h3>
|
||||
<p class="muted" style="font-size:.85rem; margin-bottom:16px">
|
||||
Cron-Job einrichten: <code>php <?= e(dirname(__DIR__)) ?>/cron/process_backups.php</code><br>
|
||||
<?php if ($lastAutoRun): ?><span style="font-size:.78rem">Letzter Lauf: <?= format_dt($lastAutoRun) ?></span><?php endif; ?>
|
||||
</p>
|
||||
<form method="post">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="action" value="save_schedule">
|
||||
<label>Backup-Intervall</label>
|
||||
<select name="schedule">
|
||||
<?php foreach (['disabled'=>'Deaktiviert','daily'=>'Täglich','weekly'=>'Wöchentlich','monthly'=>'Monatlich'] as $v=>$l): ?>
|
||||
<option value="<?= $v ?>" <?= bget('schedule')===$v?'selected':'' ?>><?= $l ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<label style="margin-top:14px">Backup-Rotation (lokale Kopien behalten)</label>
|
||||
<select name="rotate_keep">
|
||||
<option value="0" <?= bget('rotate_keep')==='0'?'selected':'' ?>>Alle behalten</option>
|
||||
<?php foreach ([3,5,7,10,14,30] as $n): ?>
|
||||
<option value="<?= $n ?>" <?= bget('rotate_keep')===(string)$n?'selected':'' ?>><?= $n ?> Backups behalten</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<p class="muted" style="font-size:.78rem; margin-top:6px">Ältere Backups werden automatisch gelöscht wenn das Limit erreicht wird.</p>
|
||||
<div class="btn-row" style="margin-top:14px">
|
||||
<button type="submit" class="btn">Zeitplan speichern</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- FTP -->
|
||||
<div class="card" id="ftp">
|
||||
<h3>📡 FTP-Upload <span class="muted" style="font-size:.78rem; font-weight:400">(optional)</span></h3>
|
||||
<p class="muted" style="font-size:.85rem; margin-bottom:16px">Backups werden nach der Erstellung automatisch per FTP übertragen.</p>
|
||||
<form method="post">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="action" value="save_ftp">
|
||||
<div style="display:grid; grid-template-columns:1fr auto; gap:8px; align-items:flex-end; margin-bottom:10px">
|
||||
<div>
|
||||
<label>FTP-Host</label>
|
||||
<input type="text" name="ftp_host" value="<?= e(bget('ftp_host') ?? '') ?>" placeholder="ftp.beispiel.de">
|
||||
</div>
|
||||
<div style="width:80px">
|
||||
<label>Port</label>
|
||||
<input type="number" name="ftp_port" value="<?= e(bget('ftp_port') ?? '21') ?>" min="1" max="65535">
|
||||
</div>
|
||||
</div>
|
||||
<label>Benutzername</label>
|
||||
<input type="text" name="ftp_user" value="<?= e(bget('ftp_user') ?? '') ?>" placeholder="ftp-benutzer" autocomplete="off">
|
||||
<label style="margin-top:10px">Passwort</label>
|
||||
<input type="password" name="ftp_pass" placeholder="<?= bget('ftp_pass') ? '••••••••' : 'Passwort eingeben' ?>" autocomplete="new-password">
|
||||
<label style="margin-top:10px">Zielverzeichnis</label>
|
||||
<input type="text" name="ftp_path" value="<?= e(bget('ftp_path') ?? '/') ?>" placeholder="/backups/deskhub">
|
||||
<div style="display:grid; grid-template-columns:1fr 1fr; gap:10px; margin-top:10px">
|
||||
<div>
|
||||
<label>FTP-Rotation (Kopien auf Server)</label>
|
||||
<select name="ftp_keep">
|
||||
<option value="0" <?= bget('ftp_keep')==='0'?'selected':'' ?>>Alle behalten</option>
|
||||
<?php foreach ([3,5,7,10] as $n): ?>
|
||||
<option value="<?= $n ?>" <?= bget('ftp_keep')===(string)$n?'selected':'' ?>><?= $n ?> behalten</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div style="display:flex; align-items:flex-end; padding-bottom:2px">
|
||||
<label style="display:flex; align-items:center; gap:8px; font-weight:400; cursor:pointer">
|
||||
<input type="checkbox" name="ftp_tls" value="1" <?= bget('ftp_tls')==='1'?'checked':'' ?> style="width:auto">
|
||||
FTPS (TLS)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-row" style="margin-top:16px; gap:8px">
|
||||
<button type="submit" class="btn">FTP speichern</button>
|
||||
<button type="submit" form="ftp-test-form" class="btn secondary">Verbindung testen</button>
|
||||
</div>
|
||||
</form>
|
||||
<form method="post" id="ftp-test-form" style="display:none">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="action" value="test_ftp">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Hinweise -->
|
||||
<div class="card">
|
||||
<h3>ℹ️ Hinweise</h3>
|
||||
<ul style="padding-left:20px; line-height:2; font-size:.88rem; color:var(--muted)">
|
||||
<li>Backups enthalten alle Dateien inkl. <strong style="color:var(--ink)">Datenbank</strong> und <strong style="color:var(--ink)">Konfiguration</strong></li>
|
||||
<li>Backups werden automatisch vor jedem Update erstellt</li>
|
||||
<li>Für automatische Backups den Cron einrichten:<br><code>0 2 * * * php <?= e(dirname(__DIR__)) ?>/cron/process_backups.php</code></li>
|
||||
<li>Backups liegen in <code>backups/</code> und sind von außen nicht zugänglich</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('backup-form').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
var btn = this.querySelector('button[type=submit]');
|
||||
btn.disabled = true;
|
||||
btn.textContent = '⏳ Backup wird erstellt...';
|
||||
this.submit();
|
||||
});
|
||||
</script>
|
||||
<?php require __DIR__ . '/_footer.php'; ?>
|
||||
Reference in New Issue
Block a user