530 lines
37 KiB
PHP
530 lines
37 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_update_all') {
|
||
$res = plugin_catalog_update_all($pdo);
|
||
if ($res['updated']) {
|
||
audit_log($staff['id'], 'Plugins aus dem Katalog aktualisiert: ' . implode(', ', $res['updated']));
|
||
flash('success', count($res['updated']) . ' Plugin(s) aktualisiert: ' . implode(', ', $res['updated']) . '.');
|
||
} elseif (!$res['failed']) {
|
||
flash('info', 'Alle Plugins sind aktuell.');
|
||
}
|
||
foreach ($res['failed'] as $failedSlug => $failedMsg) flash('error', $failedSlug . ': ' . $failedMsg);
|
||
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) ?: '');
|
||
|
||
$updates = plugin_updates_available($pdo);
|
||
$updBySlug = [];
|
||
foreach ($updates as $u) $updBySlug[$u['slug']] = $u;
|
||
// Zustand je Plugin: active | inactive | error; dazu Update-Kennzeichen
|
||
$stateOf = [];
|
||
$counts = ['all' => 0, 'active' => 0, 'inactive' => 0, 'error' => 0, 'update' => 0];
|
||
foreach ($installed as $slug => $p) {
|
||
$bad = !$manifests[$slug] || (!$p['is_active'] && (int) $p['fail_count'] >= PLUGIN_FAIL_LIMIT);
|
||
$st = $bad ? 'error' : ($p['is_active'] ? 'active' : 'inactive');
|
||
$stateOf[$slug] = $st;
|
||
$counts['all']++;
|
||
$counts[$st]++;
|
||
if (isset($updBySlug[$slug])) $counts['update']++;
|
||
}
|
||
// Erste Menüseite eines geladenen Plugins für den Link „Öffnen“
|
||
$openUrl = [];
|
||
foreach (plugin_state()['menus'] as $__m) {
|
||
if (!isset($openUrl[$__m['plugin']])) $openUrl[$__m['plugin']] = '/staff/plugin.php?p=' . rawurlencode($__m['plugin']) . '&page=' . rawurlencode($__m['page']);
|
||
}
|
||
$tab = in_array($_GET['tab'] ?? '', ['add', 'settings'], true) ? $_GET['tab'] : 'installed';
|
||
if ($errors) {
|
||
$failedAction = $_POST['action'] ?? '';
|
||
if (in_array($failedAction, ['install', 'catalog_install'], true)) $tab = 'add';
|
||
if ($failedAction === 'save_settings') $tab = 'settings';
|
||
}
|
||
|
||
$banner = function (array $c, string $name): string {
|
||
$inner = !empty($c['preview'])
|
||
? '<img src="/staff/catalog_preview.php?kind=plugin&slug=' . e($c['slug']) . '" alt="">'
|
||
: '<span>' . e(mb_strtoupper(mb_substr($name, 0, 1))) . '</span>';
|
||
return '<div class="pl-hero">' . $inner . '</div>';
|
||
};
|
||
$pageTitle = 'Plugins';
|
||
$activePage = 'plugins';
|
||
require __DIR__ . '/_header.php';
|
||
?>
|
||
<style>
|
||
.pl-head { display:flex; align-items:center; gap:14px; flex-wrap:wrap; margin-bottom:6px }
|
||
.pl-head h1 { margin:0 }
|
||
.pl-tabs { display:flex; gap:4px; border-bottom:1px solid var(--border); margin:14px 0 16px; flex-wrap:wrap }
|
||
.pl-tabs button { background:none; border:none; border-bottom:3px solid transparent; padding:9px 14px; font:inherit; font-weight:600; color:var(--muted); cursor:pointer }
|
||
.pl-tabs button[aria-selected=true] { color:var(--primary); border-bottom-color:var(--primary) }
|
||
.pl-bar { display:flex; justify-content:space-between; align-items:center; gap:12px; flex-wrap:wrap; margin-bottom:12px }
|
||
.pl-filters { display:flex; gap:4px 14px; flex-wrap:wrap; font-size:.9rem }
|
||
.pl-filters button { background:none; border:none; padding:2px 0; font:inherit; color:var(--primary); cursor:pointer }
|
||
.pl-filters button[aria-pressed=true] { color:inherit; font-weight:700; border-bottom:2px solid var(--primary) }
|
||
.pl-filters .n { color:var(--muted); font-weight:400 }
|
||
.pl-search { max-width:260px; margin:0 }
|
||
.pl-card[hidden] { display:none }
|
||
.pl-title { display:flex; gap:12px; align-items:flex-start }
|
||
.pl-title strong { font-size:1.02rem }
|
||
.pl-actions { margin-top:8px; display:flex; flex-wrap:wrap; gap:6px 14px; align-items:center; font-size:.86rem }
|
||
.pl-actions form { display:inline; margin:0 }
|
||
.pl-link { background:none; border:none; padding:0; font:inherit; color:var(--primary); cursor:pointer; text-decoration:none }
|
||
.pl-link:hover { text-decoration:underline }
|
||
.pl-link.danger { color:#dc2626 }
|
||
.pl-meta { font-size:.82rem; color:var(--muted) }
|
||
.pl-meta p { margin:0 0 6px; color:inherit }
|
||
.pl-tag { display:inline-block; border-radius:20px; padding:1px 9px; font-size:.72rem; font-weight:700; border:1px solid var(--border); margin-right:4px }
|
||
.pl-tag.ok { background:#e8f6ee; color:#15803d; border-color:#b7e1c6 }
|
||
.pl-tag.warn { background:#fff4d6; color:#92600a; border-color:#f1d38a }
|
||
.pl-tag.bad { background:#fdeaea; color:#b91c1c; border-color:#f3b9b9 }
|
||
.pl-tag.info { background:#e8f1ff; color:#1d4ed8; border-color:#bcd3f7 }
|
||
.pl-err { color:#b45309; font-size:.8rem; margin-top:6px; word-break:break-word }
|
||
.pl-empty { padding:26px; text-align:center; color:var(--muted) }
|
||
.pl-empty[hidden] { display:none }
|
||
.pl-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(420px,1fr)); gap:16px }
|
||
.pl-card { background:var(--card,#fff); border:1px solid var(--border); border-radius:6px; display:flex; flex-direction:column; overflow:hidden }
|
||
.pl-card-top { display:grid; grid-template-columns:128px minmax(0,1fr) auto; gap:16px; padding:18px 18px 14px; align-items:start }
|
||
.pl-card.st-active { border-top:3px solid #15803d }
|
||
.pl-card.st-error { border-top:3px solid #dc2626 }
|
||
.pl-card-icon img, .pl-card-icon div { width:128px !important; height:128px !important; border-radius:14px !important; object-fit:cover; font-size:2.6rem !important }
|
||
.pl-card-main h4 { margin:0 0 8px; font-size:1.05rem; color:var(--primary) }
|
||
.pl-card-main p { margin:0 0 8px; font-size:.86rem; line-height:1.45 }
|
||
.pl-card-main p.by { font-style:italic; color:var(--muted); font-size:.82rem }
|
||
.pl-card-act { display:flex; flex-direction:column; align-items:flex-end; gap:12px }
|
||
.pl-card-act .btn { white-space:nowrap }
|
||
.pl-installed-badge { display:inline-block; padding:8px 14px; border-radius:8px; background:#e8f6ee; color:#15803d; border:1px solid #b7e1c6; font-weight:700; font-size:.88rem; white-space:nowrap }
|
||
.pl-card-act .pl-install { background:var(--primary); color:#fff; font-weight:700 }
|
||
.pl-more { font-size:.85rem; color:var(--primary) }
|
||
.pl-card-main p:not(.by) { display:-webkit-box; -webkit-line-clamp:4; line-clamp:4; -webkit-box-orient:vertical; overflow:hidden }
|
||
.pl-card-detail { display:none }
|
||
#pl-modal { margin:auto; border:none; border-radius:8px; padding:0; width:min(900px, 94vw); max-height:90vh; background:var(--card,#fff); color:inherit; overflow:hidden }
|
||
#pl-modal::backdrop { background:rgba(0,0,0,.65) }
|
||
#pl-modal .pl-modal-inner { display:flex; flex-direction:column; max-height:90vh }
|
||
#pl-modal .pl-card-detail { display:flex; flex-direction:column; min-height:0; flex:1; overflow:hidden; padding:0 }
|
||
#pl-modal .pl-modal-body { display:grid; grid-template-columns:minmax(0,1fr) 230px; overflow:auto; min-height:0 }
|
||
#pl-modal .pl-modal-text { padding:20px 24px; font-size:.92rem; line-height:1.55 }
|
||
#pl-modal .pl-modal-text h3 { margin:0 0 10px; font-size:1.25rem }
|
||
#pl-modal .pl-modal-text h4 { margin:16px 0 6px }
|
||
#pl-modal .pl-modal-side { padding:20px; background:var(--bg-soft,#f6f8fa); border-left:1px solid var(--border-soft); font-size:.84rem }
|
||
#pl-modal .pl-modal-side p { margin:0 0 10px }
|
||
#pl-modal .pl-modal-foot { padding:12px 20px; border-top:1px solid var(--border-soft); display:flex; flex-direction:row-reverse; justify-content:space-between; align-items:center; gap:14px; flex-wrap:wrap; background:var(--bg-soft,#f6f8fa) }
|
||
.pl-hero { min-height:110px; max-height:42vh; flex:none; background:#0b2a30; display:flex; align-items:center; justify-content:center; overflow:hidden }
|
||
.pl-hero img { display:block; width:100%; height:auto; max-height:42vh; object-fit:contain }
|
||
.pl-hero span { font-size:3.2rem; font-weight:700; color:#fff }
|
||
#pl-modal .pl-modal-foot form { margin:0; display:flex; align-items:center; gap:10px; flex-wrap:wrap }
|
||
#pl-modal .pl-modal-foot .pl-confirm { margin:0 }
|
||
#pl-modal .pl-modal-foot .btn.danger { padding:7px 12px; font-size:.85rem }
|
||
.pl-modal-close { position:absolute; top:10px; right:10px; z-index:2; width:34px; height:34px; border-radius:50%; border:none; background:rgba(0,0,0,.55); color:#fff; font-size:1.2rem; cursor:pointer; line-height:1 }
|
||
@media (max-width:640px) { #pl-modal .pl-modal-body { grid-template-columns:1fr } #pl-modal .pl-modal-side { border-left:none; border-top:1px solid var(--border-soft) } }
|
||
.pl-card .pl-foot { margin-top:auto; display:flex; justify-content:space-between; align-items:flex-end; gap:12px; flex-wrap:wrap; padding:14px 18px; background:var(--bg-soft,#f6f8fa); border-top:1px solid var(--border-soft); font-size:.8rem }
|
||
@media (max-width:560px) { .pl-grid { grid-template-columns:1fr } .pl-card-top { grid-template-columns:72px minmax(0,1fr) } .pl-card-icon img, .pl-card-icon div { width:72px !important; height:72px !important } .pl-card-act { grid-column:1 / -1; flex-direction:row; align-items:center } }
|
||
.pl-confirm { display:block; font-size:.76rem; margin-bottom:4px }
|
||
[data-pl-panel][hidden] { display:none }
|
||
@media (max-width:760px) { .pl-search { max-width:none; width:100% } }
|
||
</style>
|
||
|
||
<div class="pl-head">
|
||
<h1>Plugins</h1>
|
||
<button type="button" class="btn secondary" data-pl-tab="add">Neu hinzufügen</button>
|
||
</div>
|
||
<?php foreach ($errors as $err): ?><div class="flash error"><?= e($err) ?></div><?php endforeach; ?>
|
||
<?php if ($updates): ?>
|
||
<div class="flash warning"><strong>Updates verfügbar:</strong> <?= e(implode(', ', array_map(fn($u) => $u['name'] . ' ' . $u['installed'] . ' → ' . $u['available'], $updates))) ?>. <a href="#" data-pl-tab="installed" data-pl-filter="update">Anzeigen</a>
|
||
<form method="post" style="display:inline;margin:0 0 0 10px"><?= csrf_field() ?><input type="hidden" name="action" value="catalog_update_all"><button class="btn" type="submit" style="padding:3px 12px;font-size:.82rem" onclick="this.textContent='⏳ Wird aktualisiert …'"><?= count($updates) === 1 ? 'Jetzt aktualisieren' : 'Alle aktualisieren' ?></button></form></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="pl-tabs" role="tablist">
|
||
<button type="button" role="tab" data-pl-tab="installed" aria-selected="<?= $tab === 'installed' ? 'true' : 'false' ?>">Installiert (<?= $counts['all'] ?>)</button>
|
||
<button type="button" role="tab" data-pl-tab="add" aria-selected="<?= $tab === 'add' ? 'true' : 'false' ?>">Plugin hinzufügen</button>
|
||
<?php if (!$strict): ?><button type="button" role="tab" data-pl-tab="settings" aria-selected="<?= $tab === 'settings' ? 'true' : 'false' ?>">Katalog & Schlüssel</button><?php endif; ?>
|
||
</div>
|
||
|
||
<!-- Installiert -->
|
||
<section data-pl-panel="installed" <?= $tab === 'installed' ? '' : 'hidden' ?>>
|
||
<div class="pl-bar">
|
||
<div class="pl-filters" role="group" aria-label="Filter">
|
||
<button type="button" data-pl-filter="all" aria-pressed="true">Alle <span class="n">(<?= $counts['all'] ?>)</span></button>
|
||
<button type="button" data-pl-filter="active" aria-pressed="false">Aktiv <span class="n">(<?= $counts['active'] ?>)</span></button>
|
||
<button type="button" data-pl-filter="inactive" aria-pressed="false">Inaktiv <span class="n">(<?= $counts['inactive'] ?>)</span></button>
|
||
<?php if ($counts['error']): ?><button type="button" data-pl-filter="error" aria-pressed="false">Mit Fehler <span class="n">(<?= $counts['error'] ?>)</span></button><?php endif; ?>
|
||
<?php if ($counts['update']): ?><button type="button" data-pl-filter="update" aria-pressed="false">Update verfügbar <span class="n">(<?= $counts['update'] ?>)</span></button><?php endif; ?>
|
||
</div>
|
||
<input type="search" class="pl-search" id="pl-search" placeholder="Installierte Plugins durchsuchen …" aria-label="Installierte Plugins durchsuchen">
|
||
</div>
|
||
|
||
<div class="pl-grid" id="pl-installed">
|
||
<?php foreach ($installed as $slug => $p): $m = $manifests[$slug]; $perms = $m['permissions'] ?? []; $st = $stateOf[$slug]; $upd = $updBySlug[$slug] ?? null;
|
||
$trusted = ($p['source'] ?? '') === 'catalog' && !empty($p['signature_ok']); ?>
|
||
<div class="pl-card st-<?= e($st) ?>" data-state="<?= e($st) ?>" data-update="<?= $upd ? '1' : '0' ?>" data-text="<?= e(mb_strtolower($p['name'] . ' ' . $slug . ' ' . ($m['description'] ?? '') . ' ' . ($m['author'] ?? ''))) ?>">
|
||
<div class="pl-card-top">
|
||
<div class="pl-card-icon"><?= catalog_thumb('plugin', $catBySlug[$slug] ?? ['slug' => $slug, 'name' => $p['name']]) ?></div>
|
||
<div class="pl-card-main">
|
||
<h4><?= e($p['name']) ?></h4>
|
||
<p><?= $m && $m['description'] !== '' ? e($m['description']) : (!$m ? '<span style="color:#dc2626">plugin.json fehlt oder ist ungültig – das Plugin kann nicht geladen werden.</span>' : '') ?></p>
|
||
<p class="by">Von <span><?= $m && $m['author'] !== '' ? e($m['author']) : 'unbekannt' ?></span></p>
|
||
<div>
|
||
<?php if ($p['is_active']): ?><span class="pl-tag ok">Aktiv</span>
|
||
<?php elseif ($st === 'error'): ?><span class="pl-tag bad"><?= $m ? 'Wegen Fehlern deaktiviert' : 'Ungültig' ?></span>
|
||
<?php else: ?><span class="pl-tag">Inaktiv</span><?php endif; ?>
|
||
<?php if ($upd): ?><span class="pl-tag warn">Update <span><?= e($upd['available']) ?></span></span><?php endif; ?>
|
||
</div>
|
||
</div>
|
||
<div class="pl-card-act">
|
||
<?php if ($p['is_active']): ?>
|
||
<form method="post" style="margin:0"><?= 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 if (isset($openUrl[$slug])): ?><a class="pl-more" href="<?= e($openUrl[$slug]) ?>">Öffnen</a><?php endif; ?>
|
||
<?php else: ?>
|
||
<form method="post" style="margin:0;text-align:right" 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 && !$trusted): ?><label class="pl-confirm"><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; ?>
|
||
<?php if ($upd): ?><form method="post" style="margin:0;text-align:right"><?= csrf_field() ?><input type="hidden" name="action" value="catalog_install"><input type="hidden" name="slug" value="<?= e($slug) ?>"><button class="pl-more" type="submit" style="background:none;border:0;padding:0;cursor:pointer;font:inherit;text-decoration:underline" onclick="this.textContent='⏳ Wird aktualisiert …'">Jetzt aktualisieren</button></form><?php endif; ?>
|
||
<a href="#" class="pl-more">Weitere Details</a>
|
||
</div>
|
||
</div>
|
||
<div class="pl-card-detail" hidden>
|
||
<?= $banner($catBySlug[$slug] ?? ['slug' => $slug], $p['name']) ?>
|
||
<div class="pl-modal-body">
|
||
<div class="pl-modal-text">
|
||
<h3><?= e($p['name']) ?></h3>
|
||
<p><?= $m && $m['description'] !== '' ? nl2br(e($m['description'])) : '' ?></p>
|
||
<?php if (!$m): ?><p style="color:#dc2626">plugin.json fehlt oder ist ungültig – das Plugin kann nicht geladen werden.</p><?php endif; ?>
|
||
<?php if ($perms): ?>
|
||
<h4>Berechtigungen</h4>
|
||
<ul><?php foreach ($perms as $perm): ?><li><?= e($permLabels[$perm] ?? $perm) ?> <span style="opacity:.7">(<?= e($perm) ?>)</span></li><?php endforeach; ?></ul>
|
||
<?php elseif ($m): ?><p>Dieses Plugin fordert keine Berechtigungen an.</p><?php endif; ?>
|
||
<?php if (!empty($p['last_error'])): ?><h4>Letzte Fehlermeldung</h4><div class="pl-err"><?= e(mb_strimwidth((string) $p['last_error'], 0, 300, '…')) ?></div><?php endif; ?>
|
||
</div>
|
||
<aside class="pl-modal-side">
|
||
<p><strong>Version:</strong> <?= e($p['version']) ?></p>
|
||
<p><strong>Autor:</strong> <?= $m && $m['author'] !== '' ? e($m['author']) : 'unbekannt' ?></p>
|
||
<p><strong>Kennung:</strong> <code><?= e($slug) ?></code></p>
|
||
<p><strong>Zustand:</strong> <?= $p['is_active'] ? 'Aktiv' : ($st === 'error' ? 'Fehler' : 'Inaktiv') ?></p>
|
||
<p><strong>Herkunft:</strong> <?= ($p['source'] ?? '') === 'catalog' ? 'Aus dem Katalog' : (!empty($p['signature_ok']) ? 'Manuell hochgeladen' : 'Nicht vom Herausgeber geprüft') ?></p>
|
||
<p><strong>Signatur:</strong> <?= !empty($p['signature_ok']) ? '✓ vom Herausgeber signiert' : 'nicht signiert (eigenes Plugin)' ?></p>
|
||
<?php if ($upd): ?><p><strong>Update:</strong> <span><?= e($upd['available']) ?></span> verfügbar</p><?php endif; ?>
|
||
</aside>
|
||
</div>
|
||
<div class="pl-modal-foot">
|
||
<?php if ($p['is_active']): ?>
|
||
<form method="post"><?= 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" 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 && !$trusted): ?><label class="pl-confirm"><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" onsubmit="return confirm('Plugin wirklich entfernen?')">
|
||
<?= csrf_field() ?><input type="hidden" name="slug" value="<?= e($slug) ?>"><input type="hidden" name="action" value="uninstall">
|
||
<label class="pl-confirm"><input type="checkbox" name="remove_data" value="1"> Auch die Daten des Plugins löschen</label>
|
||
<button type="submit" class="btn danger">Plugin entfernen</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
<div class="pl-foot">
|
||
<div>
|
||
<div>Version <strong><?= e($p['version']) ?></strong></div>
|
||
<div class="muted"><?= $p['is_active'] ? 'Aktiv' : ($st === 'error' ? 'Fehler' : 'Inaktiv') ?></div>
|
||
</div>
|
||
<div style="text-align:right">
|
||
<div><?= !empty($p['signature_ok']) ? '<span style="color:#15803d;font-weight:600">✓ Signiert</span> vom Herausgeber' : '<span style="color:#b45309;font-weight:600">Eigenes Plugin</span>' ?></div>
|
||
<div class="muted"><?= $perms ? count($perms) . ' Berechtigung' . (count($perms) === 1 ? '' : 'en') : 'Keine Berechtigungen' ?></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
<div class="pl-empty" id="pl-none" <?= $installed ? 'hidden' : '' ?>><?= $installed ? 'Kein Plugin passt zu Filter oder Suche.' : 'Noch keine Plugins installiert. Unter „Plugin hinzufügen“ findest du den Katalog und den ZIP-Upload.' ?></div>
|
||
</section>
|
||
|
||
<!-- Plugin hinzufügen -->
|
||
<section data-pl-panel="add" <?= $tab === 'add' ? '' : 'hidden' ?>>
|
||
<div class="card">
|
||
<div class="pl-bar" style="margin-bottom:0">
|
||
<h3 style="margin:0"><?= $official ? 'Geprüfte Plugins' : 'Plugin-Katalog' ?></h3>
|
||
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
|
||
<?php if ($catalog && !empty($catalog['ok'])): ?><input type="search" class="pl-search" id="pl-search-cat" placeholder="Katalog durchsuchen …" aria-label="Katalog durchsuchen"><?php endif; ?>
|
||
<?php if ($catalog): ?><a class="btn secondary" href="/staff/plugins.php?tab=add&refresh=1">Katalog aktualisieren</a><?php endif; ?>
|
||
</div>
|
||
</div>
|
||
<?php if (!$catalog): ?>
|
||
<p class="muted" style="font-size:.85rem">Es ist noch keine Katalog-Adresse hinterlegt. Trage unter „Katalog & Schlüssel“ 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:6px 0 12px">Stand: <span><?= e(date('d.m.Y H:i', (int) $catalog['cached_at'])) ?></span></div><?php endif; ?>
|
||
<div class="pl-grid" id="pl-catalog">
|
||
<?php foreach ($catalog['plugins'] as $c): $have = $installed[$c['slug']] ?? null; $newer = $have && version_compare($c['version'], $have['version'], '>'); ?>
|
||
<div class="pl-card" data-text="<?= e(mb_strtolower($c['name'] . ' ' . $c['slug'] . ' ' . $c['description'] . ' ' . $c['author'])) ?>">
|
||
<div class="pl-card-top">
|
||
<div class="pl-card-icon"><?= catalog_thumb('plugin', $c) ?></div>
|
||
<div class="pl-card-main">
|
||
<h4><?= e($c['name']) ?></h4>
|
||
<p><?= e($c['description']) ?></p>
|
||
<p class="by">Von <span><?= $c['author'] !== '' ? e($c['author']) : 'unbekannt' ?></span></p>
|
||
<div>
|
||
<?php if (!empty($c['language'])): ?><span class="pl-tag info" title="Sprachpaket: nach der Aktivierung erscheint die Flagge zum Umschalten">🌐 Sprache: <?= e($c['language']['name']) ?></span><?php endif; ?>
|
||
<?php if (!empty($c['featured'])): ?><span class="pl-tag warn" title="Vom Herausgeber empfohlen">★ Empfohlen</span><?php endif; ?>
|
||
</div>
|
||
</div>
|
||
<div class="pl-card-act">
|
||
<?php if ($have && !$newer): ?><span class="pl-installed-badge">✓ Installiert</span>
|
||
<?php elseif ($strict && $c['signature'] === ''): ?><span class="muted" style="font-size:.78rem;text-align:right">nicht signiert,<br>nicht installierbar</span>
|
||
<?php else: ?>
|
||
<form method="post" style="margin:0"><?= csrf_field() ?><input type="hidden" name="action" value="catalog_install"><input type="hidden" name="slug" value="<?= e($c['slug']) ?>">
|
||
<button class="btn pl-install" type="submit"><?= $newer ? 'Jetzt aktualisieren' : 'Jetzt installieren' ?></button></form>
|
||
<?php endif; ?>
|
||
<a href="#" class="pl-more">Weitere Details</a>
|
||
</div>
|
||
</div>
|
||
<div class="pl-card-detail" hidden>
|
||
<?= $banner($c, $c['name']) ?>
|
||
<div class="pl-modal-body">
|
||
<div class="pl-modal-text">
|
||
<h3><?= e($c['name']) ?></h3>
|
||
<p><?= nl2br(e($c['description'])) ?></p>
|
||
<?php if ($c['permissions']): ?>
|
||
<h4>Berechtigungen</h4>
|
||
<ul><?php foreach ($c['permissions'] as $perm): ?><li><?= e($permLabels[$perm] ?? $perm) ?> <span style="opacity:.7">(<?= e($perm) ?>)</span></li><?php endforeach; ?></ul>
|
||
<?php else: ?><p>Dieses Plugin fordert keine Berechtigungen an.</p><?php endif; ?>
|
||
</div>
|
||
<aside class="pl-modal-side">
|
||
<p><strong>Version:</strong> <?= e($c['version']) ?></p>
|
||
<p><strong>Autor:</strong> <?= $c['author'] !== '' ? e($c['author']) : 'unbekannt' ?></p>
|
||
<p><strong>Kennung:</strong> <code><?= e($c['slug']) ?></code></p>
|
||
<p><strong>Installiert:</strong> <?= $have ? e($have['version']) : 'nein' ?></p>
|
||
<p><strong>Signatur:</strong> <?= $c['signature'] !== '' ? ($hasKey ? '✓ vom Herausgeber signiert' : 'vorhanden, Schlüssel fehlt') : 'nicht signiert' ?></p>
|
||
</aside>
|
||
</div>
|
||
<div class="pl-modal-foot">
|
||
<?php if ($have && !$newer): ?><span class="pl-installed-badge">✓ Installiert</span>
|
||
<?php elseif ($strict && $c['signature'] === ''): ?><span class="muted">nicht signiert, 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 ? 'Jetzt aktualisieren' : 'Jetzt installieren' ?></button></form>
|
||
<?php endif; ?>
|
||
</div>
|
||
</div>
|
||
<div class="pl-foot">
|
||
<div>
|
||
<div>Version <strong><?= e($c['version']) ?></strong></div>
|
||
<div class="muted"><?= $have ? 'Installiert: <span>' . e($have['version']) . '</span>' : 'Nicht installiert' ?></div>
|
||
</div>
|
||
<div style="text-align:right">
|
||
<div><?= $c['signature'] !== '' ? ($hasKey ? '<span style="color:#15803d;font-weight:600">✓ Signiert</span> vom Herausgeber' : '<span style="color:#b45309;font-weight:600">Signatur vorhanden, Schlüssel fehlt</span>') : '<span style="color:#b45309;font-weight:600">Nicht signiert</span>' ?></div>
|
||
<div class="muted"><?= $c['permissions'] ? count($c['permissions']) . ' Berechtigung' . (count($c['permissions']) === 1 ? '' : 'en') : 'Keine Berechtigungen' ?></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
<?php if (!$catalog['plugins']): ?><div class="pl-empty">Der Katalog enthält keine Plugins.</div><?php else: ?><div class="pl-empty" id="pl-cat-none" hidden>Kein Plugin im Katalog passt zur Suche.</div><?php endif; ?>
|
||
<?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>
|
||
<details style="margin-top:8px"><summary class="muted" style="cursor:pointer;font-size:.85rem">Signatur und Prüfsumme (optional)</summary>
|
||
<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">
|
||
</details>
|
||
<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>
|
||
</section>
|
||
|
||
<!-- Katalog und Schlüssel -->
|
||
<?php if (!$strict): ?>
|
||
<section data-pl-panel="settings" <?= $tab === 'settings' ? '' : 'hidden' ?>>
|
||
<div class="card">
|
||
<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>
|
||
</section>
|
||
<?php endif; ?>
|
||
|
||
<dialog id="pl-modal" aria-label="Plugin-Details"><button type="button" class="pl-modal-close" aria-label="Schließen">✕</button><div class="pl-modal-inner" id="pl-modal-content"></div></dialog>
|
||
<script>
|
||
(function () {
|
||
var panels = document.querySelectorAll('[data-pl-panel]');
|
||
var filter = 'all', q = '';
|
||
function show(name) {
|
||
panels.forEach(function (p) { p.hidden = p.getAttribute('data-pl-panel') !== name; });
|
||
document.querySelectorAll('.pl-tabs [data-pl-tab]').forEach(function (b) { b.setAttribute('aria-selected', b.getAttribute('data-pl-tab') === name ? 'true' : 'false'); });
|
||
try { history.replaceState(null, '', location.pathname + (name === 'installed' ? '' : '?tab=' + name)); } catch (e) {}
|
||
}
|
||
function apply() {
|
||
var rows = document.querySelectorAll('#pl-installed .pl-card'), shown = 0;
|
||
rows.forEach(function (r) {
|
||
var ok = (filter === 'all' || (filter === 'update' ? r.dataset.update === '1' : r.dataset.state === filter)) && (q === '' || r.dataset.text.indexOf(q) !== -1);
|
||
r.hidden = !ok; if (ok) shown++;
|
||
});
|
||
var none = document.getElementById('pl-none');
|
||
if (none && rows.length) none.hidden = shown > 0;
|
||
document.querySelectorAll('.pl-filters [data-pl-filter]').forEach(function (b) { b.setAttribute('aria-pressed', b.getAttribute('data-pl-filter') === filter ? 'true' : 'false'); });
|
||
}
|
||
document.querySelectorAll('[data-pl-tab]').forEach(function (b) {
|
||
b.addEventListener('click', function (ev) {
|
||
ev.preventDefault();
|
||
show(b.getAttribute('data-pl-tab'));
|
||
if (b.hasAttribute('data-pl-filter')) { filter = b.getAttribute('data-pl-filter'); apply(); }
|
||
});
|
||
});
|
||
document.querySelectorAll('.pl-filters [data-pl-filter]').forEach(function (b) {
|
||
b.addEventListener('click', function () { filter = b.getAttribute('data-pl-filter'); apply(); });
|
||
});
|
||
var s = document.getElementById('pl-search');
|
||
if (s) s.addEventListener('input', function () { q = s.value.trim().toLowerCase(); apply(); });
|
||
var modal = document.getElementById('pl-modal'), modalBody = document.getElementById('pl-modal-content');
|
||
document.querySelectorAll('.pl-more[href="#"]:not([data-pl-tab])').forEach(function (a) {
|
||
a.addEventListener('click', function (ev) {
|
||
ev.preventDefault();
|
||
var d = a.closest('.pl-card').querySelector('.pl-card-detail');
|
||
if (!d || !modal || !modal.showModal) return;
|
||
modalBody.innerHTML = d.outerHTML;
|
||
var inner = modalBody.firstElementChild; inner.removeAttribute('hidden');
|
||
modal.showModal();
|
||
});
|
||
});
|
||
if (modal) {
|
||
modal.querySelector('.pl-modal-close').addEventListener('click', function () { modal.close(); });
|
||
modal.addEventListener('click', function (ev) { if (ev.target === modal) modal.close(); });
|
||
}
|
||
var sc = document.getElementById('pl-search-cat');
|
||
if (sc) sc.addEventListener('input', function () {
|
||
var v = sc.value.trim().toLowerCase(), n = 0;
|
||
document.querySelectorAll('#pl-catalog .pl-card').forEach(function (c) { var ok = v === '' || c.dataset.text.indexOf(v) !== -1; c.hidden = !ok; if (ok) n++; });
|
||
var none = document.getElementById('pl-cat-none'); if (none) none.hidden = n > 0;
|
||
});
|
||
apply();
|
||
})();
|
||
</script>
|
||
|
||
<?php require __DIR__ . '/_footer.php'; ?>
|