Files
deskhub/DeskHub-Webseite/staff/plugins.php
T
2026-10-24 20:14:39 +00:00

250 lines
18 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__ . '/_ui.php';
$staff = require_admin();
$pdo = get_db();
$errors = [];
$permLabels = plugin_known_permissions();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
verify_csrf();
$action = $_POST['action'] ?? '';
$slug = (string) ($_POST['slug'] ?? '');
if ($action === 'save_settings' && plugin_strict_mode()) {
$errors[] = 'Im geprüften Modus sind Katalog und Schlüssel fest vorgegeben.';
} elseif ($action === 'save_settings') {
$url = trim((string) ($_POST['catalog_url'] ?? ''));
$key = trim((string) ($_POST['publisher_key'] ?? ''));
if ($url !== '' && ($e = plugin_url_check($url))) $errors[] = 'Katalog-Adresse: ' . $e;
if ($key !== '' && !plugin_public_key_valid($key)) $errors[] = 'Der Herausgeber-Schlüssel ist kein gültiger öffentlicher RSA-Schlüssel (PEM, mindestens 2048 Bit).';
if (!empty($_POST['require_signature']) && $key === '') $errors[] = 'Für „Signatur erforderlich“ muss ein Herausgeber-Schlüssel hinterlegt sein.';
if (!$errors) {
set_setting('plugin_catalog_url', $url);
set_setting('plugin_publisher_key', $key);
set_setting('plugin_require_signature', !empty($_POST['require_signature']) ? '1' : '0');
audit_log($staff['id'], 'Plugin-Einstellungen geändert (Katalog/Herausgeber-Schlüssel)');
flash('success', 'Einstellungen gespeichert.');
redirect('/staff/plugins.php');
}
}
if ($action === 'install') {
$f = $_FILES['plugin_zip'] ?? [];
if (empty($_POST['trust'])) {
$errors[] = 'Bitte bestätigen, dass du der Quelle des Plugins vertraust.';
} elseif (($f['error'] ?? 99) !== UPLOAD_ERR_OK) {
$errors[] = 'Upload-Fehler (Code ' . ($f['error'] ?? 'unbekannt') . ').';
} elseif (strtolower(pathinfo((string) ($f['name'] ?? ''), PATHINFO_EXTENSION)) !== 'zip') {
$errors[] = 'Nur ZIP-Dateien sind erlaubt.';
} else {
$res = plugin_install_zip($pdo, $f['tmp_name'], trim((string) ($_POST['sha256'] ?? '')), trim((string) ($_POST['signature'] ?? '')), 'upload');
if (!$res['ok']) {
$errors[] = $res['error'];
} else {
audit_log($staff['id'], ($res['updated'] ? 'Plugin aktualisiert: ' : 'Plugin installiert: ') . "{$res['slug']} {$res['version']} (SHA-256 {$res['hash']}" . ($res['signed'] ? ', signiert' : ', nicht signiert') . ')');
flash('success', plugin_install_message($res, plugins_installed($pdo)[$res['slug']] ?? []));
redirect('/staff/plugins.php');
}
}
}
if ($action === 'catalog_install' && plugin_valid_slug($slug)) {
$res = plugin_catalog_install($pdo, $slug);
if ($res['ok']) {
audit_log($staff['id'], ($res['updated'] ? 'Plugin aus dem Katalog aktualisiert: ' : 'Plugin aus dem Katalog installiert: ') . "{$res['slug']} {$res['version']} (SHA-256 {$res['hash']}" . ($res['signed'] ? ', signiert' : ', nicht signiert') . ')');
flash('success', plugin_install_message($res, plugins_installed($pdo)[$res['slug']] ?? []));
redirect('/staff/plugins.php');
}
$errors[] = $res['error'];
}
if (in_array($action, ['activate', 'deactivate'], true) && plugin_valid_slug($slug)) {
if ($action === 'activate') {
$bk = create_backup(); // vor Datenbank-Änderungen des Plugins
if (!$bk['ok']) $errors[] = 'Backup fehlgeschlagen – das Plugin wurde nicht aktiviert: ' . ($bk['error'] ?? 'unbekannter Fehler');
}
if (!$errors) {
$manifest = plugin_read_manifest(plugins_dir() . '/' . $slug)[0];
// Signierte Plugins aus dem Katalog des Herausgebers brauchen keine zusätzliche Bestätigung; manuell hochgeladene schon
$row = plugins_installed($pdo)[$slug] ?? [];
$fromCatalog = ($row['source'] ?? '') === 'catalog' && !empty($row['signature_ok']);
$approve = (!empty($_POST['confirm_permissions']) || $fromCatalog) && $manifest ? $manifest['permissions'] : null;
$res = plugin_set_active($pdo, $slug, $action === 'activate', $approve);
if ($res['ok']) {
audit_log($staff['id'], ($action === 'activate' ? 'Plugin aktiviert: ' : 'Plugin deaktiviert: ') . $slug);
flash('success', $action === 'activate' ? 'Plugin aktiviert.' : 'Plugin deaktiviert.');
redirect('/staff/plugins.php');
}
$errors[] = $res['error'];
}
}
if ($action === 'uninstall' && plugin_valid_slug($slug)) {
$res = plugin_uninstall($pdo, $slug, !empty($_POST['remove_data']));
if ($res['ok']) {
audit_log($staff['id'], 'Plugin entfernt: ' . $slug . (!empty($_POST['remove_data']) ? ' (inkl. Daten)' : ''));
flash('success', 'Plugin entfernt.');
redirect('/staff/plugins.php');
}
$errors[] = $res['error'];
}
}
$installed = plugins_installed($pdo);
$manifests = [];
foreach ($installed as $slug => $row) $manifests[$slug] = plugin_read_manifest(plugins_dir() . '/' . $slug)[0];
$disabled = plugins_globally_disabled();
$strict = plugin_strict_mode();
$official = plugin_official_configured();
$catalog = plugin_catalog_url() !== '' ? plugin_catalog_cached(isset($_GET['refresh']), 60) : null;
// Katalogeinträge je Plugin (für die Vorschaubilder der installierten Plugins)
$catBySlug = [];
if ($catalog && !empty($catalog['ok'])) foreach ($catalog['plugins'] as $__c) $catBySlug[$__c['slug']] = $__c;
$hasKey = plugin_publisher_key() !== '';
$catalogHost = (string) (parse_url(plugin_catalog_url(), PHP_URL_HOST) ?: '');
$pageTitle = 'Plugins';
$activePage = 'plugins';
require __DIR__ . '/_header.php';
?>
<h1 style="margin-top:0">Plugins</h1>
<?php foreach ($errors as $err): ?><div class="flash error"><?= e($err) ?></div><?php endforeach; ?>
<?php $updates = plugin_updates_available($pdo); if ($updates): ?>
<div class="flash warning"><strong>Updates verfügbar:</strong> <?= e(implode(', ', array_map(fn($u) => $u['name'] . ' ' . $u['installed'] . ' → ' . $u['available'], $updates))) ?>. Unten im Katalog mit „Aktualisieren“ installieren.</div>
<?php endif; ?>
<?php if ($disabled): ?>
<div class="flash error">Alle Plugins sind abgeschaltet (Datei <code>plugins/.disabled</code> oder Einstellung <code>plugins.enabled = false</code>). Bereits aktive Plugins werden nicht geladen.</div>
<?php endif; ?>
<div class="card">
<table>
<thead><tr><th>Plugin</th><th>Version</th><th>Status</th><th>Herkunft</th><th></th></tr></thead>
<tbody>
<?php foreach ($installed as $slug => $p): $m = $manifests[$slug]; $perms = $m['permissions'] ?? []; ?>
<tr>
<td>
<div style="display:flex;gap:12px;align-items:flex-start"><?= catalog_thumb('plugin', $catBySlug[$slug] ?? ['slug' => $slug, 'name' => $p['name']]) ?><div style="min-width:0">
<strong><?= e($p['name']) ?></strong> <span class="muted" style="font-size:.78rem"><?= e($slug) ?></span>
<?php if ($m && $m['description'] !== ''): ?><div class="muted" style="font-size:.82rem"><?= e($m['description']) ?></div><?php endif; ?>
<?php if ($m && $m['author'] !== ''): ?><div class="muted" style="font-size:.78rem">von <?= e($m['author']) ?></div><?php endif; ?>
<?php if ($perms): ?>
<details style="font-size:.78rem;margin-top:4px"><summary>Berechtigungen (<?= count($perms) ?>)</summary>
<ul style="margin:4px 0 0;padding-left:18px"><?php foreach ($perms as $perm): ?><li><?= e($permLabels[$perm] ?? $perm) ?> <span class="muted">(<?= e($perm) ?>)</span></li><?php endforeach; ?></ul>
</details>
<?php elseif ($m): ?><div class="muted" style="font-size:.78rem">Fordert keine Berechtigungen an.</div><?php endif; ?>
<?php if (!$m): ?><div style="color:#dc2626;font-size:.82rem">plugin.json fehlt oder ist ungültig – das Plugin kann nicht geladen werden.</div><?php endif; ?>
</div></div>
</td>
<td><?= e($p['version']) ?></td>
<td>
<?php if ($p['is_active']): ?><span style="color:#15803d;font-weight:600">Aktiv</span>
<?php elseif ((int) $p['fail_count'] >= PLUGIN_FAIL_LIMIT): ?><span style="color:#dc2626;font-weight:600">Wegen Fehlern deaktiviert</span>
<?php else: ?><span class="muted">Inaktiv</span><?php endif; ?>
<?php if (!empty($p['last_error'])): ?><div style="color:#b45309;font-size:.74rem;margin-top:4px;max-width:260px;white-space:normal;word-break:break-word"><?= e(mb_strimwidth((string) $p['last_error'], 0, 300, '…')) ?></div><?php endif; ?>
</td>
<td style="font-size:.82rem">
<?php if (!empty($p['signature_ok'])): ?><span style="color:#15803d;font-weight:600">✓ Signiert</span>
<?php else: ?><span style="color:#b45309;font-weight:600">Eigenes Plugin</span><?php endif; ?><br>
<span class="muted"><?= ($p['source'] ?? '') === 'catalog' ? 'Aus dem Katalog' : (!empty($p['signature_ok']) ? 'Manuell hochgeladen' : 'Nicht vom Herausgeber geprüft') ?></span>
</td>
<td style="white-space:nowrap">
<?php if ($p['is_active']): ?>
<form method="post" style="display:inline"><?= csrf_field() ?><input type="hidden" name="slug" value="<?= e($slug) ?>"><input type="hidden" name="action" value="deactivate"><button class="btn secondary" type="submit">Deaktivieren</button></form>
<?php else: ?>
<form method="post" style="display:inline-block;text-align:left" onsubmit="return confirm('Plugin aktivieren? Vorher wird ein Backup erstellt und evtl. Datenbank-Änderungen des Plugins eingespielt.')">
<?= csrf_field() ?><input type="hidden" name="slug" value="<?= e($slug) ?>"><input type="hidden" name="action" value="activate">
<?php if ($perms && !(($p['source'] ?? '') === 'catalog' && !empty($p['signature_ok']))): ?><label style="font-size:.76rem;display:block"><input type="checkbox" name="confirm_permissions" value="1" required> Berechtigungen bestätigen</label><?php endif; ?>
<button class="btn" type="submit" <?= $m ? '' : 'disabled' ?>>Aktivieren</button>
</form>
<?php endif; ?>
<form method="post" style="display:inline" onsubmit="return confirm('Plugin wirklich entfernen?')">
<?= csrf_field() ?><input type="hidden" name="slug" value="<?= e($slug) ?>"><input type="hidden" name="action" value="uninstall">
<label style="font-size:.78rem"><input type="checkbox" name="remove_data" value="1"> Daten löschen</label>
<button type="submit" style="background:none;border:none;color:#dc2626;cursor:pointer">Entfernen</button>
</form>
</td>
</tr>
<?php endforeach; ?>
<?php if (!$installed): ?><tr><td colspan="5" class="muted">Noch keine Plugins installiert.</td></tr><?php endif; ?>
</tbody>
</table>
</div>
<div class="card" style="margin-top:20px">
<div style="display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap">
<h3 style="margin:0"><?= $official ? 'Geprüfte Plugins' : 'Plugin-Katalog' ?></h3>
<?php if ($catalog): ?><a class="btn secondary" href="/staff/plugins.php?refresh=1">Katalog aktualisieren</a><?php endif; ?>
</div>
<?php if (!$catalog): ?>
<p class="muted" style="font-size:.85rem">Es ist noch keine Katalog-Adresse hinterlegt. Trage unten die Adresse der Plugin-Webseite ein, dann könnt ihr Plugins direkt von dort installieren.</p>
<?php elseif (!$catalog['ok']): ?>
<div class="flash error" style="margin-top:12px"><?= e($catalog['error']) ?></div>
<?php else: ?>
<?php if (!empty($catalog['cached_at'])): ?><div class="muted" style="font-size:.76rem;margin-top:6px">Stand: <?= e(date('d.m.Y H:i', (int) $catalog['cached_at'])) ?></div><?php endif; ?>
<table style="margin-top:12px">
<thead><tr><th>Plugin</th><th>Version</th><th>Signatur</th><th></th></tr></thead>
<tbody>
<?php foreach ($catalog['plugins'] as $c): $have = $installed[$c['slug']] ?? null; $newer = $have && version_compare($c['version'], $have['version'], '>'); ?>
<tr>
<td><div style="display:flex;gap:12px;align-items:flex-start"><?= catalog_thumb('plugin', $c) ?><div style="min-width:0">
<strong><?= e($c['name']) ?></strong> <?php if (!empty($c['language'])): ?><span title="Sprachpaket: nach der Aktivierung erscheint die Flagge zum Umschalten" style="background:#e8f1ff;color:#1d4ed8;border:1px solid #bcd3f7;border-radius:20px;padding:1px 9px;font-size:.72rem;font-weight:700">🌐 Sprache: <?= e($c['language']['name']) ?></span> <?php endif; ?><?php if (!empty($c['featured'])): ?><span title="Vom Herausgeber empfohlen" style="background:#fff4d6;color:#92600a;border:1px solid #f1d38a;border-radius:20px;padding:1px 9px;font-size:.72rem;font-weight:700">★ Empfohlen</span><?php endif; ?> <span class="muted" style="font-size:.78rem"><?= e($c['slug']) ?></span>
<div class="muted" style="font-size:.82rem"><?= e($c['description']) ?></div>
<?php if ($c['author'] !== ''): ?><div class="muted" style="font-size:.78rem">von <?= e($c['author']) ?></div><?php endif; ?>
<?php if ($c['permissions']): ?><div class="muted" style="font-size:.76rem">Fordert an: <?= e(implode(', ', array_map(fn($p) => $permLabels[$p] ?? $p, $c['permissions']))) ?></div><?php endif; ?>
</div></div></td>
<td><?= e($c['version']) ?><?= $have ? '<div class="muted" style="font-size:.76rem">installiert: ' . e($have['version']) . '</div>' : '' ?></td>
<td style="font-size:.82rem"><?= $c['signature'] !== '' ? ($hasKey ? '<span style="color:#15803d;font-weight:600">✓ Signiert</span>' : '<span style="color:#b45309">Signatur vorhanden, Schlüssel fehlt</span>') : '<span style="color:#b45309">Nicht signiert</span>' ?></td>
<td>
<?php if ($have && !$newer): ?><span class="muted">aktuell</span>
<?php elseif ($strict && $c['signature'] === ''): ?><span class="muted" style="font-size:.78rem">nicht signiert,<br>nicht installierbar</span>
<?php else: ?>
<form method="post"><?= csrf_field() ?><input type="hidden" name="action" value="catalog_install"><input type="hidden" name="slug" value="<?= e($c['slug']) ?>">
<button class="btn" type="submit"><?= $newer ? 'Aktualisieren' : 'Installieren' ?></button></form>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php if (!$catalog['plugins']): ?><tr><td colspan="4" class="muted">Der Katalog enthält keine Plugins.</td></tr><?php endif; ?>
</tbody>
</table>
<?php endif; ?>
</div>
<div class="card" style="margin-top:20px">
<h3 style="margin-top:0">Eigenes Plugin hochladen</h3>
<p class="muted" style="font-size:.85rem">Hier könnt ihr eigene Plugins als ZIP installieren oder aktualisieren, auch ohne Signatur. Diese Plugins sind <strong>nicht vom Herausgeber geprüft</strong> und laufen mit allen Rechten des Systems, auch mit Zugriff auf die gesamte Datenbank. Installiert nur Code, den ihr selbst geschrieben oder geprüft habt.</p>
<form method="post" enctype="multipart/form-data">
<?= csrf_field() ?><input type="hidden" name="action" value="install">
<label for="plugin_zip">ZIP-Datei</label>
<input type="file" id="plugin_zip" name="plugin_zip" accept=".zip" required>
<label for="signature">Signatur (optional, Base64)</label>
<input type="text" id="signature" name="signature" placeholder="Nur ausfüllen, wenn der Anbieter eine Signatur nennt" autocomplete="off">
<label for="sha256">Erwartete Prüfsumme (SHA-256, optional)</label>
<input type="text" id="sha256" name="sha256" placeholder="Nur ausfüllen, wenn der Anbieter eine Prüfsumme nennt" autocomplete="off">
<label style="margin-top:10px"><input type="checkbox" name="trust" value="1" required> Ich kenne den Inhalt dieses Plugins und vertraue ihm. Mir ist bewusst, dass es mit allen Rechten des Systems läuft.</label>
<div class="btn-row" style="margin-top:12px"><button type="submit" class="btn">Hochladen &amp; installieren</button></div>
</form>
<p class="muted" style="font-size:.82rem">Das Plugin wird nach der Installation nicht automatisch aktiviert. Vor dem Aktivieren bestätigt ihr die Berechtigungen, die es anfordert.</p>
</div>
<?php if (!$strict): ?>
<div class="card" style="margin-top:20px">
<h3 style="margin-top:0">Katalog und Herausgeber-Schlüssel</h3>
<form method="post">
<?= csrf_field() ?><input type="hidden" name="action" value="save_settings">
<label for="catalog_url">Katalog-Adresse (https)</label>
<input type="text" id="catalog_url" name="catalog_url" value="<?= e(plugin_catalog_url()) ?>" placeholder="https://plugins.example.de/catalog.php?type=plugins">
<label for="publisher_key">Öffentlicher Herausgeber-Schlüssel (PEM)</label>
<textarea id="publisher_key" name="publisher_key" rows="6" style="font-family:monospace;font-size:.78rem" placeholder="-----BEGIN PUBLIC KEY-----"><?= e(plugin_publisher_key()) ?></textarea>
<label style="margin-top:8px"><input type="checkbox" name="require_signature" value="1" <?= plugin_signatures_required() ? 'checked' : '' ?>> Nur signierte Plugins installieren (empfohlen, sobald ein Schlüssel hinterlegt ist)</label>
<p class="muted" style="font-size:.82rem">Den Schlüssel bekommst du von der Plugin-Webseite. Ist er hinterlegt, prüft DeskHub bei jeder Installation, ob das Paket wirklich vom Herausgeber stammt und unverändert ist.</p>
<div class="btn-row"><button type="submit" class="btn">Speichern</button></div>
</form>
</div>
<?php endif; ?>
<?php require __DIR__ . '/_footer.php'; ?>