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'] ?? ''; // 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() === [] ? ' (Ist das Repository privat? Dann in config/config.php unter update → token ein Lese-Token eintragen.)' : ''); } $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'; ?>

🔄 System-Update

Update verfügbar: Version vom
Datenbank
ausstehende Datenbank-Änderung(en)
Der Code ist neuer als die Datenbank. Vor dem Einspielen wird automatisch ein Backup erstellt.
Details
✓ Die Datenbank ist aktuell
Alle bekannten Datenbank-Änderungen sind eingespielt.
Installierte Version
DeskHub Ticketsystem
Manuelles Update

ZIP-Datei von Gitea hochladen und installieren.

1): ?>

Bestimmte Version installieren

Hinweise