['label' => 'E-Mail-Abruf', 'file' => 'fetch_emails.php', 'fn' => 'run_fetch_emails'], 'check_sla' => ['label' => 'SLA-Prüfung', 'file' => 'check_sla.php', 'fn' => 'run_check_sla'], 'check_escalations' => ['label' => 'Eskalationsregeln', 'file' => 'check_escalations.php', 'fn' => 'run_check_escalations'], 'process_recurring' => ['label' => 'Wiederkehrende Tickets', 'file' => 'process_recurring.php', 'fn' => 'run_process_recurring'], 'process_backups' => ['label' => 'Automatische Backups', 'file' => 'process_backups.php', 'fn' => 'run_process_backups'], 'anonymize_data' => ['label' => 'Datenanonymisierung', 'file' => 'anonymize_data.php', 'fn' => 'run_anonymization'], 'auto_close_tickets'=> ['label' => 'Automatisches Schließen', 'file' => 'auto_close_tickets.php','fn' => 'run_auto_close_tickets'], 'process_snoozes' => ['label' => 'Wiedervorlagen', 'file' => 'process_snoozes.php', 'fn' => 'run_process_snoozes'], 'process_mail_queue' => ['label' => 'Mail-Warteschlange', 'file' => 'process_mail_queue.php', 'fn' => 'run_process_mail_queue'], 'remind_customers' => ['label' => 'Kundenerinnerung', 'file' => 'remind_customers.php', 'fn' => 'run_remind_customers'], 'send_csat' => ['label' => 'Zufriedenheitsabfrage', 'file' => 'send_csat.php', 'fn' => 'run_send_csat'], 'status_checks' => ['label' => 'Statusprüfungen', 'file' => 'status_checks.php', 'fn' => 'run_status_checks'], 'send_scheduled_replies' => ['label' => 'Geplante Antworten', 'file' => 'send_scheduled_replies.php', 'fn' => 'run_send_scheduled_replies'], 'send_report_schedules' => ['label' => 'Berichte per E-Mail', 'file' => 'send_report_schedules.php', 'fn' => 'run_send_report_schedules'], 'run_plugins' => ['label' => 'Plugins (Hintergrundaufgaben)', 'file' => 'run_plugins.php', 'fn' => 'run_plugins_cron'], ]; if ($_SERVER['REQUEST_METHOD'] === 'POST') { verify_csrf(); $action = $_POST['action'] ?? ''; if ($action === 'save_system') { $status = ($_POST['helpdesk_status'] ?? 'online') === 'offline' ? 'offline' : 'online'; $offlineMsg = trim($_POST['offline_message'] ?? ''); $defaultDept = (int)($_POST['default_department_id'] ?? 0); $kbEnabled = !empty($_POST['kb_enabled']) ? '1' : '0'; if (!in_array($defaultDept, array_column($departments, 'id'))) { $errors[] = 'Bitte eine gültige Standardabteilung wählen.'; } else { set_setting('helpdesk_status', $status); set_setting('offline_message', $offlineMsg ?: 'Das Supportcenter ist derzeit nicht erreichbar.'); set_setting('default_department_id', (string)$defaultDept); set_setting('kb_enabled', $kbEnabled); set_setting('minecraft_enabled', !empty($_POST['minecraft_enabled']) ? '1' : '0'); set_setting('ticket_ratings_enabled', !empty($_POST['ticket_ratings_enabled']) ? '1' : '0'); set_setting('auto_time_tracking_enabled', !empty($_POST['auto_time_tracking_enabled']) ? '1' : '0'); set_setting('auto_assign_on_open', !empty($_POST['auto_assign_on_open']) ? '1' : '0'); set_setting('live_chat_enabled', !empty($_POST['live_chat_enabled']) ? '1' : '0'); $liveChatDept = $_POST['live_chat_department_id'] ?? ''; set_setting('live_chat_department_id', ($liveChatDept !== '' && in_array((int)$liveChatDept, array_column($departments, 'id'))) ? (string)(int)$liveChatDept : ''); set_setting('live_chat_send_transcript', !empty($_POST['live_chat_send_transcript']) ? '1' : '0'); set_setting('auto_close_enabled', !empty($_POST['auto_close_enabled']) ? '1' : '0'); $autoCloseDays = max(1, min(90, (int) ($_POST['auto_close_days'] ?? 7))); set_setting('auto_close_days', (string) $autoCloseDays); set_setting('sla_notify_mode', ($_POST['sla_notify_mode'] ?? 'admins') === 'assignee' ? 'assignee' : 'admins'); set_setting('wait_reminder_enabled', !empty($_POST['wait_reminder_enabled']) ? '1' : '0'); set_setting('wait_reminder_hours', (string) max(1, min(72, (int) ($_POST['wait_reminder_hours'] ?? 4)))); set_setting('wait_reminder_email', !empty($_POST['wait_reminder_email']) ? '1' : '0'); set_setting('remind_enabled', !empty($_POST['remind_enabled']) ? '1' : '0'); set_setting('remind_after_days', (string) max(1, min(60, (int) ($_POST['remind_after_days'] ?? 3)))); csat_set_enabled(!empty($_POST['csat_enabled']), (int) ($_POST['csat_delay_minutes'] ?? 30)); $timeout = max(5, min(480, (int)($_POST['staff_session_timeout'] ?? 30))); set_setting('staff_session_timeout', (string)$timeout); audit_log($staff['id'], "Systemeinstellungen geändert"); flash('success', 'Systemeinstellungen gespeichert.'); redirect('/staff/settings.php?tab=allgemein'); } } if ($action === 'save_privacy') { $enabled = !empty($_POST['retention_enabled']) ? '1' : '0'; $months = max(1, min(120, (int)($_POST['retention_months'] ?? 24))); $ipAnon = !empty($_POST['ip_anonymize']) ? '1' : '0'; set_setting('retention_enabled', $enabled); set_setting('retention_months', (string)$months); set_setting('ip_anonymize', $ipAnon); set_setting('gdpr_tools_enabled', !empty($_POST['gdpr_tools_enabled']) ? '1' : '0'); audit_log($staff['id'], 'Datenschutzeinstellungen geändert'); flash('success', 'Datenschutzeinstellungen gespeichert.'); redirect('/staff/settings.php?tab=datenschutz'); } if ($action === 'run_anonymize_now') { define('DESKHUB_CRON_RUNNER', true); require_once __DIR__ . '/../cron/anonymize_data.php'; $result = run_anonymization(get_db(), true); audit_log($staff['id'], "Datenanonymisierung manuell ausgeführt: {$result['count']} Ticket(s)"); flash('success', "{$result['count']} Ticket(s) anonymisiert."); redirect('/staff/settings.php?tab=datenschutz'); } // Manueller Start eines Cronjobs aus dem Backend ("Jetzt ausführen"-Button, Tab Cron-Jobs). // Führt exakt die gleiche Logik aus wie der echte Cronjob, nur direkt per Klick statt per crontab. if ($action === 'run_cron_now') { $job = $_POST['job'] ?? ''; if (!isset($cronJobs[$job])) { flash('error', 'Unbekannter Cronjob.'); redirect('/staff/settings.php?tab=cron'); } $meta = $cronJobs[$job]; define('DESKHUB_CRON_RUNNER', true); require_once __DIR__ . '/../cron/' . $meta['file']; $fn = $meta['fn']; try { if ($job === 'anonymize_data') { $result = $fn(get_db(), true); $summary = "{$result['count']} Ticket(s) anonymisiert."; } elseif ($job === 'process_backups') { // Manueller Klick erzwingt ein Backup unabhängig vom Zeitplan. $result = $fn(get_db(), true, true); $summary = $result['ok'] ? ($result['ran'] ? 'Backup erstellt.' : 'Kein Backup nötig.') : 'Backup fehlgeschlagen.'; } else { $result = $fn(get_db(), true); $summary = end($result['log']) ?: 'Ausgeführt.'; } audit_log($staff['id'], "Cronjob manuell ausgeführt: {$meta['label']} – {$summary}"); flash('success', "{$meta['label']}: {$summary}"); } catch (Throwable $e) { audit_log($staff['id'], "Cronjob manuell fehlgeschlagen: {$meta['label']} – " . $e->getMessage()); flash('error', "{$meta['label']} fehlgeschlagen: " . $e->getMessage()); } redirect('/staff/settings.php?tab=cron'); } // Ein-/Ausschalten des eingebauten Auto-Cron (siehe includes/pseudo_cron.php). if ($action === 'save_pseudo_cron') { $enabled = !empty($_POST['pseudo_cron_enabled']) ? '1' : '0'; set_setting('pseudo_cron_enabled', $enabled); audit_log($staff['id'], $enabled === '1' ? 'Automatischer Hintergrund-Cron aktiviert' : 'Automatischer Hintergrund-Cron deaktiviert'); flash('success', 'Gespeichert.'); redirect('/staff/settings.php?tab=cron'); } // Echte System-Crontab installieren/entfernen (nur auf Servern mit Shell-Zugriff möglich, // siehe includes/crontab_installer.php – schreibt ausschließlich feste, fest hinterlegte Zeilen). if ($action === 'install_crontab') { $result = crontab_install(); if ($result['ok']) { audit_log($staff['id'], 'Server-Crontab installiert/aktualisiert (6 DeskHub-Cronjobs)'); flash('success', 'Crontab installiert. Die 6 Cronjobs laufen jetzt direkt über den Server-Scheduler.'); } else { audit_log($staff['id'], 'Server-Crontab-Installation fehlgeschlagen: ' . $result['error']); flash('error', 'Installation fehlgeschlagen: ' . $result['error']); } redirect('/staff/settings.php?tab=cron'); } if ($action === 'remove_crontab') { $result = crontab_uninstall(); if ($result['ok']) { audit_log($staff['id'], 'Server-Crontab-Eintrag von DeskHub entfernt'); flash('success', 'Crontab-Eintrag entfernt.'); } else { audit_log($staff['id'], 'Entfernen der Server-Crontab fehlgeschlagen: ' . $result['error']); flash('error', 'Entfernen fehlgeschlagen: ' . $result['error']); } redirect('/staff/settings.php?tab=cron'); } if ($action === 'save_name') { $name = trim($_POST['display_name'] ?? ''); $alertTo = trim((string) ($_POST['error_alert_email'] ?? '')); foreach (preg_split('/[,;\s]+/', $alertTo, -1, PREG_SPLIT_NO_EMPTY) ?: [] as $m) { if (!filter_var($m, FILTER_VALIDATE_EMAIL)) { $errors[] = 'Die Adresse für Fehler-Mails ist ungültig: ' . $m; break; } } if (!$name) { $errors[] = 'Bitte einen Anzeigenamen angeben.'; } elseif (!$errors) { set_setting('display_name', $name); $siteUrl = rtrim(trim($_POST['site_url'] ?? ''), '/'); if ($siteUrl) set_setting('site_url', $siteUrl); $tz = trim($_POST['timezone'] ?? ''); if ($tz && in_array($tz, timezone_identifiers_list(), true)) set_setting('timezone', $tz); set_setting('mail_enabled', !empty($_POST['mail_enabled']) ? '1' : '0'); set_setting('error_alert', !empty($_POST['error_alert']) ? '1' : '0'); set_setting('error_alert_email', mb_substr($alertTo, 0, 300)); audit_log($staff['id'], "Anzeigename / Basis-Einstellungen geändert: $name"); flash('success', 'Einstellungen gespeichert.'); redirect('/staff/settings.php?tab=allgemein'); } } if ($action === 'upload_logo') { $file = $_FILES['logo'] ?? []; $allowed = ['png','jpg','jpeg','gif','svg']; $ext = strtolower(pathinfo($file['name'] ?? '', PATHINFO_EXTENSION)); if (empty($file['name'])) { $errors[] = 'Bitte eine Bilddatei auswählen.'; } elseif ($file['error'] !== UPLOAD_ERR_OK) { $errors[] = 'Upload fehlgeschlagen.'; } elseif ($file['size'] > 2*1024*1024) { $errors[] = 'Max. 2 MB.'; } elseif (!in_array($ext, $allowed, true)) { $errors[] = 'Erlaubt: PNG, JPG, GIF, SVG.'; } elseif (($imgErr = favicon_validate_file($file['tmp_name'], $ext)) !== null) { $errors[] = $imgErr; } else { foreach ($allowed as $e2) @unlink($branding.'/logo.'.$e2); $dest = $branding.'/logo.'.$ext; if (!move_uploaded_file($file['tmp_name'], $dest)) { $errors[] = 'Speichern fehlgeschlagen.'; } else { set_setting('logo_path', '/assets/branding/logo.'.$ext.'?v='.time()); audit_log($staff['id'],'Logo aktualisiert'); flash('success','Logo aktualisiert.'); redirect('/staff/settings.php?tab=design'); } } } if ($action === 'upload_favicon') { $file = $_FILES['favicon'] ?? []; $ext = strtolower(pathinfo($file['name'] ?? '', PATHINFO_EXTENSION)); $err = null; if (empty($file['name'])) $err = 'Bitte eine Datei auswählen.'; elseif (($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) $err = 'Upload fehlgeschlagen' . (($file['error'] ?? 0) === UPLOAD_ERR_INI_SIZE ? ' (die Datei ist größer als das Upload-Limit des Servers).' : '.'); elseif ($file['size'] > 1024 * 1024) $err = 'Das Favicon darf höchstens 1 MB groß sein.'; elseif (!isset(favicon_types()[$ext])) $err = 'Erlaubt: ICO, PNG, JPG, GIF, WebP, SVG.'; else $err = favicon_validate_file($file['tmp_name'], $ext); if ($err !== null) { $errors[] = $err; } else { foreach (array_keys(favicon_types()) as $e2) @unlink($branding . '/favicon.' . $e2); if (!move_uploaded_file($file['tmp_name'], $branding . '/favicon.' . $ext)) { $errors[] = 'Speichern fehlgeschlagen (Ordner assets/branding/ nicht beschreibbar?).'; } else { set_setting('favicon_path', '/assets/branding/favicon.' . $ext . '?v=' . time()); audit_log($staff['id'], 'Favicon aktualisiert'); flash('success', 'Favicon aktualisiert. Browser zeigen Änderungen oft erst nach Neuladen (Strg+F5) oder in einem neuen Tab.'); redirect('/staff/settings.php?tab=design'); } } } if ($action === 'remove_favicon') { foreach (array_keys(favicon_types()) as $e2) @unlink($branding . '/favicon.' . $e2); set_setting('favicon_path', null); audit_log($staff['id'], 'Favicon entfernt'); flash('success', 'Favicon entfernt.'); redirect('/staff/settings.php?tab=design'); } if ($action === 'save_logo_options') { $hide = !empty($_POST['logo_hide_name']) ? '1' : '0'; set_setting('logo_hide_name', $hide); audit_log($staff['id'], $hide === '1' ? 'Name neben dem Logo ausgeblendet' : 'Name neben dem Logo wird angezeigt'); flash('success', $hide === '1' ? 'Der Name wird neben dem Logo nicht mehr angezeigt.' : 'Der Name wird wieder neben dem Logo angezeigt.'); redirect('/staff/settings.php?tab=design'); } if ($action === 'remove_logo') { foreach (['png','jpg','jpeg','gif','svg'] as $e2) @unlink($branding.'/logo.'.$e2); set_setting('logo_path', null); audit_log($staff['id'],'Logo entfernt'); flash('success','Logo entfernt.'); redirect('/staff/settings.php?tab=design'); } if ($action === 'upload_banner') { $file = $_FILES['banner'] ?? []; $allowed = ['png','jpg','jpeg','gif']; $ext = strtolower(pathinfo($file['name'] ?? '', PATHINFO_EXTENSION)); if (empty($file['name'])) { $errors[] = 'Bitte eine Bilddatei auswählen.'; } elseif ($file['error'] !== UPLOAD_ERR_OK) { $errors[] = 'Upload fehlgeschlagen.'; } elseif ($file['size'] > 5*1024*1024) { $errors[] = 'Max. 5 MB.'; } elseif (!in_array($ext, $allowed, true)) { $errors[] = 'Erlaubt: PNG, JPG, GIF.'; } elseif (($imgErr = favicon_validate_file($file['tmp_name'], $ext)) !== null) { $errors[] = $imgErr; } else { foreach ($allowed as $e2) @unlink($branding.'/banner.'.$e2); $dest = $branding.'/banner.'.$ext; if (!move_uploaded_file($file['tmp_name'], $dest)) { $errors[] = 'Speichern fehlgeschlagen.'; } else { set_setting('banner_path', '/assets/branding/banner.'.$ext.'?v='.time()); audit_log($staff['id'],'Titelbild aktualisiert'); flash('success','Titelbild aktualisiert.'); redirect('/staff/settings.php?tab=design'); } } } if ($action === 'remove_banner') { foreach (['png','jpg','jpeg','gif'] as $e2) @unlink($branding.'/banner.'.$e2); set_setting('banner_path', null); audit_log($staff['id'],'Titelbild entfernt'); flash('success','Titelbild entfernt.'); redirect('/staff/settings.php?tab=design'); } if ($action === 'save_homepage_content') { $popularArticlesEnabled = !empty($_POST['homepage_popular_articles_enabled']) ? '1' : '0'; set_setting('homepage_popular_articles_enabled', $popularArticlesEnabled); audit_log( $staff['id'], $popularArticlesEnabled === '1' ? 'Beliebte Artikel auf der Startseite eingeblendet' : 'Beliebte Artikel auf der Startseite ausgeblendet' ); flash('success', 'Startseiten-Inhalte gespeichert.'); redirect('/staff/settings.php?tab=design'); } if ($action === 'upload_custom_theme') { try { $theme = install_custom_theme_upload($_FILES['theme_zip'] ?? []); $actionLabel = $theme['replaced'] ? 'aktualisiert' : 'hochgeladen'; audit_log($staff['id'], "Kundenportal-Theme {$actionLabel}: {$theme['name']}"); flash('success', "Theme „{$theme['name']}“ wurde {$actionLabel}."); redirect('/staff/settings.php?tab=design'); } catch (Throwable $e) { $errors[] = $e->getMessage(); } } if ($action === 'save_theme_catalog' && plugin_strict_mode()) { $errors[] = 'Im geprüften Modus ist der Katalog fest vorgegeben.'; } elseif ($action === 'save_theme_catalog') { $url = trim((string) ($_POST['theme_catalog_url'] ?? '')); if ($url !== '' && ($urlError = plugin_url_check($url))) { $errors[] = 'Katalog-Adresse: ' . $urlError; } else { set_setting('theme_catalog_url', $url); flash('success', 'Katalog-Adresse gespeichert.'); redirect('/staff/settings.php?tab=design'); } } if ($action === 'theme_catalog_install') { $res = theme_catalog_install((string) ($_POST['slug'] ?? '')); if ($res['ok']) { audit_log($staff['id'], "Kundenportal-Theme aus dem Katalog installiert: {$res['theme']['name']} (SHA-256 {$res['hash']}" . ($res['signed'] ? ', signiert' : ', nicht signiert') . ')'); flash('success', "Theme „{$res['theme']['name']}“ installiert (" . ($res['signed'] ? 'Signatur geprüft' : 'nicht signiert') . '). Es ist noch nicht aktiv.'); redirect('/staff/settings.php?tab=design'); } $errors[] = $res['error']; } if ($action === 'activate_custom_theme') { $themeId = (string) ($_POST['theme_id'] ?? ''); $theme = null; foreach (list_custom_themes() as $candidate) { if ($candidate['id'] === $themeId) { $theme = $candidate; break; } } if ($theme === null) { $errors[] = 'Das ausgewählte Theme wurde nicht gefunden.'; } else { set_setting('active_custom_theme', $themeId); audit_log($staff['id'], "Kundenportal-Theme aktiviert: {$theme['name']}"); flash('success', "Theme „{$theme['name']}“ ist jetzt aktiv."); redirect('/staff/settings.php?tab=design'); } } if ($action === 'deactivate_custom_theme') { set_setting('active_custom_theme', null); audit_log($staff['id'], 'Benutzerdefiniertes Kundenportal-Theme deaktiviert'); flash('success', 'Das Standarddesign ist jetzt aktiv.'); redirect('/staff/settings.php?tab=design'); } if ($action === 'delete_custom_theme') { $themeId = (string) ($_POST['theme_id'] ?? ''); $theme = null; foreach (list_custom_themes() as $candidate) { if ($candidate['id'] === $themeId) { $theme = $candidate; break; } } if ($theme === null || !delete_custom_theme($themeId)) { $errors[] = 'Das Theme konnte nicht gelöscht werden.'; } else { if (get_setting('active_custom_theme', '') === $themeId) { set_setting('active_custom_theme', null); } audit_log($staff['id'], "Kundenportal-Theme gelöscht: {$theme['name']}"); flash('success', "Theme „{$theme['name']}“ wurde gelöscht."); redirect('/staff/settings.php?tab=design'); } } if ($action === 'save_uploads') { $dangerous = ['php','php3','php4','php5','phtml','phar','exe','bat','cmd','sh','pl','py','asp','aspx','jsp','cgi']; $extList = array_filter(array_map('trim', explode(',', strtolower($_POST['upload_allowed_extensions'] ?? '')))); $extList = array_diff($extList, $dangerous); set_setting('upload_allowed_extensions', implode(', ', $extList)); $mailExt = array_diff(array_filter(array_map(fn($e) => ltrim(trim($e), '.'), explode(',', strtolower($_POST['mail_attachment_extra_extensions'] ?? '')))), $dangerous); set_setting('mail_attachment_extra_extensions', implode(', ', $mailExt)); set_setting('upload_max_size_mb', max(1, min(100, (int)($_POST['upload_max_size_mb'] ?? 10)))); audit_log($staff['id'], 'Upload-Einstellungen geändert'); flash('success', 'Upload-Einstellungen gespeichert.'); redirect('/staff/settings.php?tab=uploads'); } if ($action === 'save_footer_pages') { foreach (['footer_privacy_label','footer_privacy_url','footer_privacy_html','footer_imprint_label','footer_imprint_url','footer_imprint_html'] as $k) set_setting($k, trim($_POST[$k] ?? '') ?: null); audit_log($staff['id'], 'Footer-Seiten geändert'); flash('success', 'Footer-Einstellungen gespeichert.'); redirect('/staff/settings.php?tab=footer'); } if ($action === 'save_notifications') { $discordNew = trim($_POST['discord_webhook_url'] ?? ''); $tgTokenNew = trim($_POST['telegram_bot_token'] ?? ''); $tgChatId = trim($_POST['telegram_chat_id'] ?? ''); // Leer lassen = unverändert; Wert "-" = explizit löschen; sonst verschlüsseln if ($discordNew !== '') { set_setting('discord_webhook_url', $discordNew === '-' ? null : encrypt_secret($discordNew)); } if ($tgTokenNew !== '') { set_setting('telegram_bot_token', $tgTokenNew === '-' ? null : encrypt_secret($tgTokenNew)); } if ($tgChatId !== '') { set_setting('telegram_chat_id', $tgChatId === '-' ? null : $tgChatId); } audit_log($staff['id'], 'Benachrichtigungen geändert'); flash('success', 'Benachrichtigungseinstellungen gespeichert.'); redirect('/staff/settings.php?tab=benachrichtigungen'); } // Push-Benachrichtigungen für die Android-App (Firebase Cloud Messaging) if ($action === 'save_fcm') { set_setting('fcm_enabled', !empty($_POST['fcm_enabled']) ? '1' : '0'); set_setting('fcm_show_details', !empty($_POST['fcm_show_details']) ? '1' : '0'); set_setting('push_brand', fcm_clean_brand((string) ($_POST['push_brand'] ?? ''))); flash('success', 'Einstellungen für die Android-Benachrichtigungen gespeichert.'); redirect('/staff/settings.php?tab=benachrichtigungen'); } if ($action === 'remove_fcm') { set_setting('fcm_service_account', ''); set_setting('fcm_access_token', ''); audit_log($staff['id'], 'Firebase-Dienstkonto für Android-Push entfernt'); flash('success', 'Der Firebase-Schlüssel wurde vom Server entfernt.'); redirect('/staff/settings.php?tab=benachrichtigungen'); } if ($action === 'test_fcm') { if (fcm_mode() === 'none') { flash('error', 'Push ist nicht eingerichtet: Es ist kein Push-Dienst eingetragen und kein eigener Firebase-Schlüssel hinterlegt.'); redirect('/staff/settings.php?tab=benachrichtigungen'); } $t = fcm_send_test($pdo, (int) $staff['id']); if ($t['devices'] === 0) { flash('error', 'Auf deinem Konto ist noch kein Android-Gerät angemeldet. Melde dich in der App an (Benachrichtigungen erlauben) und versuche es erneut.'); redirect('/staff/settings.php?tab=benachrichtigungen'); } $t['sent'] > 0 ? flash('success', "Testnachricht an {$t['sent']} Gerät(e) gesendet. Sie sollte jetzt auf dem Handy erscheinen.") : flash('error', 'Senden fehlgeschlagen: ' . ($t['error'] !== '' ? $t['error'] : 'unbekannter Fehler')); redirect('/staff/settings.php?tab=benachrichtigungen'); } if ($action === 'test_notifications') { $sent = []; if (discord_notify('🔔 Testnachricht von DeskHub')) $sent[] = 'Discord'; if (telegram_notify('🔔 Testnachricht von DeskHub')) $sent[] = 'Telegram'; $sent ? flash('success', 'Gesendet an: '.implode(', ', $sent)) : flash('error', 'Kein Kanal konfiguriert oder Fehler.'); redirect('/staff/settings.php?tab=benachrichtigungen'); } if ($action === 'save_smtp') { set_setting('smtp_host', trim($_POST['smtp_host'] ?? '') ?: null); set_setting('smtp_port', trim($_POST['smtp_port'] ?? '') ?: null); set_setting('smtp_encryption', $_POST['smtp_encryption'] ?? 'tls'); set_setting('smtp_username', trim($_POST['smtp_username'] ?? '') ?: null); if (trim($_POST['smtp_password'] ?? '') !== '') set_setting('smtp_password', encrypt_secret(trim($_POST['smtp_password']))); set_setting('smtp_from_name', trim($_POST['smtp_from_name'] ?? '') ?: null); set_setting('smtp_from_email', trim($_POST['smtp_from_email'] ?? '') ?: null); audit_log($staff['id'], 'SMTP geändert'); flash('success', 'SMTP-Einstellungen gespeichert.'); redirect('/staff/settings.php?tab=email'); } if ($action === 'test_smtp') { $testBody = email_wrap(email_p('Diese Test-E-Mail bestätigt, dass Ihr SMTP-Versand korrekt eingerichtet ist. 🎉')); $ok = send_mail($staff['email'], 'DeskHub – Test-E-Mail', $testBody, null, false); $ok ? flash('success', 'Test-Mail an '.$staff['email'].' gesendet.') : flash('error', 'Versand fehlgeschlagen. Bitte SMTP-Daten prüfen. '.smtp_last_error()); redirect('/staff/settings.php?tab=email'); } // Einzelnes globales IMAP-Konto wurde durch die Tabelle imap_accounts abgelöst // (mehrere Postfächer möglich, siehe staff/imap_accounts.php). Die alten // Einzel-Settings (imap_host/imap_user/...) werden nur noch einmalig per // Migration php_0011_imap_accounts als erstes Postfach übernommen. } // ── Lazy-Encryption: Klartext-Credentials beim ersten Admin-Besuch sichern ── // Direkt über PDO (nicht get_setting) um den statischen Cache zu umgehen. // Idempotent: AES-GCM-Blobs (base64, min. 28 Bytes) werden nicht erneut verschlüsselt. foreach (['discord_webhook_url', 'telegram_bot_token', 'smtp_password', 'imap_pass'] as $_lek) { $stmt_le = $pdo->prepare('SELECT setting_value FROM settings WHERE setting_key = ?'); $stmt_le->execute([$_lek]); $raw_le = $stmt_le->fetchColumn(); if ($raw_le !== false && $raw_le !== '' && $raw_le !== null) { $decoded_le = base64_decode($raw_le, true); if ($decoded_le === false || strlen($decoded_le) < 28) { // Klartext gefunden → jetzt verschlüsseln $pdo->prepare('UPDATE settings SET setting_value = ? WHERE setting_key = ?') ->execute([encrypt_secret($raw_le), $_lek]); } } } unset($_lek, $stmt_le, $raw_le, $decoded_le); // ── Werte laden ─────────────────────────────────────────────────────────────── $currentLogo = get_setting('logo_path'); $currentBanner = get_setting('banner_path'); $homepagePopularArticlesEnabled = get_setting('homepage_popular_articles_enabled', '1') === '1'; $currentName = get_setting('display_name', config()['site']['name']); $helpdeskStatus = get_setting('helpdesk_status', 'online'); $offlineMessage = get_setting('offline_message', 'Das Supportcenter ist derzeit nicht erreichbar.'); $defaultDeptId = get_setting('default_department_id', (string)($departments[0]['id'] ?? '')); $kbEnabled = get_setting('kb_enabled', '1') === '1'; $discordUrl = get_setting('discord_webhook_url', ''); $telegramToken = get_setting('telegram_bot_token', ''); $telegramChatId = get_setting('telegram_chat_id', ''); $smtpHost = get_setting('smtp_host', ''); $smtpPort = get_setting('smtp_port', '587'); $smtpEncryption = get_setting('smtp_encryption', 'tls'); $smtpUsername = get_setting('smtp_username', ''); $smtpFromName = get_setting('smtp_from_name', ''); $smtpFromEmail = get_setting('smtp_from_email', ''); $footerPrivacyLabel = get_setting('footer_privacy_label', 'Datenschutz'); $footerPrivacyUrl = get_setting('footer_privacy_url', ''); $footerPrivacyHtml = get_setting('footer_privacy_html', ''); $footerImprintLabel = get_setting('footer_imprint_label', 'Impressum'); $footerImprintUrl = get_setting('footer_imprint_url', ''); $footerImprintHtml = get_setting('footer_imprint_html', ''); $uploadExtensions = get_setting('upload_allowed_extensions', implode(', ', config()['uploads']['allowed_extensions'])); $mailExtraExtensions = get_setting('mail_attachment_extra_extensions', MAIL_ATTACHMENT_EXTRA_DEFAULT); $uploadMaxSize = get_setting('upload_max_size_mb', '10'); $customThemes = list_custom_themes(); $activeCustomTheme = get_setting('active_custom_theme', ''); $cronFetch = cron_last_run_label(get_setting('cron_fetch_emails_last_run')); $cronSla = cron_last_run_label(get_setting('cron_check_sla_last_run')); $cronRecurring = cron_last_run_label(get_setting('cron_process_recurring_last_run')); $cronBackups = cron_last_run_label(get_setting('cron_process_backups_last_run')); $cronEscalations = cron_last_run_label(get_setting('cron_check_escalations_last_run')); $cronAnonymize = cron_last_run_label(get_setting('cron_anonymize_data_last_run')); $cronAutoClose = cron_last_run_label(get_setting('cron_auto_close_tickets_last_run')); $cronSnoozes = cron_last_run_label(get_setting('cron_process_snoozes_last_run')); $cronRemind = cron_last_run_label(get_setting('cron_remind_customers_last_run')); $cronPlugins = cron_last_run_label(get_setting('cron_run_plugins_last_run')); $cronCsat = cron_last_run_label(get_setting('cron_send_csat_last_run')); $tab = $_GET['tab'] ?? 'allgemein'; $pageTitle = 'Einstellungen'; $activePage = 'settings'; require __DIR__ . '/_header.php'; ?>

Einstellungen

📥 osTicket Import
'Allgemein', 'design' => 'Design', 'email' => 'E-Mail', 'benachrichtigungen' => 'Benachrichtigungen', 'uploads' => 'Uploads', 'footer' => 'Footer', 'cron' => 'Cron-Jobs', 'datenschutz' => '🔒 Datenschutz', ]; foreach ($tabs as $key => $label): $active = $tab === $key; ?>

System


Tage

Stunden (zählt nur die Geschäftszeiten, wenn sie in den SLA-Einstellungen eingerichtet sind)

Tage (am besten kürzer als die Frist zum automatischen Schließen)

Minuten (so bleibt Zeit, ein versehentlich geschlossenes Ticket wieder zu öffnen). Gilt nur für Tickets, die ab dem Einschalten geschlossen werden. Die Auswertung steht unter Übersicht.
Minuten (5–480). Countdown wird oben rechts angezeigt.

Anzeigename & URL

Wird im Browser-Tab, in E-Mails und in der Web-App angezeigt.

Wird in E-Mail-Links verwendet. Kein abschließendes /

Gemeldet werden Fehler, bei denen eine Seite nicht angezeigt werden kann. Dieselbe Meldung kommt höchstens einmal pro Stunde, insgesamt höchstens 10 Mails pro Stunde. Voraussetzung ist ein funktionierender E-Mail-Versand und eine erreichbare Datenbank.

Firmenlogo

PNG/SVG empfohlen, max. 2 MB. Wird überall statt „DeskHub" angezeigt. Der Name der Installation steht standardmäßig daneben und lässt sich unten ausblenden.

Logo

Kein Logo hinterlegt.

Favicon (Symbol im Browser-Tab)

ICO, PNG, JPG, GIF, WebP oder SVG, höchstens 1 MB, am besten quadratisch (z. B. 256×256). Ohne Favicon verwendet DeskHub das Logo.

Favicon

Kein eigenes Favicon hinterlegt.

Titelbild (Hero-Banner)

Hintergrundbild im dunklen Hero-Bereich. Mind. 1600×500 px, max. 5 MB.

Banner

Kein Titelbild hinterlegt.

Startseiten-Inhalte

Legen Sie fest, welche zusätzlichen Bereiche auf der Kundenportal-Startseite angezeigt werden.

Eigene Kundenportal-Themes

Laden Sie mehrere Designs als ZIP hoch und aktivieren Sie eines davon global für alle Kunden. Das ZIP muss im Stammverzeichnis eine theme.css enthalten. Zulässig sind CSS, PNG, JPG, GIF, WebP, WOFF und WOFF2; maximal 10 MB. Autor und Version können im CSS-Kopf mit Author: und Version: angegeben werden.

Der Anzeigename wird automatisch aus dem ZIP-Dateinamen übernommen, zum Beispiel Classic-Support-Blue.zip → Classic Support Blue. Ein erneuter Upload mit demselben Namen ersetzt das vorhandene Theme.

Noch keine eigenen Themes hochgeladen.

· Version Aktiv
Von

Themes aus dem Katalog

Fertige Designs direkt installieren. Prüfsumme und Signatur werden vor der Installation geprüft.

Katalog aktualisieren

Katalog aktualisieren
ThemeVersionSignatur
von
installiert:
✓ Signiert' : 'Nicht signiert' ?> ✓ Installiert
Der Katalog enthält keine Themes.

SMTP-Versand

E-Mail-Abruf (IMAP)

Postfächer, aus denen eingehende E-Mails als Tickets importiert werden. Es können mehrere Postfächer angelegt werden (z.B. ein eigenes je Abteilung) – jedes mit eigenem Server, Zugangsdaten und optionaler Standardabteilung.

query('SELECT COUNT(*) FROM imap_accounts WHERE is_active = 1')->fetchColumn(); ?>

0): ?> ● aktive(s) Postfach/Postfächer eingerichtet ○ Noch kein Postfach eingerichtet · Letzter Abruf:

Postfächer verwalten →

Discord & Telegram

Mitarbeiter werden bei neuen Tickets und Antworten benachrichtigt. Ticket-Daten werden dabei an den jeweiligen Drittanbieter übermittelt.

🔒 Webhook-URL und Bot-Token werden verschlüsselt gespeichert. Leer lassen = unverändert. Wert - löscht ihn.

Kanal-Einstellungen → Integrationen → Webhooks → Neuer Webhook

Bot über @BotFather erstellen, Chat-ID via @userinfobot ermitteln.

query('SELECT COUNT(*) FROM push_devices')->fetchColumn(); $__fcmErr = (string) get_setting('fcm_last_error', ''); $__fcmOk = (string) get_setting('fcm_last_ok', ''); $__fcmFromConfig = (string) (config()['fcm']['service_account_file'] ?? '') !== ''; ?>

📱 Android-App: Push-Benachrichtigungen

Popups auf dem Handy bei neuen Tickets, Kundenantworten und Zuweisungen, auch wenn die App geschlossen ist. Die Nachrichten laufen über Google (Firebase Cloud Messaging).

✓ Eigenes Firebase-Projekt ·
✓ Push ist aktiv · über den DeskHub-Push-Dienst, es ist nichts einzurichten
Nicht verfügbar. In dieser Version ist kein Push-Dienst eingetragen. Ihr könnt unten ein eigenes Firebase-Projekt verwenden.
Gerät(e) angemeldet
Letzter Fehler:

Erscheint in jedem Popup neben dem App-Namen („DeskHub“). Den App-Namen selbst kann Android pro Server nicht ändern.

Erlaubte Dateitypen

Gefährliche Typen (PHP, EXE, SH etc.) werden immer blockiert – unabhängig von dieser Liste.

Kunden schicken oft Büro-Dateien per Mail, die im Formular nicht erlaubt sind. Diese Endungen gelten zusätzlich zur Liste oben, aber nur für Mails. Gefährliche Typen bleiben gesperrt.
MB

Datenschutz & Impressum

Externer Link oder HTML-Inhalt (öffnet Popup). Ist ein Link gesetzt, wird das HTML-Feld ignoriert.

🔒 Datenschutzerklärung
📄 Impressum
getAttribute(PDO::ATTR_DRIVER_NAME) === 'sqlite' ? "SELECT COUNT(*) FROM tickets WHERE status IN ('closed','resolved') AND updated_at < datetime('now', '-' || ? || ' months') AND requester_email NOT LIKE 'anon_%'" : "SELECT COUNT(*) FROM tickets WHERE status IN ('closed','resolved') AND updated_at < DATE_SUB(NOW(), INTERVAL ? MONTH) AND requester_email NOT LIKE 'anon_%'"; $previewStmt = $pdo->prepare($previewSql); $previewStmt->execute([$retMonths]); $previewCount = (int)$previewStmt->fetchColumn(); ?>

Automatische Datenlöschung

Geschlossene und gelöste Tickets werden nach der gewählten Zeit automatisch anonymisiert. Name, E-Mail-Adresse und IP werden unkenntlich gemacht — das Ticket und seine Historie bleiben erhalten.

Monate

Zeigt im Kundenprofil die Knöpfe „Alle Daten exportieren“ und „Kundendaten löschen“. Jede Nutzung wird im Audit-Log protokolliert.

Status & Vorschau

Aktuell würden Ticket(s) anonymisiert (geschlossen/gelöst, älter als Monate).

Was wird anonymisiert?

Crontab-Eintrag

0 3 * * * php /pfad/zu/deskhub/cron/anonymize_data.php

🔄 Automatischer Hintergrund-Betrieb (ohne Server-Cronjob)

Läuft komplett eingebaut, ganz ohne echten Server-Cronjob und ohne externen Dienst: bei jeder Anmeldung/Nutzung im Backend und in der App wird im Hintergrund geprüft, ob einer der Jobs unten fällig ist, und automatisch ausgeführt. Solange regelmäßig jemand das Backend oder die App nutzt, laufen alle Jobs von selbst dauerhaft weiter – ganz ohne Einrichtung bei Strato oder einem externen Anbieter.

Status: ● aktiv' : '○ deaktiviert' ?> · Letzte automatische Prüfung:

Cron-Status

Jeder Job kann hier zusätzlich jederzeit sofort manuell gestartet werden.

$cronFetch, 'check_sla' => $cronSla, 'check_escalations' => $cronEscalations, 'process_recurring' => $cronRecurring, 'process_backups' => $cronBackups, 'anonymize_data' => $cronAnonymize, 'auto_close_tickets'=> $cronAutoClose, 'process_snoozes' => $cronSnoozes, 'process_mail_queue' => cron_last_run_label(get_setting('cron_process_mail_queue_last_run')), 'remind_customers' => $cronRemind, 'run_plugins' => $cronPlugins, 'send_csat' => $cronCsat, 'status_checks' => cron_last_run_label(get_setting('cron_status_checks_last_run')), 'send_scheduled_replies' => cron_last_run_label(get_setting('cron_send_scheduled_replies_last_run')), 'send_report_schedules' => cron_last_run_label(get_setting('cron_send_report_schedules_last_run')), ]; ?> $jobMeta): ?>

Echter Server-Cronjob

✅ Dieser Server unterstützt eine automatische Installation (Shell-Zugriff erkannt). DeskHub trägt dafür einen klar markierten Block in die Crontab des Server-Benutzers ein – alle sonstigen vorhandenen Einträge bleiben unangetastet.

Status: ● Installiert ○ Nicht installiert

❌ Auf diesem Server nicht möglich:
Typisch bei Shared-Hosting (z.B. den meisten Strato-Webhosting-Paketen). Der oben aktive automatische Hintergrund-Betrieb deckt das in der Regel bereits ab – alternativ unten die Zeilen manuell im Hosting-Kundenlogin eintragen, falls dort ein Cronjob-Manager verfügbar ist.

Crontab-Zeilen anzeigen