From f189918dc42bf28c99d0ecf878e5fdade18237fe Mon Sep 17 00:00:00 2001 From: M_Viper Date: Sat, 24 Oct 2026 20:14:39 +0000 Subject: [PATCH] Upload via GUI (335 Dateien) --- DeskHub-Webseite/plugins/README.md | 2 +- DeskHub-Webseite/staff/forgot_password.php | 1 + DeskHub-Webseite/staff/languages.php | 85 ++++++++++++++++++++++ DeskHub-Webseite/staff/live_tickets.php | 64 ++++++++++++++++ DeskHub-Webseite/staff/login.php | 30 +++++--- DeskHub-Webseite/staff/plugin.php | 6 +- DeskHub-Webseite/staff/plugins.php | 24 ++++-- DeskHub-Webseite/staff/reports.php | 54 ++++++++++++-- 8 files changed, 239 insertions(+), 27 deletions(-) create mode 100644 DeskHub-Webseite/staff/languages.php create mode 100644 DeskHub-Webseite/staff/live_tickets.php diff --git a/DeskHub-Webseite/plugins/README.md b/DeskHub-Webseite/plugins/README.md index 588f2430..03b8bfc5 100644 --- a/DeskHub-Webseite/plugins/README.md +++ b/DeskHub-Webseite/plugins/README.md @@ -32,7 +32,7 @@ plugins/mein-plugin/ } ``` -`permissions` nennt die Erweiterungspunkte, die das Plugin nutzt (`hooks.tickets`, `hooks.cron`, `ui.menu`, `ui.sidebar`, `api.endpoints`, `db.migrations`). Der Admin bestätigt sie bei der Aktivierung; nicht deklarierte Erweiterungspunkte werden abgelehnt. Das ist keine Sandbox. Vollständige Dokumentation inklusive Signieren: README.md im Hauptverzeichnis. +`permissions` nennt die Erweiterungspunkte, die das Plugin nutzt (`hooks.tickets`, `hooks.cron`, `ui.menu`, `ui.sidebar`, `api.endpoints`, `api.public`, `db.migrations`). Der Admin bestätigt sie bei der Aktivierung; nicht deklarierte Erweiterungspunkte werden abgelehnt. Das ist keine Sandbox. Die Hooks `cron.minute` (etwa jede Minute, für kurze Prüfabstände) und `cron.run` (höchstens alle 5 Minuten), beide mit der Berechtigung `hooks.cron`, werden ausgelöst vom echten Server-Cronjob (`cron/run_plugins.php`, wird unter Einstellungen → Cron-Jobs eingetragen) und vom eingebauten Hintergrund-Betrieb, nie doppelt. Der Filter `staff.page.top` (Berechtigung `ui.sidebar`) ergänzt einen Hinweis oben auf jeder Seite des Mitarbeiterbereichs (fertiges HTML; Kontext: `staff`, `activePage`). Vollständige Dokumentation inklusive Signieren: README.md im Hauptverzeichnis. ## Erweiterungspunkte diff --git a/DeskHub-Webseite/staff/forgot_password.php b/DeskHub-Webseite/staff/forgot_password.php index 243d930d..95ea0ab3 100644 --- a/DeskHub-Webseite/staff/forgot_password.php +++ b/DeskHub-Webseite/staff/forgot_password.php @@ -45,6 +45,7 @@ $pageTitle = 'Passwort vergessen'; +
diff --git a/DeskHub-Webseite/staff/languages.php b/DeskHub-Webseite/staff/languages.php new file mode 100644 index 00000000..aa8a2777 --- /dev/null +++ b/DeskHub-Webseite/staff/languages.php @@ -0,0 +1,85 @@ +query('SELECT text FROM i18n_missing ORDER BY hits DESC, text')->fetchAll(PDO::FETCH_COLUMN) as $t) $out[$t] = ''; + foreach ($existing as $k => $v) if (is_string($v) && ($k === '' || $k[0] !== '_')) $out[$k] = $v; + $out = ['_info' => 'Schlüssel = deutscher Text, Wert = Übersetzung. {} steht für wechselnde Teile. Leere Werte werden ignoriert.'] + $out; + header('Content-Type: application/json; charset=utf-8'); + header('Content-Disposition: attachment; filename="strings-' . preg_replace('/[^a-z\-]/', '', $code) . '.json"'); + echo json_encode($out, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); + exit; +} + +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + verify_csrf(); + $action = $_POST['action'] ?? ''; + if ($action === 'save') { + $def = strtolower((string) ($_POST['default_language'] ?? 'de')); + set_setting('default_language', isset($langs[$def]) ? $def : 'de'); + set_setting('i18n_collect', !empty($_POST['i18n_collect']) ? '1' : '0'); + audit_log($admin['id'], 'Sprach-Einstellungen geändert'); + flash('success', 'Gespeichert.'); + } elseif ($action === 'clear') { + $pdo->exec('DELETE FROM i18n_missing'); + flash('success', 'Gesammelte Texte gelöscht.'); + } + redirect('/staff/languages.php'); +} + +$collected = (int) $pdo->query('SELECT COUNT(*) FROM i18n_missing')->fetchColumn(); +$collect = get_setting('i18n_collect', '0') === '1'; +$pageTitle = 'Sprachen'; +$activePage = 'languages'; +require __DIR__ . '/_header.php'; +?> +

Sprachen

+

Jede Sprache ist ein Plugin („Sprachpaket“): Deutsch ist eingebaut, weitere Sprachen installieren Sie unter Plugins. Besucher und Mitarbeiter wechseln die Sprache über die Flaggen. Texte ohne Übersetzung bleiben deutsch.

+ +
+

Installierte Sprachen

+ $l): $n = $code === 'de' ? null : i18n_dictionary($code)['count']; ?> +
+ +
+
+ +
+ +

Noch kein Sprachpaket aktiv. Installieren Sie zum Beispiel „English“ unter Plugins; dann erscheinen die Flaggen.

+
+ +
+

Einstellungen

+ + + +
+
+ +
+

Gesammelte Texte:

+

Das sind deutsche Texte, die der Sammelmodus gesehen hat. Sie bilden die Vorlage für ein neues Sprachpaket.

+
+
+ +
+

Eigene Sprache als Plugin erstellen

+
    +
  1. Sammelmodus einschalten, die Seiten durchklicken, dann bei Deutsch „Leere Vorlage laden“ (alle gesammelten Texte, noch ohne Übersetzung). Für Ergänzungen zu einer vorhandenen Sprache bei dieser „Vorlage laden“: bereits Übersetztes ist eingetragen.
  2. +
  3. In der Datei strings.json die Werte übersetzen. Der Schlüssel ist der deutsche Text, {} steht für wechselnde Teile („Willkommen, {}“ → „Welcome, {}“).
  4. +
  5. Ordner mit diesen Dateien packen: plugin.json, plugin.php (kann leer sein), flag.svg (Flagge, höchstens 40 KB), strings.json.
  6. +
  7. Die plugin.json sieht so aus: {"name":"Français","version":"1.0.0","entry":"plugin.php","language":{"code":"fr","name":"Français","flag":"flag.svg","strings":"strings.json"}}
  8. +
  9. ZIP unter Plugins hochladen und aktivieren (das Sprachpaket braucht keine Berechtigungen). Die Flagge erscheint sofort.
  10. +
+
+ diff --git a/DeskHub-Webseite/staff/live_tickets.php b/DeskHub-Webseite/staff/live_tickets.php new file mode 100644 index 00000000..461408a6 --- /dev/null +++ b/DeskHub-Webseite/staff/live_tickets.php @@ -0,0 +1,64 @@ + 'login']); + exit; +} +// Abgelaufene Sitzung (Inaktivität) gilt auch hier als abgemeldet; dieser Abruf verlängert die Sitzung nie +$__timeout = (int) (get_setting('staff_session_timeout', '30') ?: 30); +$__last = (int) ($_SESSION['staff_last_activity'] ?? 0); +if ($__last && (time() - $__last) > $__timeout * 60) { + http_response_code(401); + echo json_encode(['error' => 'login', 'sessionExpired' => true]); + exit; +} +// Die Sitzungsdatei sofort freigeben: Der Abruf läuft alle paar Sekunden und soll andere Seitenaufrufe nicht ausbremsen +if (session_status() === PHP_SESSION_ACTIVE) session_write_close(); + +$pdo = get_db(); +$since = max(0, (int) ($_GET['since'] ?? 0)); +[$deptFilter, $deptParams] = dept_filter_sql(staff_department_ids($staff), 't.department_id'); +$count = function (string $where, array $params = []) use ($pdo, $deptFilter, $deptParams): int { + $s = $pdo->prepare("SELECT COUNT(*) FROM tickets t WHERE {$where}{$deptFilter}"); + $s->execute(array_merge($params, $deptParams)); + return (int) $s->fetchColumn(); +}; +$open = "t.status NOT IN ('resolved','closed')"; +$counts = [ + 'open' => $count($open), + 'mine' => $count("t.assigned_staff_id = ? AND {$open}", [(int) $staff['id']]), + 'unassigned' => $count("t.assigned_staff_id IS NULL AND {$open}"), + 'overdue' => $count("{$open} AND t.resolution_due_at IS NOT NULL AND t.resolution_due_at < ?", [db_now()]), + 'unanswered' => $count("t.status = 'open'"), +]; + +// Neue Tickets: ohne Spam, ohne Live-Chats (die haben eigene Hinweise) und ohne zusammengeführte +$visible = "t.is_spam = 0 AND t.source <> 'chat' AND t.merged_into IS NULL"; +$m = $pdo->prepare("SELECT COALESCE(MAX(t.id), 0) FROM tickets t WHERE {$visible}{$deptFilter}"); +$m->execute($deptParams); +$latest = (int) $m->fetchColumn(); +$tickets = []; +$more = 0; +if ($since > 0 && $latest > $since) { + $s = $pdo->prepare("SELECT t.id, t.ticket_number, t.subject, t.requester_name, d.name AS department FROM tickets t LEFT JOIN departments d ON d.id = t.department_id + WHERE t.id > ? AND {$visible}{$deptFilter} ORDER BY t.id ASC LIMIT 6"); + $s->execute(array_merge([$since], $deptParams)); + foreach ($s->fetchAll() as $t) { + $tickets[] = ['id' => (int) $t['id'], 'number' => (string) $t['ticket_number'], 'subject' => mb_substr((string) $t['subject'], 0, 120), 'name' => mb_substr((string) $t['requester_name'], 0, 60), 'department' => (string) ($t['department'] ?? '')]; + } + $c = $pdo->prepare("SELECT COUNT(*) FROM tickets t WHERE t.id > ? AND {$visible}{$deptFilter}"); + $c->execute(array_merge([$since], $deptParams)); + $more = max(0, (int) $c->fetchColumn() - count($tickets)); +} +echo json_encode(['latest' => $latest, 'counts' => $counts, 'tickets' => $tickets, 'more' => $more], JSON_UNESCAPED_UNICODE); diff --git a/DeskHub-Webseite/staff/login.php b/DeskHub-Webseite/staff/login.php index c9c01ecb..0525fc89 100644 --- a/DeskHub-Webseite/staff/login.php +++ b/DeskHub-Webseite/staff/login.php @@ -9,8 +9,10 @@ if (current_staff()) { $errors = []; $need2fa = false; -if ($_SERVER['REQUEST_METHOD'] === 'POST') { - verify_csrf(); +if ($_SERVER['REQUEST_METHOD'] === 'POST' && !csrf_valid()) { + // Die Anmeldeseite war so lange offen, dass die Sitzung auf dem Server inzwischen aufgeräumt wurde: neu anmelden statt Fehlerseite + $errors[] = 'Die Anmeldeseite war zu lange geöffnet und die Sitzung ist abgelaufen. Bitte melden Sie sich jetzt noch einmal an.'; +} elseif ($_SERVER['REQUEST_METHOD'] === 'POST') { $action = $_POST['action'] ?? 'login'; if ($action === 'verify_totp') { @@ -82,25 +84,26 @@ $displayName = get_setting('display_name', config()['site']['name']); - + style="background-image:url(''); background-size:cover; background-position:center"> + -
-
+