Files
deskhub/DeskHub-Webseite/staff/backup.php
T
2026-10-16 19:12:21 +00:00

351 lines
20 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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'; ?>