diff --git a/plugin-webseite/README.md b/plugin-webseite/README.md index 10aa3cba..ea886682 100644 --- a/plugin-webseite/README.md +++ b/plugin-webseite/README.md @@ -37,6 +37,8 @@ Setzt den Katalog dieser Webseite als **offiziellen Katalog** eurer DeskHub-Vers **Plugin:** ZIP mit `plugin.json` (`slug`, `name`, `version`, optional `description`, `author`, `requires`, `permissions`), siehe die DeskHub-Anleitung. Alle Angaben liest die Webseite aus der `plugin.json`. +**Sprachpaket (Mehrsprachigkeit):** ein normales Plugin ohne Berechtigungen, dessen `plugin.json` zusätzlich `"language": {"code": "en", "name": "English", "flag": "flag.svg", "strings": "strings.json"}` enthält. Die Webseite prüft den Sprachcode (`en`, `pt-br` …), merkt sich Code und Name und nennt sie im Katalog (`language`); DeskHub zeigt dann „🌐 Sprache: English“ und blendet nach der Aktivierung die Flagge zum Umschalten ein. Flagge und Texte liegen im Paket. Fertige Pakete: `Plugins/plugins/lang-en-1.0.0.zip` und `lang-de-1.0.0.zip`, passende Vorschaubilder in `theme/vorschaubilder/plugin-lang-en.png` und `plugin-lang-de.png`. Nach einem Update dieser Webseite legt sie die neue Spalte beim ersten Aufruf selbst an. + **Theme:** ZIP mit `theme.css` im Stammverzeichnis (nur CSS, Bilder und Schriften, kein PHP). Kennung, Name und Beschreibung tragt ihr beim Hochladen ein. Autor und Version stehen am besten im CSS-Kopf: ```css diff --git a/plugin-webseite/admin.php b/plugin-webseite/admin.php index 6d5a6d2e..2f6bff3f 100644 --- a/plugin-webseite/admin.php +++ b/plugin-webseite/admin.php @@ -82,6 +82,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { } else { $error = 'Ungültige Installation.'; } + } elseif ($action === 'feature' || $action === 'unfeature') { + $message = site_set_featured((int) ($_POST['id'] ?? 0), $action === 'feature') ? ($action === 'feature' ? 'Als „Empfohlen“ markiert. DeskHub zeigt es nach der nächsten Katalog-Aktualisierung (spätestens in wenigen Minuten).' : 'Empfehlung entfernt.') : null; + $error = $message ? null : 'Eintrag nicht gefunden.'; } elseif ($action === 'delete') { $message = site_delete_version((int) ($_POST['id'] ?? 0)) ? 'Version entfernt.' : null; $error = $message ? null : 'Version nicht gefunden.'; @@ -148,7 +151,8 @@ site_header('Verwaltung');
Noch keine.
| = site_h($v['version']) ?> | = site_h(substr($v['created_at'], 0, 16)) ?> | = (int) $v['downloads'] ?> Downloads | = $v['signature'] !== '' ? 'signiert' : 'nicht signiert' ?> | diff --git a/plugin-webseite/inc/lib.php b/plugin-webseite/inc/lib.php index a11b2bf6..8c5f1862 100644 --- a/plugin-webseite/inc/lib.php +++ b/plugin-webseite/inc/lib.php @@ -20,6 +20,7 @@ function site_known_permissions(): array 'ui.menu' => 'Eigene Menüpunkte und Seiten im Mitarbeiterbereich', 'ui.sidebar' => 'Bausteine in der Seitenleiste der Ticketansicht', 'api.endpoints' => 'Eigene Endpunkte in der Mitarbeiter-API', + 'api.public' => 'Öffentliche Webhook-Adresse (zum Beispiel für Discord oder Telegram); das Plugin prüft die Echtheit der Anfragen selbst', 'db.migrations' => 'Eigene Datenbank-Tabellen anlegen', ]; } @@ -94,9 +95,21 @@ function site_db(): PDO signature TEXT NOT NULL DEFAULT '', size INTEGER NOT NULL, requires TEXT NOT NULL DEFAULT '', permissions TEXT NOT NULL DEFAULT '', downloads INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL, UNIQUE (item_id, version), FOREIGN KEY (item_id) REFERENCES items(id) ON DELETE CASCADE)"); + // Empfehlung (nachträglich ergänzt, damit bestehende Datenbanken ohne Handarbeit weiterlaufen) + $cols = array_column($pdo->query('PRAGMA table_info(items)')->fetchAll(), 'name'); + if (!in_array('featured', $cols, true)) $pdo->exec('ALTER TABLE items ADD COLUMN featured INTEGER NOT NULL DEFAULT 0'); + if (!in_array('language', $cols, true)) $pdo->exec('ALTER TABLE items ADD COLUMN language TEXT NULL'); return $pdo; } +/** Plugin bzw. Theme als „Empfohlen“ markieren oder die Markierung entfernen. */ +function site_set_featured(int $itemId, bool $featured): bool +{ + $stmt = site_db()->prepare('UPDATE items SET featured = ? WHERE id = ?'); + $stmt->execute([$featured ? 1 : 0, $itemId]); + return $stmt->rowCount() > 0; +} + function site_h(?string $s): string { return htmlspecialchars((string) $s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); @@ -181,10 +194,18 @@ function site_inspect_package(string $type, string $zipPath): array if ($name === '') return ['ok' => false, 'error' => 'plugin.json: "name" fehlt.']; if (!site_valid_version($version)) return ['ok' => false, 'error' => 'plugin.json: ungültige Versionsnummer.']; $perms = array_values(array_unique(array_map('strval', (array) ($data['permissions'] ?? [])))); - foreach ($perms as $p) if (!isset(site_known_permissions()[$p])) return ['ok' => false, 'error' => "plugin.json: unbekannte Berechtigung \"{$p}\"."]; + // Neue Berechtigungen, die DeskHub später einführt, dürfen die Webseite nicht blockieren: Form "bereich.name" genügt, den Inhalt prüft DeskHub beim Installieren + foreach ($perms as $p) if (!isset(site_known_permissions()[$p]) && !preg_match('/^[a-z]{2,12}\.[a-z_]{2,30}$/', $p)) return ['ok' => false, 'error' => "plugin.json: unbekannte Berechtigung \"{$p}\"."]; + // Sprachpaket: "language": {"code":"en","name":"English"} (die Flagge und die Texte liegen im Paket, DeskHub liest sie dort) + $lang = null; + if (is_array($data['language'] ?? null)) { + $code = strtolower((string) ($data['language']['code'] ?? '')); + if (!preg_match('/^[a-z]{2,3}(-[a-z]{2,4})?$/', $code)) return ['ok' => false, 'error' => 'plugin.json: language.code fehlt oder ist ungültig (zum Beispiel en oder pt-br).']; + $lang = ['code' => $code, 'name' => mb_substr(trim((string) ($data['language']['name'] ?? $code)), 0, 40) ?: $code]; + } sort($perms); return ['ok' => true, 'meta' => ['slug' => $slug, 'name' => mb_substr($name, 0, 120), 'version' => $version, 'description' => mb_substr((string) ($data['description'] ?? ''), 0, 1000), - 'author' => mb_substr((string) ($data['author'] ?? ''), 0, 120), 'requires' => (string) ($data['requires'] ?? ''), 'permissions' => $perms]]; + 'author' => mb_substr((string) ($data['author'] ?? ''), 0, 120), 'requires' => (string) ($data['requires'] ?? ''), 'permissions' => $perms, 'language' => $lang]]; } // Theme: Angaben stehen im Kopf der theme.css (Author:, Version:) $head = substr($main, 0, 2000); @@ -278,12 +299,12 @@ function site_publish(string $type, string $zipPath, array $form = [], ?string $ $pdo->beginTransaction(); try { if ($item) { - $pdo->prepare('UPDATE items SET name = ?, description = ?, author = ?, preview = COALESCE(?, preview), updated_at = ? WHERE id = ?') - ->execute([$meta['name'], $meta['description'], $meta['author'], $preview, $now, $item['id']]); + $pdo->prepare('UPDATE items SET name = ?, description = ?, author = ?, preview = COALESCE(?, preview), language = ?, updated_at = ? WHERE id = ?') + ->execute([$meta['name'], $meta['description'], $meta['author'], $preview, !empty($meta['language']) ? json_encode($meta['language'], JSON_UNESCAPED_UNICODE) : null, $now, $item['id']]); $itemId = (int) $item['id']; } else { - $pdo->prepare('INSERT INTO items (type, slug, name, description, author, preview, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)') - ->execute([$type, $meta['slug'], $meta['name'], $meta['description'], $meta['author'], $preview, $now, $now]); + $pdo->prepare('INSERT INTO items (type, slug, name, description, author, preview, language, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)') + ->execute([$type, $meta['slug'], $meta['name'], $meta['description'], $meta['author'], $preview, !empty($meta['language']) ? json_encode($meta['language'], JSON_UNESCAPED_UNICODE) : null, $now, $now]); $itemId = (int) $pdo->lastInsertId(); } $pdo->prepare('INSERT INTO versions (item_id, version, file, sha256, signature, size, requires, permissions, created_at) VALUES (?,?,?,?,?,?,?,?,?)') @@ -334,7 +355,7 @@ function site_delete_version(int $versionId): bool function site_items(string $type, string $search = ''): array { $pdo = site_db(); - $stmt = $pdo->prepare('SELECT * FROM items WHERE type = ? ORDER BY name COLLATE NOCASE'); + $stmt = $pdo->prepare('SELECT * FROM items WHERE type = ? ORDER BY featured DESC, name COLLATE NOCASE'); $stmt->execute([$type]); $out = []; foreach ($stmt->fetchAll() as $item) { @@ -380,6 +401,8 @@ function site_catalog(string $type): array 'downloadUrl' => $base . '/download.php?type=' . $type . '&slug=' . rawurlencode($item['slug']) . '&version=' . rawurlencode($v['version']), 'sha256' => $v['sha256'], 'signature' => $v['signature'], ]; + if (!empty($item['featured'])) $entry['featured'] = true; + if ($type === 'plugin' && !empty($item['language']) && is_array($l = json_decode((string) $item['language'], true))) $entry['language'] = ['code' => (string) $l['code'], 'name' => (string) $l['name']]; if ($item['preview']) $entry['previewUrl'] = $base . '/preview.php?file=' . rawurlencode($item['preview']); if ($type === 'plugin') { $entry['requires'] = $v['requires'];