250 lines
18 KiB
PHP
250 lines
18 KiB
PHP
<?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 & 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'; ?>
|