Upload via GUI (267 Dateien)

This commit is contained in:
2026-10-19 18:05:57 +00:00
parent f3e53e6182
commit c800be9693
12 changed files with 2694 additions and 2082 deletions
+47 -5
View File
@@ -127,6 +127,7 @@ function plugin_filter_permission(string $hook): ?string
{
if ($hook === 'ticket.view.sidebar') return 'ui.sidebar';
if (str_starts_with($hook, 'ticket.')) return 'hooks.tickets';
if ($hook === 'backup.list.rows') return 'ui.menu'; // wer einen Reiter unter „Backup“ anbieten darf, darf auch die Liste der Backups ergänzen
return null;
}
@@ -146,14 +147,18 @@ function plugin_add_filter(string $hook, callable $fn, int $priority = 10): void
$state['filters'][$hook][$priority][] = ['fn' => $fn, 'plugin' => $state['current']];
}
/** Menüpunkt im Mitarbeiterbereich. $page ist der Dateiname unter plugins/<slug>/pages/ (ohne .php). */
function plugin_register_menu(string $label, string $page, ?string $permission = null): void
/**
* Menüpunkt im Mitarbeiterbereich. $page ist der Dateiname unter plugins/<slug>/pages/ (ohne .php).
* Mit $belongsTo (zum Beispiel 'backup', siehe plugin_parent_pages()) erscheint kein eigener Eintrag in der Seitenleiste: Die Seite wird
* stattdessen als Reiter innerhalb dieser Seite angeboten (zum Beispiel unter „Backup“). Dafür ist keine zusätzliche Berechtigung nötig (ui.menu genügt).
*/
function plugin_register_menu(string $label, string $page, ?string $permission = null, ?string $belongsTo = null): void
{
if (!plugin_permission_allowed('ui.menu')) return;
$state = &plugin_state();
$slug = $state['current'];
if ($slug === null || !preg_match('/^[a-z0-9_-]+$/i', $page)) return;
$state['menus'][] = ['plugin' => $slug, 'label' => mb_substr($label, 0, 60), 'page' => $page, 'permission' => $permission];
$state['menus'][] = ['plugin' => $slug, 'label' => mb_substr($label, 0, 60), 'page' => $page, 'permission' => $permission, 'belongs_to' => ($belongsTo !== null && isset(plugin_parent_pages()[$belongsTo])) ? $belongsTo : null];
}
/**
@@ -306,11 +311,45 @@ function plugin_menu_items(?array $staff): array
$items = [];
foreach (plugin_state()['menus'] as $m) {
if ($m['permission'] !== null && (!$staff || !has_permission($staff, $m['permission']))) continue;
if (!empty($m['belongs_to'])) continue; // erscheint als Reiter in der übergeordneten Seite, nicht in der Seitenleiste
$items[] = $m + ['url' => '/staff/plugin.php?p=' . rawurlencode($m['plugin']) . '&page=' . rawurlencode($m['page'])];
}
return $items;
}
/** Seiten, unter denen Plugins Reiter anbieten dürfen: Schlüssel => [Bezeichnung des ersten Reiters, Adresse]. */
function plugin_parent_pages(): array
{
return ['backup' => ['Backups', '/staff/backup.php']];
}
/** Plugin-Seiten (mit Rechteprüfung), die als Reiter zu einer übergeordneten Seite gehören. */
function plugin_subpages(string $parent, ?array $staff): array
{
$items = [];
foreach (plugin_state()['menus'] as $m) {
if (($m['belongs_to'] ?? null) !== $parent) continue;
if ($m['permission'] !== null && (!$staff || !has_permission($staff, $m['permission']))) continue;
$items[] = $m + ['url' => '/staff/plugin.php?p=' . rawurlencode($m['plugin']) . '&page=' . rawurlencode($m['page'])];
}
return $items;
}
/** Reiterleiste einer übergeordneten Seite (leer, wenn kein Plugin Reiter anbietet). $active: 'parent' oder '<plugin>:<seite>'. */
function plugin_subpage_tabs(string $parent, ?array $staff, string $active = 'parent'): string
{
$parents = plugin_parent_pages();
$subs = plugin_subpages($parent, $staff);
if (!$subs || !isset($parents[$parent])) return '';
$tab = function (string $label, string $url, bool $on): string {
return '<a href="' . htmlspecialchars($url, ENT_QUOTES) . '" style="padding:8px 16px;border-radius:8px 8px 0 0;text-decoration:none;font-size:.9rem;'
. ($on ? 'background:var(--card-bg,#fff);border:1px solid var(--border);border-bottom-color:var(--card-bg,#fff);font-weight:600;color:inherit;position:relative;top:1px' : 'color:var(--muted);border:1px solid transparent') . '">' . htmlspecialchars($label) . '</a>';
};
$html = '<div class="plugin-tabs" style="display:flex;gap:4px;flex-wrap:wrap;border-bottom:1px solid var(--border);margin:0 0 18px">' . $tab($parents[$parent][0], $parents[$parent][1], $active === 'parent');
foreach ($subs as $s) $html .= $tab($s['label'], $s['url'], $active === $s['plugin'] . ':' . $s['page']);
return $html . '</div>';
}
// ─── Verwaltung (Installieren, Aktivieren, Entfernen) ────────────────────────
function plugins_installed(PDO $pdo): array
@@ -785,10 +824,13 @@ function plugin_run_migrations(PDO $pdo, string $slug): array
if (in_array($key, $done, true)) continue;
$sql = preg_replace('/^\s*--.*$/m', '', (string) file_get_contents($file));
try {
$pdo->beginTransaction();
// SQLite kann Tabellen-Änderungen in einer Transaktion zurückrollen. MySQL beendet bei CREATE/ALTER TABLE jede offene
// Transaktion von selbst ("There is no active transaction"), dort läuft die Datei deshalb ohne Transaktion.
$useTransaction = $driver === 'sqlite';
if ($useTransaction) $pdo->beginTransaction();
foreach (array_filter(array_map('trim', explode(';', $sql))) as $stmt) $pdo->exec($stmt);
$pdo->prepare('INSERT INTO schema_migrations (migration) VALUES (?)')->execute([$key]);
$pdo->commit();
if ($useTransaction && $pdo->inTransaction()) $pdo->commit();
$applied++;
} catch (Throwable $e) {
if ($pdo->inTransaction()) $pdo->rollBack();