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]; } /** Hinweis für die Fehlersuche bei Schreibrechten: unter welchem Benutzer läuft PHP, wem gehört die Programmdatei. */ function update_owner_hint(string $sample = ''): string { if (!function_exists('posix_geteuid')) return ''; $detail = ''; if ($sample !== '') { $path = realpath(__DIR__ . '/..') . '/' . $sample; $dir = dirname($path); $nm = fn($path) => (($o = @fileowner($path)) !== false) ? (@posix_getpwuid($o)['name'] ?? (string) $o) : '?'; $detail = ' Beispiel ' . $sample . ' (Besitzer ' . $nm($path) . ', Ordner-Besitzer ' . $nm($dir) . '): ' . (is_file($path) ? 'Rechte ' . substr(sprintf('%o', (int) @fileperms($path)), -4) . ', beschreibbar: ' . (is_writable($path) ? 'ja' : 'nein') : 'Datei fehlt') . (!empty($GLOBALS['update_swap_reasons']) ? ' Ordner neu anlegen nicht möglich: ' . implode('; ', array_map(fn($k, $v) => "$k: $v", array_keys($GLOBALS['update_swap_reasons']), $GLOBALS['update_swap_reasons'])) . '.' : '') . '; Ordner Rechte ' . substr(sprintf('%o', (int) @fileperms($dir)), -4) . ', beschreibbar: ' . (is_writable($dir) ? 'ja' : 'nein') . '.'; } $me = @posix_getpwuid(posix_geteuid())['name'] ?? (string) posix_geteuid(); $owner = @fileowner(__DIR__ . '/../version.php'); $ownerName = $owner !== false ? (@posix_getpwuid($owner)['name'] ?? (string) $owner) : '?'; return $detail . " Der Webserver läuft als „{$me}“, die Programmdateien gehören „{$ownerName}“. Der Webserver-Benutzer muss die Dateien ändern dürfen (Besitzer auf „{$me}“ ändern oder Schreibrechte für die Gruppe geben; beim Hoster im Dateimanager oder per Support)."; } /** Kopiert einen Ordner samt Inhalt. */ function update_copy_tree(string $from, string $to): bool { if (!is_dir($to) && !@mkdir($to, 0755)) return false; foreach (scandir($from) ?: [] as $e) { if ($e === '.' || $e === '..') continue; if (is_dir("$from/$e") ? !update_copy_tree("$from/$e", "$to/$e") : !@copy("$from/$e", "$to/$e")) return false; } return true; } /** Löscht einen Ordner samt Inhalt, soweit erlaubt. */ function update_remove_tree(string $dir): void { foreach (scandir($dir) ?: [] as $e) { if ($e === '.' || $e === '..') continue; is_dir("$dir/$e") && !is_link("$dir/$e") ? update_remove_tree("$dir/$e") : @unlink("$dir/$e"); } @rmdir($dir); } /** * Der Webserver darf in einen Ordner nicht schreiben (er gehört einem anderen Benutzer, z. B. root): Dann wird der Ordner samt Inhalt neu angelegt, * und zwar der oberste nicht beschreibbare Ordner unterhalb des ersten beschreibbaren. Das geht, solange der DeskHub-Hauptordner dem Webserver gehört: * Umbenennen und Neuanlegen im beschreibbaren Elternordner ist unabhängig vom Besitzer der Dateien darin. Der Inhalt wird kopiert, der alte Ordner * gelöscht oder, wenn das nicht erlaubt ist, als „….alt-…“ stehen gelassen (und per .htaccess gesperrt). */ function update_swap_dir(string $dir): bool { $root = (string) realpath(__DIR__ . '/..'); $name = basename($dir); if (!is_dir($dir) || is_link($dir)) return false; $cand = $dir; while (!is_writable(dirname($cand))) { if ($cand === $root || dirname($cand) === $cand) { $GLOBALS['update_swap_reasons'][$name] = 'auch der DeskHub-Hauptordner ist für den Webserver nicht beschreibbar'; return false; } $cand = dirname($cand); } if ($cand === $root) return false; // Daten- und Konfigurationsordner werden nie umgesetzt (Größe, laufende Zugriffe) $top = explode('/', trim(str_replace('\\', '/', substr($cand, strlen($root))), '/'))[0]; if (in_array($top, ['config', 'database', 'uploads', 'backups', 'plugins', 'app_downloads'], true)) { $GLOBALS['update_swap_reasons'][$name] = "Ordner „{$top}“ gehört einem anderen Benutzer und wird aus Sicherheitsgründen nicht umgesetzt"; return false; } $old = $cand . '.alt-' . date('Ymd-His'); if (!@rename($cand, $old)) { $GLOBALS['update_swap_reasons'][$name] = 'Umbenennen nicht erlaubt (' . trim((string) (error_get_last()['message'] ?? '')) . ')'; return false; } if (!update_copy_tree($old, $cand)) { update_remove_tree($cand); @rename($old, $cand); $GLOBALS['update_swap_reasons'][$name] = 'Inhalt konnte nicht kopiert werden'; return false; } update_remove_tree($old); if (is_dir($old)) { $GLOBALS['update_leftovers'][] = ltrim(str_replace('\\', '/', substr($old, strlen($root))), '/'); update_block_leftovers($root); } return true; } /** Trägt in die .htaccess im Hauptordner eine Regel ein, die übrig gebliebene „….alt-…“-Ordner nicht ausliefert (falls noch nicht vorhanden). */ function update_block_leftovers(string $root): void { $file = $root . '/.htaccess'; $rule = "\n# DeskHub: übrig gebliebene alte Ordner eines Updates nicht ausliefern\nRewriteCond %{REQUEST_URI} \.alt-[0-9]{8}-[0-9]{6}(/|$) [NC]\nRewriteRule ^ - [F,L]\n"; $cur = is_file($file) ? (string) @file_get_contents($file) : ''; if ($cur !== '' && !str_contains($cur, '.alt-[0-9]{8}')) update_write_file($file, $cur . $rule); } /** Schreibt eine Datei. Gelingt das nicht (z. B. gehört die Datei dem FTP-Benutzer), wird sie über eine Hilfsdatei im selben Ordner ersetzt. */ function update_write_file(string $dest, string $data): bool { if (@file_put_contents($dest, $data) !== false) return true; // Selbstheilung: Gehören Datei oder Ordner dem Webserver-Benutzer, aber ohne Schreibrecht, wird das Recht gesetzt (fremde Dateien kann PHP nicht ändern) $dir = dirname($dest); if (function_exists('posix_geteuid')) { $me = posix_geteuid(); if (is_file($dest) && @fileowner($dest) === $me && !is_writable($dest)) @chmod($dest, 0644); if (@fileowner($dir) === $me && !is_writable($dir)) @chmod($dir, 0755); if (@file_put_contents($dest, $data) !== false) return true; } $tmp = $dest . '.neu' . bin2hex(random_bytes(3)); if (@file_put_contents($tmp, $data) === false) return false; @unlink($dest); if (@rename($tmp, $dest)) return true; @unlink($tmp); // Letzter Ausweg: Ordner gehört einem anderen Benutzer, der übergeordnete ist beschreibbar → Ordner neu anlegen if (!is_writable($dir) && update_swap_dir($dir)) return @file_put_contents($dest, $data) !== false; return false; } // ── 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', '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; $deferred = null; $unchanged = 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 // 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 (update_path_is_skipped($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; // Zugriffsregeln (.htaccess) gehören dem Server: Hoster tragen dort eigene Zeilen ein (z. B. die PHP-Version). Eine vorhandene wird nie überschrieben. if (strtolower(basename($relNorm)) === '.htaccess' && is_file($dest)) { $unchanged++; continue; } // Unveränderte Dateien brauchen nicht geschrieben zu werden (gehören sie dem FTP-Benutzer, wäre das sonst ein unnötiger Fehler) if (is_file($dest) && filesize($dest) === strlen($data) && hash_equals(md5($data), (string) md5_file($dest))) { $unchanged++; continue; } // Diese Seite wird zuletzt ersetzt: Das laufende Skript ist schon geladen, so mischt sich nichts Altes und Neues im selben Aufruf if ($relNorm === 'staff/update.php') { $deferred = [$dest, $data, $rel]; continue; } if (update_write_file($dest, $data)) { $extracted++; } else { $failed[] = $rel; } } if ($deferred) { if (is_file($deferred[0]) && md5_file($deferred[0]) === md5($deferred[1])) $unchanged++; elseif (update_write_file($deferred[0], $deferred[1])) $extracted++; else $failed[] = $deferred[2]; } $zip->close(); if ($extracted === 0 && $unchanged === 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, 'unchanged' => $unchanged, 'failed' => $failed]; } /** * Installiert ein geprüftes Paket: Backup, Dateien, Datenbank-Update, Versionsnummer. Bei Erfolg Weiterleitung, sonst die Fehlertexte. * * @return list */ function update_finish_install(string $tmp, string $tagName, bool $signatureChecked, array $staff, string $sourceLabel): array { $errors = []; $failedNote = ''; $bk = create_backup(); $res = do_install($tmp); @unlink($tmp); if (!$res['ok']) return [$res['error']]; // 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 ($sourceLabel): {$res['extracted']} Dateien{$migNote}" . ($signatureChecked ? ' (Signatur geprüft)' : '')); // Versionsnummer in version.php aktualisieren $newVer = ltrim($tagName, 'v'); $verFile = realpath(__DIR__ . '/../version.php'); $versionOk = false; if ($verFile && $newVer) { $vc = file_get_contents($verFile); $vc = preg_replace("/define\('DESKHUB_VERSION',\s*'[^']+'\)/", "define('DESKHUB_VERSION', '$newVer')", $vc); $versionOk = update_write_file($verFile, $vc) && str_contains((string) @file_get_contents($verFile), "'$newVer'"); } if (!$versionOk) $errors[] = "Die Dateien wurden installiert, aber version.php konnte nicht auf $newVer geändert werden (keine Schreibrechte). Bitte die Datei version.php im Hauptordner von Hand auf Version $newVer setzen oder ihre Rechte prüfen (der Webserver-Benutzer muss sie ändern dürfen)."; if ($res['failed']) $failedNote = ' ⚠ ' . count($res['failed']) . ' Datei(en) konnten nicht ersetzt werden und blieben auf dem alten Stand (sie gehören einem anderen Benutzer, nur der Hoster kann das ändern): ' . implode(', ', array_slice($res['failed'], 0, 8)) . (count($res['failed']) > 8 ? ' …' : '') . update_owner_hint((string) $res['failed'][0]); $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); if ($errors) return $errors; flash('success', "✅ Update auf $newVer: {$res['extracted']} Dateien." . ($signatureChecked ? ' Signatur geprüft.' : '') . (!empty($GLOBALS['update_leftovers']) ? ' Alte Ordner, die sich nicht löschen ließen (bitte per FTP entfernen): ' . implode(', ', $GLOBALS['update_leftovers']) . '.' : '') . ($bk['ok'] ? " Backup: {$bk['filename']}" : '') . $failedNote); redirect('/staff/update.php'); return []; } // ── 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'); } } } // Update von der Plugin-Webseite des Herausgebers (Hauptquelle): Prüfsumme und, mit eingebautem Schlüssel, Signatur werden geprüft. if ($action === 'auto_update' && preg_match('/^v?\d+\.\d+\.\d+([\-+][0-9A-Za-z.\-]+)?$/', trim($_POST['tag_name'] ?? '')) && ($pubRel = update_publisher_find(trim($_POST['tag_name'])))) { [$pubTmp, $pubErr] = update_publisher_fetch($pubRel); if ($pubErr !== null) { audit_log($staff['id'], 'Update ' . $pubRel['tag_name'] . ' abgelehnt: ' . $pubErr); $errors[] = $pubErr; } else { $errors = array_merge($errors, update_finish_install($pubTmp, (string) $pubRel['tag_name'], update_signature_required(), $staff, 'Plugin-Webseite')); } $action = 'handled'; } // Ausweichquelle: Auto-Download aus den Gitea-Releases. 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 = update_publisher_releases(); $relSource = $releases ? 'Plugin-Webseite' : 'Gitea'; $relErr = ''; $code = $releases ? 200 : 0; if (!$releases) [$code, $resp] = update_download(UPDATE_REPO_API . '/releases?limit=20', update_http_headers(), 8); if ($releases) { // Hauptquelle: Plugin-Webseite } elseif ($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'; ?>

🔄 System-Update

Die Liste der Versionen konnte nicht geladen werden:
Update verfügbar: Version vom
Änderungen:
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
Updates kommen von: 1]) !== '' ? ' (Plugin-Webseite nicht erreichbar oder ohne Update)' : '' ?>
Manuelles Update

Eine ZIP-Datei des Herausgebers hochladen und installieren.

1): ?>

Bestimmte Version installieren

Zugang zum Update-Server

Nur für die Ausweichquelle Gitea (wenn die Plugin-Webseite keine Updates liefert). Nötig, wenn das Repository mit den Releases privat ist. Gitea meldet dann beim Laden der Versionen „404“. Ein Token mit Leserecht genügt (in Gitea: Einstellungen → Anwendungen → Zugriffstoken erzeugen, Berechtigung „repository: Lesen“). Es wird verschlüsselt gespeichert.

✓ Token aus config/config.php wird verwendet. Es hat Vorrang vor einem hier gespeicherten.

✓ Token gespeichert. Zum Ersetzen unten ein neues eintragen.

Kein Token eingetragen (öffentlicher Zugriff).

Alternative: in config/config.php eintragen

In der Datei config/config.php (Hauptordner von DeskHub, im Ordner config) als eigener Eintrag im großen return [ … ];, zum Beispiel direkt unter 'app_key':

'update' => ['token' => 'HIER_DAS_TOKEN'],

Hinweise