481 lines
26 KiB
PHP
481 lines
26 KiB
PHP
<?php
|
||
require_once __DIR__ . '/../includes/bootstrap.php';
|
||
require_once __DIR__ . '/../includes/backup.php';
|
||
$staff = require_admin();
|
||
$pdo = get_db();
|
||
|
||
$errors = [];
|
||
|
||
const UPDATE_REPO_API = 'https://git.viper.ipv64.net/api/v1/repos/M_Viper/deskhub';
|
||
|
||
/** Lädt eine https-Adresse (Weiterleitungen nur auf https). Gibt [Status, Inhalt, Fehlertext] zurück. */
|
||
function update_download(string $url, array $headers, int $timeout): array
|
||
{
|
||
$ch = curl_init($url);
|
||
curl_setopt_array($ch, [
|
||
CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => true, CURLOPT_MAXREDIRS => 3, CURLOPT_TIMEOUT => $timeout,
|
||
CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_PROTOCOLS => CURLPROTO_HTTPS, CURLOPT_REDIR_PROTOCOLS => CURLPROTO_HTTPS,
|
||
CURLOPT_HTTPHEADER => $headers,
|
||
]);
|
||
$data = curl_exec($ch);
|
||
$code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||
$err = curl_error($ch);
|
||
curl_close($ch);
|
||
return [$code, is_string($data) ? $data : '', $err];
|
||
}
|
||
|
||
// ── ZIP installieren ─────────────────────────────────────────────────────────
|
||
function do_install(string $zipPath): array
|
||
{
|
||
$zip = new ZipArchive();
|
||
$openResult = $zip->open($zipPath);
|
||
if ($openResult !== true) {
|
||
return ['ok' => false, 'error' => 'ZIP konnte nicht geöffnet werden (Code: ' . $openResult . ', Datei: ' . $zipPath . ', Größe: ' . filesize($zipPath) . ')'];
|
||
}
|
||
|
||
$rootDir = str_replace('/', DIRECTORY_SEPARATOR, realpath(__DIR__ . '/..'));
|
||
$protected = [
|
||
'config/config.php',
|
||
'database/deskhub.sqlite',
|
||
'database/deskhub.db',
|
||
'.env',
|
||
'version.php',
|
||
'staff/update.php', // Sich selbst nicht überschreiben während des Updates
|
||
'includes/bootstrap.php', // Bootstrap nicht mid-request überschreiben
|
||
];
|
||
|
||
// Gitea-Prefix erkennen (z.B. "deskhub/")
|
||
$prefix = '';
|
||
for ($i = 0; $i < min($zip->numFiles, 5); $i++) {
|
||
$n = $zip->getNameIndex($i);
|
||
if (substr($n, -1) === '/' && substr_count(rtrim($n, '/'), '/') === 0) {
|
||
$prefix = $n;
|
||
break;
|
||
}
|
||
}
|
||
|
||
$extracted = 0;
|
||
$failed = [];
|
||
|
||
for ($i = 0; $i < $zip->numFiles; $i++) {
|
||
$name = $zip->getNameIndex($i);
|
||
|
||
// Verzeichnisse überspringen
|
||
if (substr($name, -1) === '/') continue;
|
||
|
||
// Prefix entfernen
|
||
if ($prefix) {
|
||
if (strpos($name, $prefix) !== 0) continue;
|
||
$rel = substr($name, strlen($prefix));
|
||
} else {
|
||
$rel = $name;
|
||
}
|
||
|
||
if ($rel === '') continue;
|
||
|
||
// Pfad prüfen: kein Verlassen des Installationsordners (Zip-Slip), keine absoluten Pfade
|
||
$relNorm = str_replace(chr(92), '/', $rel);
|
||
if ($relNorm[0] === '/' || preg_match('#(^|/)\.\.(/|$)#', $relNorm) || preg_match('#^[A-Za-z]:#', $relNorm) || strpos($relNorm, "\0") !== false) {
|
||
$failed[] = $rel;
|
||
continue;
|
||
}
|
||
// Die Plugin-Webseite und Entwickler-Werkzeuge gehören nicht auf Kunden-Installationen; Plugins, App-Dateien, Uploads und Backups gehören dem System und werden von Updates nie angefasst
|
||
if (preg_match('#^(plugins|uploads|backups|app_downloads)/#', $relNorm) || preg_match('#^(plugin-webseite|tools|tests|docs|desktop|mobile|theme-packages|vorschaubilder|\.gitea|\.github)/#', $relNorm)) continue;
|
||
|
||
// Geschützte Dateien überspringen
|
||
$skip = false;
|
||
foreach ($protected as $p) {
|
||
if ($rel === $p || str_replace('/', DIRECTORY_SEPARATOR, $rel) === str_replace('/', DIRECTORY_SEPARATOR, $p)) {
|
||
$skip = true;
|
||
break;
|
||
}
|
||
}
|
||
if ($skip) continue;
|
||
|
||
// Windows-Pfad
|
||
$dest = $rootDir . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $rel);
|
||
$dir = dirname($dest);
|
||
|
||
if (!is_dir($dir)) {
|
||
mkdir($dir, 0755, true);
|
||
}
|
||
|
||
$data = $zip->getFromIndex($i);
|
||
if ($data === false) continue;
|
||
|
||
$written = file_put_contents($dest, $data);
|
||
if ($written !== false) {
|
||
$extracted++;
|
||
} else {
|
||
$failed[] = $rel;
|
||
}
|
||
}
|
||
|
||
$zip->close();
|
||
|
||
if ($extracted === 0) {
|
||
$msg = 'Keine Dateien extrahiert!';
|
||
if ($prefix) $msg .= ' Prefix: "' . $prefix . '"';
|
||
if ($failed) $msg .= ' Fehlgeschlagen: ' . implode(', ', array_slice($failed, 0, 3));
|
||
return ['ok' => false, 'error' => $msg];
|
||
}
|
||
|
||
return ['ok' => true, 'extracted' => $extracted, 'failed' => $failed];
|
||
}
|
||
|
||
// ── POST ─────────────────────────────────────────────────────────────────────
|
||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||
// CSRF prüfen ohne Token-Rotation (wegen Datei-Upload multipart)
|
||
$csrfToken = $_POST['csrf_token'] ?? '';
|
||
if (!$csrfToken || !hash_equals($_SESSION['csrf_token'] ?? '', $csrfToken)) {
|
||
http_response_code(403);
|
||
die('Ungültige Anfrage (CSRF-Token fehlt oder ist ungültig). Bitte gehen Sie zurück und versuchen Sie es erneut.');
|
||
}
|
||
$action = $_POST['action'] ?? '';
|
||
|
||
// Zugangstoken für den Update-Server speichern oder entfernen (nur Admins erreichen diese Seite)
|
||
if ($action === 'save_update_token') {
|
||
$newToken = trim((string) ($_POST['update_token'] ?? ''));
|
||
if ($newToken === '' || !preg_match('/^[A-Za-z0-9_\-.]{20,200}$/', $newToken)) {
|
||
$errors[] = 'Das Token sieht ungültig aus (erwartet: 20 bis 200 Zeichen aus Buchstaben, Ziffern, _ - .). Bitte genau so einfügen, wie Gitea es anzeigt.';
|
||
} else {
|
||
set_setting('update_token', encrypt_secret($newToken));
|
||
audit_log($staff['id'], 'Zugangstoken für den Update-Server gespeichert');
|
||
flash('success', 'Zugangstoken gespeichert. Die Release-Liste wird jetzt damit geladen.');
|
||
redirect('/staff/update.php');
|
||
}
|
||
}
|
||
if ($action === 'remove_update_token') {
|
||
set_setting('update_token', '');
|
||
audit_log($staff['id'], 'Zugangstoken für den Update-Server entfernt');
|
||
flash('success', 'Zugangstoken entfernt.');
|
||
redirect('/staff/update.php');
|
||
}
|
||
|
||
// Manueller Upload
|
||
if ($action === 'upload_update') {
|
||
$f = $_FILES['update_zip'] ?? [];
|
||
if (($f['error'] ?? 99) !== UPLOAD_ERR_OK) {
|
||
$errors[] = 'Upload-Fehler Code: ' . ($f['error'] ?? 'unbekannt');
|
||
} elseif (strtolower(pathinfo($f['name'] ?? '', PATHINFO_EXTENSION)) !== 'zip') {
|
||
$errors[] = 'Nur ZIP-Dateien erlaubt.';
|
||
} else {
|
||
$tmp = $f['tmp_name'];
|
||
$bk = create_backup();
|
||
$res = do_install($tmp);
|
||
if (!$res['ok']) {
|
||
$errors[] = $res['error'];
|
||
} else {
|
||
// Schema-Migrationen nach Update einspielen
|
||
require_once __DIR__ . '/../database/migrate.php';
|
||
$mig = run_migrations(get_db());
|
||
$migNote = $mig['applied'] > 0 ? " · {$mig['applied']} Migration(en) eingespielt" : '';
|
||
if ($mig['errors']) $errors = array_merge($errors, $mig['errors']);
|
||
audit_log($staff['id'], "Update installiert: {$res['extracted']} Dateien{$migNote}");
|
||
flash('success', "✅ {$res['extracted']} Dateien aktualisiert.{$migNote}" . ($bk['ok'] ? " Backup: {$bk['filename']}" : ''));
|
||
redirect('/staff/update.php');
|
||
}
|
||
}
|
||
}
|
||
|
||
// Nur die Datenbank auf den Stand des installierten Codes bringen (z. B. nach Datei-Upload per FTP)
|
||
if ($action === 'migrate_db') {
|
||
require_once __DIR__ . '/../database/migrate.php';
|
||
$pending = migrations_pending($pdo);
|
||
if (!$pending) {
|
||
flash('success', 'Die Datenbank ist bereits auf dem aktuellen Stand.');
|
||
redirect('/staff/update.php');
|
||
}
|
||
$bk = create_backup();
|
||
if (!$bk['ok']) {
|
||
$errors[] = 'Backup fehlgeschlagen – die Datenbank wurde nicht verändert: ' . ($bk['error'] ?? 'unbekannter Fehler');
|
||
} else {
|
||
$mig = run_migrations($pdo);
|
||
if ($mig['errors']) {
|
||
$errors = array_merge($errors, $mig['errors']);
|
||
audit_log($staff['id'], 'Datenbank-Update mit Fehlern: ' . implode('; ', $mig['errors']));
|
||
} else {
|
||
audit_log($staff['id'], "Datenbank aktualisiert: {$mig['applied']} Migration(en)");
|
||
flash('success', "✅ Datenbank aktualisiert ({$mig['applied']} Migration(en)). Backup: {$bk['filename']}");
|
||
redirect('/staff/update.php');
|
||
}
|
||
}
|
||
}
|
||
|
||
// Auto-Download vom Update-Server. Ist ein Herausgeber-Schlüssel eingebaut, wird nur ein signiertes Release installiert.
|
||
if ($action === 'auto_update') {
|
||
$tagName = trim($_POST['tag_name'] ?? '');
|
||
$headers = update_http_headers();
|
||
if (!preg_match('/^v?\d+\.\d+\.\d+([\-+][0-9A-Za-z.\-]+)?$/', $tagName)) {
|
||
$errors[] = 'Kein gültiges Release gewählt.';
|
||
} else {
|
||
$verifyRequired = update_signature_required();
|
||
$zipUrl = UPDATE_REPO_API === '' ? '' : 'https://git.viper.ipv64.net/M_Viper/deskhub/archive/' . rawurlencode($tagName) . '.zip';
|
||
$sigUrl = null;
|
||
[$relCode, $relBody] = update_download(UPDATE_REPO_API . '/releases/tags/' . rawurlencode($tagName), $headers, 10);
|
||
$release = $relCode === 200 ? (json_decode($relBody, true) ?: []) : [];
|
||
$assets = update_find_assets($release, $tagName);
|
||
if ($assets['zip']) { $zipUrl = $assets['zip']; $sigUrl = $assets['sig']; }
|
||
|
||
if ($verifyRequired && (!$assets['zip'] || !$sigUrl)) {
|
||
$errors[] = 'Dieses Release ist nicht signiert (Anhänge deskhub-' . ltrim($tagName, 'vV') . '.zip und .zip.sig fehlen). Aus Sicherheitsgründen wird es nicht installiert.';
|
||
} else {
|
||
[$code, $data, $err] = update_download($zipUrl, $headers, 120);
|
||
if ($err) {
|
||
$errors[] = 'cURL-Fehler: ' . $err . (stripos($err, 'certificate') !== false || stripos($err, 'SSL') !== false ? ' – Das Serverzertifikat konnte nicht geprüft werden. Bitte das CA-Zertifikatspaket des Servers aktualisieren (php.ini: curl.cainfo). Die Prüfung wird aus Sicherheitsgründen nicht abgeschaltet.' : '');
|
||
} elseif ($code !== 200) {
|
||
$errors[] = "Download fehlgeschlagen (HTTP $code).";
|
||
} else {
|
||
$tmp = tempnam(sys_get_temp_dir(), 'dh_') . '.zip';
|
||
file_put_contents($tmp, $data);
|
||
$sigError = null;
|
||
if ($verifyRequired) {
|
||
[$sc, $sigBody] = update_download($sigUrl, $headers, 20);
|
||
$sigError = $sc === 200 ? update_verify_package($tagName, $tmp, $sigBody) : "Die Signatur konnte nicht geladen werden (HTTP $sc).";
|
||
}
|
||
if ($sigError !== null) {
|
||
@unlink($tmp);
|
||
audit_log($staff['id'], "Update $tagName abgelehnt: $sigError");
|
||
$errors[] = $sigError;
|
||
} else {
|
||
$bk = create_backup();
|
||
$res = do_install($tmp);
|
||
@unlink($tmp);
|
||
if (!$res['ok']) {
|
||
$errors[] = $res['error'];
|
||
} else {
|
||
// Schema-Migrationen nach Update einspielen
|
||
require_once __DIR__ . '/../database/migrate.php';
|
||
$mig = run_migrations(get_db());
|
||
$migNote = $mig['applied'] > 0 ? " · {$mig['applied']} Migration(en) eingespielt" : '';
|
||
if ($mig['errors']) $errors = array_merge($errors, $mig['errors']);
|
||
audit_log($staff['id'], "Auto-Update $tagName: {$res['extracted']} Dateien{$migNote}" . ($verifyRequired ? ' (Signatur geprüft)' : ''));
|
||
// Versionsnummer in version.php aktualisieren
|
||
$newVer = ltrim($tagName, 'v');
|
||
$verFile = realpath(__DIR__ . '/../version.php');
|
||
if ($verFile && $newVer) {
|
||
$vc = file_get_contents($verFile);
|
||
$vc = preg_replace("/define\('DESKHUB_VERSION',\s*'[^']+'\)/", "define('DESKHUB_VERSION', '$newVer')", $vc);
|
||
file_put_contents($verFile, $vc);
|
||
}
|
||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||
flash('success', "✅ Update auf $newVer: {$res['extracted']} Dateien." . ($verifyRequired ? ' Signatur geprüft.' : '') . ($bk['ok'] ? " Backup: {$bk['filename']}" : ''));
|
||
redirect('/staff/update.php');
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// ── Releases laden ────────────────────────────────────────────────────────────
|
||
$releases = [];
|
||
$relErr = '';
|
||
[$code, $resp] = update_download(UPDATE_REPO_API . '/releases?limit=20', update_http_headers(), 8);
|
||
if ($code === 200) {
|
||
$all = json_decode($resp, true) ?: [];
|
||
usort($all, fn($a, $b) => version_compare($b['tag_name'], $a['tag_name']));
|
||
$releases = $all;
|
||
} else {
|
||
$relErr = "API HTTP $code" . (update_http_headers() === []
|
||
? ($code === 404 || $code === 401 || $code === 403 ? ' – das Repository ist vermutlich privat. Unten unter „Zugang zum Update-Server“ ein Lese-Token eintragen.' : '')
|
||
: ($code === 404 || $code === 401 || $code === 403 ? ' – das eingetragene Token wird nicht akzeptiert (falsch, abgelaufen oder ohne Leserecht).' : ''));
|
||
}
|
||
$signedOnly = update_signature_required();
|
||
// Version frisch aus Datei lesen
|
||
$verFile = realpath(__DIR__ . '/../version.php') ?: '';
|
||
$verData = $verFile ? file_get_contents($verFile) : '';
|
||
preg_match("/define\s*\(\s*'DESKHUB_VERSION'\s*,\s*'([^']+)'/", $verData, $vm);
|
||
$curVer = $vm[1] ?? DESKHUB_VERSION;
|
||
$latest = $releases[0] ?? null;
|
||
$hasUpdate = $latest && version_compare($latest['tag_name'], $curVer, '>');
|
||
|
||
require_once __DIR__ . '/../database/migrate.php';
|
||
$pendingMigrations = migrations_pending($pdo);
|
||
|
||
$pageTitle = 'System-Update';
|
||
$activePage = 'update';
|
||
require __DIR__ . '/_header.php';
|
||
?>
|
||
|
||
<div class="topbar"><h1>🔄 System-Update</h1></div>
|
||
|
||
<?php foreach ($errors as $e): ?>
|
||
<div class="flash error"><?= htmlspecialchars($e) ?></div>
|
||
<?php endforeach; ?>
|
||
|
||
<?php if ($relErr): ?>
|
||
<div class="flash warning" style="margin-bottom:16px">Die Liste der Versionen konnte nicht geladen werden: <?= htmlspecialchars($relErr) ?></div>
|
||
<?php endif; ?>
|
||
|
||
<?php if ($hasUpdate): ?>
|
||
<div style="background:#e0f2f1;border:1px solid #0f6f6a;border-radius:10px;padding:16px 20px;margin-bottom:20px;display:flex;align-items:center;justify-content:space-between;gap:16px;flex-wrap:wrap">
|
||
<div>
|
||
<strong style="color:#0f6f6a">Update verfügbar: Version <?= htmlspecialchars($latest['tag_name']) ?></strong>
|
||
<span style="color:#64748b;font-size:.85rem;margin-left:10px">vom <?= date('d.m.Y', strtotime($latest['published_at'])) ?></span>
|
||
</div>
|
||
<form method="post">
|
||
<?= csrf_field() ?>
|
||
<input type="hidden" name="action" value="auto_update">
|
||
<input type="hidden" name="tag_name" value="<?= htmlspecialchars($latest['tag_name']) ?>">
|
||
<button type="submit" class="btn" onclick="return confirm('Update auf <?= htmlspecialchars($latest['tag_name']) ?> installieren?')">Jetzt installieren</button>
|
||
</form>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<div class="card" style="margin-bottom:20px;<?= $pendingMigrations ? 'border-color:#d97706' : '' ?>">
|
||
<div style="display:flex;align-items:center;justify-content:space-between;gap:16px;flex-wrap:wrap">
|
||
<div>
|
||
<div style="font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);margin-bottom:6px">Datenbank</div>
|
||
<?php if ($pendingMigrations): ?>
|
||
<strong style="color:#b45309"><?= count($pendingMigrations) ?> ausstehende Datenbank-Änderung(en)</strong>
|
||
<div style="color:var(--muted);font-size:.82rem;margin-top:4px">Der Code ist neuer als die Datenbank. Vor dem Einspielen wird automatisch ein Backup erstellt.</div>
|
||
<details style="margin-top:6px;font-size:.8rem;color:var(--muted)"><summary>Details</summary><?= htmlspecialchars(implode(', ', $pendingMigrations)) ?></details>
|
||
<?php else: ?>
|
||
<strong style="color:#15803d">✓ Die Datenbank ist aktuell</strong>
|
||
<div style="color:var(--muted);font-size:.82rem;margin-top:4px">Alle bekannten Datenbank-Änderungen sind eingespielt.</div>
|
||
<?php endif; ?>
|
||
</div>
|
||
<form method="post">
|
||
<?= csrf_field() ?>
|
||
<input type="hidden" name="action" value="migrate_db">
|
||
<button type="submit" class="btn <?= $pendingMigrations ? '' : 'secondary' ?>" <?= $pendingMigrations ? "onclick=\"return confirm('Datenbank jetzt aktualisieren? Es wird zuerst ein Backup erstellt.')\"" : 'disabled' ?>>🗄 Datenbank aktualisieren</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-bottom:20px">
|
||
|
||
<div class="card" style="margin:0">
|
||
<div style="font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);margin-bottom:12px">Installierte Version</div>
|
||
<div style="font-size:3rem;font-weight:800;color:var(--primary);line-height:1"><?= htmlspecialchars($curVer) ?></div>
|
||
<div style="color:var(--muted);font-size:.82rem;margin-top:6px">DeskHub Ticketsystem</div>
|
||
<?php if ($releases): ?>
|
||
<button onclick="document.getElementById('rel-modal').style.display='flex'" class="btn secondary" style="margin-top:16px">📋 Alle Releases</button>
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
<div class="card" style="margin:0">
|
||
<div style="font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);margin-bottom:12px">Manuelles Update</div>
|
||
<p style="color:var(--muted);font-size:.85rem;margin-bottom:14px">ZIP-Datei von Gitea hochladen und installieren.</p>
|
||
<form method="post" enctype="multipart/form-data">
|
||
<?= csrf_field() ?>
|
||
<input type="hidden" name="action" value="upload_update">
|
||
<input type="file" name="update_zip" accept=".zip" required style="margin-bottom:12px">
|
||
<button type="submit" class="btn secondary">⬆️ Hochladen & installieren</button>
|
||
</form>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<?php if (count($releases) > 1): ?>
|
||
<div class="card" style="margin-bottom:20px">
|
||
<h3>Bestimmte Version installieren</h3>
|
||
<form method="post" style="display:flex;gap:10px;align-items:flex-end;flex-wrap:wrap">
|
||
<?= csrf_field() ?>
|
||
<input type="hidden" name="action" value="auto_update">
|
||
<div style="flex:1;min-width:200px">
|
||
<select name="tag_name" required>
|
||
<option value="">– Version wählen –</option>
|
||
<?php foreach ($releases as $r): $isCur = $r['tag_name']===$curVer; ?>
|
||
<option value="<?= htmlspecialchars($r['tag_name']) ?>" <?= $isCur?'disabled':'' ?>>
|
||
<?= htmlspecialchars($r['tag_name']) ?><?= $isCur?' (installiert)':'' ?> – <?= date('d.m.Y', strtotime($r['published_at'])) ?>
|
||
</option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
</div>
|
||
<button type="submit" class="btn secondary" onclick="return confirm('Version installieren?')">Installieren</button>
|
||
</form>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<?php [$__tokenValue, $__tokenSource] = update_token_info(); ?>
|
||
<div class="card" style="margin-bottom:20px">
|
||
<h3>Zugang zum Update-Server</h3>
|
||
<p style="color:var(--muted);font-size:.86rem;margin-bottom:12px">
|
||
Nur nötig, wenn das Repository mit den Releases <strong>privat</strong> ist. Gitea meldet dann beim Laden der Versionen „404“. Ein Token mit <strong>Leserecht</strong> genügt
|
||
(in Gitea: Einstellungen → Anwendungen → Zugriffstoken erzeugen, Berechtigung „repository: Lesen“). Es wird verschlüsselt gespeichert.
|
||
</p>
|
||
<?php if ($__tokenSource === 'config'): ?>
|
||
<p style="margin-bottom:10px"><strong style="color:#15803d">✓ Token aus config/config.php wird verwendet.</strong> Es hat Vorrang vor einem hier gespeicherten.</p>
|
||
<?php elseif ($__tokenSource === 'einstellungen'): ?>
|
||
<p style="margin-bottom:10px"><strong style="color:#15803d">✓ Token gespeichert.</strong> Zum Ersetzen unten ein neues eintragen.</p>
|
||
<?php else: ?>
|
||
<p style="margin-bottom:10px;color:var(--muted)">Kein Token eingetragen (öffentlicher Zugriff).</p>
|
||
<?php endif; ?>
|
||
<form method="post" style="display:flex;gap:10px;flex-wrap:wrap;align-items:flex-end">
|
||
<?= csrf_field() ?>
|
||
<input type="hidden" name="action" value="save_update_token">
|
||
<div style="flex:1;min-width:260px">
|
||
<label for="update_token" style="margin-top:0">Zugriffstoken</label>
|
||
<input type="password" id="update_token" name="update_token" autocomplete="off" placeholder="<?= $__tokenSource === 'einstellungen' ? '•••••••• (gespeichert)' : 'Token aus Gitea einfügen' ?>" required>
|
||
</div>
|
||
<button type="submit" class="btn">Speichern</button>
|
||
</form>
|
||
<?php if ($__tokenSource === 'einstellungen'): ?>
|
||
<form method="post" style="margin-top:10px" onsubmit="return confirm('Gespeichertes Token entfernen?')">
|
||
<?= csrf_field() ?><input type="hidden" name="action" value="remove_update_token">
|
||
<button type="submit" class="btn secondary">Token entfernen</button>
|
||
</form>
|
||
<?php endif; ?>
|
||
<details style="margin-top:12px;font-size:.82rem;color:var(--muted)"><summary>Alternative: in config/config.php eintragen</summary>
|
||
<p style="margin:8px 0 4px">In der Datei <code>config/config.php</code> (Hauptordner von DeskHub, im Ordner <code>config</code>) als eigener Eintrag im großen <code>return [ … ];</code>, zum Beispiel direkt unter <code>'app_key'</code>:</p>
|
||
<pre style="background:var(--bg-soft,#f4f6f9);padding:10px;border-radius:6px;overflow:auto">'update' => ['token' => 'HIER_DAS_TOKEN'],</pre>
|
||
</details>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h3>Hinweise</h3>
|
||
<ul style="padding-left:20px;line-height:2;font-size:.88rem;color:var(--muted)">
|
||
<li><strong style="color:var(--ink)">config/config.php</strong> und <strong style="color:var(--ink)">database/deskhub.sqlite</strong> werden niemals überschrieben</li>
|
||
<?php if ($signedOnly): ?><li><strong style="color:var(--ink)">Nur signierte Releases:</strong> Updates werden nur installiert, wenn sie mit dem Schlüssel des Herausgebers signiert sind. Ein manipuliertes Paket wird abgelehnt. (Ein manuell hochgeladenes ZIP wird nicht geprüft: Laden Sie nur Pakete aus vertrauenswürdiger Quelle hoch.)</li><?php endif; ?>
|
||
|
||
<li>Nach dem Update Browser-Cache leeren <kbd>Strg+F5</kbd></li>
|
||
</ul>
|
||
</div>
|
||
|
||
<!-- Releases Modal -->
|
||
<div id="rel-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:9999;align-items:center;justify-content:center;padding:20px">
|
||
<div style="background:#fff;border-radius:14px;width:100%;max-width:600px;max-height:80vh;display:flex;flex-direction:column">
|
||
<div style="display:flex;align-items:center;justify-content:space-between;padding:18px 24px;border-bottom:1px solid var(--border)">
|
||
<h3 style="margin:0">Releases</h3>
|
||
<button onclick="document.getElementById('rel-modal').style.display='none'" style="background:none;border:none;font-size:1.5rem;cursor:pointer;color:var(--muted)">×</button>
|
||
</div>
|
||
<div style="overflow-y:auto;padding:20px 24px">
|
||
<?php foreach ($releases as $r):
|
||
$isNew = version_compare($r['tag_name'], $curVer, '>');
|
||
$isCur = $r['tag_name'] === $curVer;
|
||
?>
|
||
<div style="border:1px solid var(--border);border-radius:8px;padding:14px 16px;margin-bottom:10px<?= $isNew?';border-color:var(--primary)':'' ?>">
|
||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
|
||
<div style="display:flex;gap:8px;align-items:center">
|
||
<strong><?= htmlspecialchars($r['tag_name']) ?></strong>
|
||
<?php if ($isNew): ?><span style="background:var(--primary);color:#fff;font-size:.7rem;padding:2px 8px;border-radius:20px;font-weight:700">NEU</span><?php endif; ?>
|
||
<?php if ($isCur): ?><span style="background:#e2f5f3;color:var(--primary);font-size:.7rem;padding:2px 8px;border-radius:20px;font-weight:700">INSTALLIERT</span><?php endif; ?>
|
||
</div>
|
||
<span style="color:var(--muted);font-size:.78rem"><?= date('d.m.Y', strtotime($r['published_at'])) ?></span>
|
||
</div>
|
||
<?php if (!empty($r['body'])): ?>
|
||
<div style="font-size:.83rem;color:var(--muted);white-space:pre-wrap;margin-bottom:10px"><?= htmlspecialchars(mb_substr($r['body'], 0, 200)) ?><?= mb_strlen($r['body'])>200?'…':'' ?></div>
|
||
<?php endif; ?>
|
||
<?php if (!$isCur): ?>
|
||
<form method="post" onsubmit="return confirm('<?= htmlspecialchars($r['tag_name']) ?> installieren?')">
|
||
<?= csrf_field() ?>
|
||
<input type="hidden" name="action" value="auto_update">
|
||
<input type="hidden" name="tag_name" value="<?= htmlspecialchars($r['tag_name']) ?>">
|
||
<button type="submit" class="btn <?= $isNew?'':'secondary' ?>" style="font-size:.82rem;padding:6px 14px">⬆️ Installieren</button>
|
||
</form>
|
||
<?php endif; ?>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<script>
|
||
document.getElementById('rel-modal')?.addEventListener('click', function(e){ if(e.target===this) this.style.display='none'; });
|
||
document.addEventListener('keydown', function(e){ if(e.key==='Escape') document.getElementById('rel-modal').style.display='none'; });
|
||
</script>
|
||
|
||
<?php require __DIR__ . '/_footer.php'; ?>
|