'; } // ───────────────────────────── Ticket anlegen ───────────────────────────── /** Plätze im Formular, die DeskHub für eigene Felder kennt (alles andere kommt als „Zusätzliche Angaben“ ans Ende). */ const SLOTS = ['contact_top', 'contact', 'topic_top', 'topic', 'issue_top', 'issue_mid', 'issue']; /** * @param array|string $atts */ public static function render_ticket_form($atts = []): string { $atts = shortcode_atts([ 'department' => '', 'topic' => '', 'priority' => '', 'topics' => '', 'title' => '', ], is_array($atts) ? $atts : [], 'deskhub_ticket'); self::enqueue(); $client = Deskhub_Tickets_Client::from_settings(); if (!$client) { return current_user_can('manage_options') ? '

' . esc_html__('DeskHub Tickets ist noch nicht eingerichtet: Bitte unter Einstellungen → DeskHub Tickets Server-Adresse und API-Schlüssel eintragen. (Diese Meldung sehen nur Administratoren.)', 'deskhub-tickets') . '

' : ''; } $opts = deskhub_tickets_options(); $topics = self::parse_topics((string) $atts['topics']); $cfg = self::form_config($atts, $opts, $topics); $cfgB64 = base64_encode((string) wp_json_encode($cfg)); $state = self::take_state(); $values = ['name' => '', 'email' => '', 'subject' => '', 'message' => '', 'topic' => 0, 'fields' => []]; if (is_user_logged_in()) { $u = wp_get_current_user(); $values['name'] = (string) $u->display_name; $values['email'] = (string) $u->user_email; } if ($state && !empty($state['values']) && is_array($state['values'])) { $values = array_merge($values, array_intersect_key($state['values'], $values)); } ob_start(); echo self::wrap_open('deskhub-ticket'); if ($state && !empty($state['ok'])) { $text = trim((string) $opts['success_text']) !== '' ? (string) $opts['success_text'] : __('Vielen Dank! Wir melden uns so schnell wie möglich per E-Mail.', 'deskhub-tickets'); echo '

' . esc_html($text) . '

'; if (!empty($state['number'])) { echo '

' . sprintf(esc_html__('Ihre Ticketnummer: %s', 'deskhub-tickets'), '' . esc_html((string) $state['number']) . '') . '

'; } if ($opts['status_page_url'] !== '') { echo '

' . esc_html__('Stand abfragen', 'deskhub-tickets') . '

'; } echo '
'; return (string) ob_get_clean(); } // Kundenformulare aus DeskHub: eines je Thema der Auswahl, sonst eines für das Thema/die Abteilung. Ohne Antwort: einfaches Formular. $forms = []; $unsupported = false; $topicIds = $topics ? array_map('intval', array_column($topics, 'id')) : [(int) $cfg['t']]; foreach ($topicIds as $tid) { $f = $client->get_form($tid ?: null, (int) $cfg['d'] ?: null); if (is_wp_error($f)) { $forms = []; $unsupported = $f->get_error_code() === 'deskhub_form_unsupported'; break; } $forms[$tid] = self::normalize_form($f); } if (!$forms) { $forms = [$topicIds[0] ?? 0 => self::normalize_form(null)]; } $multi = count($topics) > 0; $selected = $multi ? (int) $values['topic'] : (int) array_key_first($forms); $active = $forms[$selected] ?? reset($forms); $texts = []; foreach ($forms as $tid => $fm) { $texts[$tid] = ['title' => $fm['title'], 'intro' => $fm['intro'], 'subjectShow' => $fm['subject']['show'], 'subjectLabel' => $fm['subject']['label'], 'messageShow' => $fm['message']['show'], 'messageLabel' => $fm['message']['label'], 'messageHint' => $fm['message']['hint'], 'submitLabel' => $fm['submitLabel'], 'attachments' => $fm['attachments']['enabled']]; } if ($unsupported && current_user_can('manage_options')) { echo '

' . esc_html__('Der DeskHub-Server kennt die Formular-Schnittstelle noch nicht (bitte DeskHub aktualisieren). Bis dahin wird das einfache Formular ohne eigene Felder gezeigt. (Diese Meldung sehen nur Administratoren.)', 'deskhub-tickets') . '

'; } $title = $atts['title'] !== '' ? (string) $atts['title'] : (string) $active['title']; echo '

' . esc_html($title) . '

'; echo '

' . esc_html((string) $active['intro']) . '

'; if ($state && !empty($state['errors'])) { echo ''; } $showSubject = (bool) $active['subject']['show']; $showMessage = (bool) $active['message']['show']; ?>

> >

>

> >

'; return (string) ob_get_clean(); } /** Abteilung, Thema, Priorität und erlaubte Themen dieses Formulars (reisen unterschrieben im Formular). */ private static function form_config(array $atts, array $opts, array $topics): array { $hasTopic = $topics || $atts['topic'] !== '' || (int) $opts['topic_id'] > 0; // Mit Thema bestimmt DeskHub die Abteilung (Thema und Abteilungsformular gehören zusammen), außer der Shortcode nennt eine ausdrücklich $dept = $atts['department'] !== '' ? absint($atts['department']) : ($hasTopic ? 0 : (int) $opts['department_id']); return [ 'd' => $dept, 't' => $atts['topic'] !== '' ? absint($atts['topic']) : (int) $opts['topic_id'], 'p' => $atts['priority'] !== '' ? absint($atts['priority']) : (int) $opts['priority_id'], 'o' => array_values(array_map('intval', array_column($topics, 'id'))), ]; } /** * Angebot für Dateianhänge aus dem Formular von DeskHub. Ohne Angabe (ältere DeskHub-Version) oder wenn die Option des Plugins * ausgeschaltet ist, gibt es keine Anhänge. * * @param mixed $a * @return array{enabled:bool,maxFiles:int,maxFileBytes:int,maxTotalBytes:int,allowed:string[]} */ public static function normalize_attachments($a): array { $off = ['enabled' => false, 'maxFiles' => 0, 'maxFileBytes' => 0, 'maxTotalBytes' => 0, 'allowed' => []]; $opts = deskhub_tickets_options(); if (!is_array($a) || empty($a['enabled']) || empty($opts['attachments'])) { return $off; } $allowed = []; foreach ((array) ($a['allowedExtensions'] ?? []) as $e) { $e = strtolower(ltrim(trim((string) $e), '.')); if ($e !== '' && preg_match('/^[a-z0-9]{1,8}$/', $e)) { $allowed[] = $e; } } $files = max(0, min(10, (int) ($a['maxFiles'] ?? 0))); $perFile = max(0, (int) ($a['maxFileBytes'] ?? 0)); $total = max(0, (int) ($a['maxTotalBytes'] ?? 0)); if ($files < 1 || $perFile < 1 || $total < 1 || !$allowed) { return $off; } return ['enabled' => true, 'maxFiles' => $files, 'maxFileBytes' => $perFile, 'maxTotalBytes' => $total, 'allowed' => $allowed]; } /** Menschenlesbare Größe ("5 MB"). */ private static function format_bytes(int $b): string { return $b >= 1048576 ? rtrim(rtrim(number_format($b / 1048576, 1, ',', ''), '0'), ',') . ' MB' : max(1, (int) round($b / 1024)) . ' KB'; } /** * Prüft die hochgeladenen Dateien gegen das Angebot und gibt sie fertig für die Schnittstelle zurück. * * @param array $files $_FILES['dht_files'] * @return array{0:string[],1:array} [Fehler, Anhänge] */ public static function collect_attachments(array $files, array $policy): array { $names = isset($files['name']) ? (array) $files['name'] : []; $errors = []; $out = []; $total = 0; $dangerous = ['php', 'php3', 'php4', 'php5', 'php7', 'php8', 'phtml', 'phar', 'exe', 'bat', 'cmd', 'sh', 'js', 'html', 'htm', 'svg']; foreach ($names as $i => $rawName) { $err = (int) ($files['error'][$i] ?? UPLOAD_ERR_NO_FILE); if ($err === UPLOAD_ERR_NO_FILE) { continue; // nichts ausgewählt } $name = sanitize_file_name(wp_unslash((string) $rawName)); if (!$policy['enabled']) { return [[__('Bei diesem Formular können keine Dateien angehängt werden.', 'deskhub-tickets')], []]; } if (count($out) >= $policy['maxFiles']) { $errors[] = sprintf(__('Bitte hängen Sie höchstens %d Dateien an.', 'deskhub-tickets'), $policy['maxFiles']); break; } if ($err !== UPLOAD_ERR_OK) { $errors[] = ($err === UPLOAD_ERR_INI_SIZE || $err === UPLOAD_ERR_FORM_SIZE) ? sprintf(__('Die Datei „%s“ ist zu groß (höchstens %s).', 'deskhub-tickets'), $name, self::format_bytes($policy['maxFileBytes'])) : sprintf(__('Die Datei „%s“ konnte nicht hochgeladen werden.', 'deskhub-tickets'), $name); continue; } $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION)); if ($name === '' || $ext === '' || in_array($ext, $dangerous, true) || !in_array($ext, $policy['allowed'], true)) { $errors[] = sprintf(__('Der Dateityp von „%1$s“ ist nicht erlaubt. Erlaubt: %2$s', 'deskhub-tickets'), $name, implode(', ', $policy['allowed'])); continue; } $size = (int) ($files['size'][$i] ?? 0); $tmp = (string) ($files['tmp_name'][$i] ?? ''); if ($size < 1 || $tmp === '' || !(is_uploaded_file($tmp) || (defined('DESKHUB_TICKETS_TESTS') && is_file($tmp)))) { $errors[] = sprintf(__('Die Datei „%s“ konnte nicht gelesen werden.', 'deskhub-tickets'), $name); continue; } if ($size > $policy['maxFileBytes']) { $errors[] = sprintf(__('Die Datei „%1$s“ ist zu groß (höchstens %2$s).', 'deskhub-tickets'), $name, self::format_bytes($policy['maxFileBytes'])); continue; } $total += $size; if ($total > $policy['maxTotalBytes']) { $errors[] = sprintf(__('Die Dateien sind zusammen zu groß (höchstens %s).', 'deskhub-tickets'), self::format_bytes($policy['maxTotalBytes'])); break; } $content = file_get_contents($tmp); if ($content === false || $content === '') { $errors[] = sprintf(__('Die Datei „%s“ konnte nicht gelesen werden.', 'deskhub-tickets'), $name); continue; } $out[] = ['name' => $name, 'data' => base64_encode($content)]; } return [$errors, $out]; } /** Bringt ein Formular von DeskHub (oder null) in eine feste Form mit Standardwerten, Feldern nach Plätzen sortiert. */ public static function normalize_form(?array $f): array { $own = $f !== null && !empty($f['ownForm']); $str = static function ($v, string $default = ''): string { return is_string($v) && trim($v) !== '' ? trim($v) : $default; }; $out = [ 'own' => $own, 'title' => $f !== null ? $str($f['title'] ?? '') : '', 'intro' => $f !== null ? $str($f['intro'] ?? '') : '', // DeskHub meldet für Standardformulare seine eigenen deutschen Standardtexte; hier gelten die (übersetzbaren) des Plugins 'subject' => [ 'show' => $f === null || !isset($f['subject']['show']) || !empty($f['subject']['show']), 'label' => $own ? $str($f['subject']['label'] ?? '', __('Betreff', 'deskhub-tickets')) : __('Betreff', 'deskhub-tickets'), ], 'message' => [ 'show' => $f === null || !isset($f['message']['show']) || !empty($f['message']['show']), 'label' => $own ? $str($f['message']['label'] ?? '', __('Ihre Nachricht', 'deskhub-tickets')) : __('Ihre Nachricht', 'deskhub-tickets'), 'hint' => $own ? $str($f['message']['hint'] ?? '') : '', ], 'submitLabel' => $own ? $str($f['submitLabel'] ?? '', __('Ticket absenden', 'deskhub-tickets')) : __('Ticket absenden', 'deskhub-tickets'), 'fields' => [], 'attachments' => self::normalize_attachments($f['attachments'] ?? null), ]; $allowed = ['text', 'textarea', 'select', 'radio', 'checkbox', 'number', 'date']; foreach ((array) ($f['fields'] ?? []) as $fd) { if (!is_array($fd) || empty($fd['id']) || !isset($fd['label'])) { continue; } $out['fields'][] = [ 'id' => (int) $fd['id'], 'label' => (string) $fd['label'], 'type' => in_array($fd['type'] ?? '', $allowed, true) ? (string) $fd['type'] : 'text', 'required' => !empty($fd['required']), 'options' => array_values(array_filter(array_map('strval', (array) ($fd['options'] ?? [])), static function ($o) { return $o !== ''; })), 'helpHtml' => (string) ($fd['helpHtml'] ?? ''), 'width' => in_array($fd['width'] ?? '', ['half', 'third', 'twothirds'], true) ? (string) $fd['width'] : 'full', 'section' => (string) ($fd['section'] ?? 'extra'), 'sectionTitle' => (string) ($fd['sectionTitle'] ?? ''), 'radioInline' => !empty($fd['radioInline']), ]; } return $out; } /** * Gibt die Felder eines Platzes im Formular aus: je Thema ein Block (bei mehreren Themen schaltet form.js den passenden ein). * $rest = true: alle Felder, die zu keinem bekannten Platz gehören („Zusätzliche Angaben“ und eigene Abschnitte). * * @param array $forms */ private static function render_slot(array $forms, array $slots, bool $multi, int $selected, array $posted, bool $rest = false): string { $html = ''; foreach ($forms as $tid => $form) { $fields = array_values(array_filter($form['fields'], static function ($f) use ($slots, $rest) { return $rest ? !in_array($f['section'], self::SLOTS, true) : in_array($f['section'], $slots, true); })); if (!$fields) { continue; } $on = !$multi || $tid === $selected; $html .= '
'; $group = null; foreach ($fields as $f) { $heading = $rest ? $f['sectionTitle'] : ''; if ($rest && $heading !== $group) { $html .= '
' . ($heading !== '' ? '

' . esc_html($heading) . '

' : '') . '
'; $group = $heading; } $html .= self::render_field($f, (string) $tid, isset($posted[$f['id']]) ? (string) $posted[$f['id']] : '', !$on); } $html .= '
'; } return $html; } private static function help_html(string $html): string { return wp_kses($html, ['a' => ['href' => [], 'target' => [], 'rel' => []], 'br' => []]); } /** Ein Eingabefeld aus der Beschreibung von DeskHub. */ private static function render_field(array $f, string $uid, string $posted, bool $disabled): string { $id = (int) $f['id']; $name = 'dht_f[' . $id . ']'; $dom = 'dht-f-' . $uid . '-' . $id; $req = !empty($f['required']); $dis = $disabled ? ' disabled' : ''; $reqAttr = $req ? ' required' : ''; $star = $req ? ' ' : ''; $label = esc_html((string) $f['label']); $help = self::help_html((string) $f['helpHtml']); $o = '
'; switch ($f['type']) { case 'textarea': $o .= '' . ''; break; case 'select': $o .= ''; break; case 'radio': $o .= '
' . $label . $star . ''; foreach ($f['options'] as $i => $opt) { $o .= ''; } $o .= '
'; break; case 'checkbox': $o .= ''; $help = ''; break; case 'number': case 'date': $o .= ''; break; default: $o .= ''; } if ($help !== '') { $o .= '

' . $help . '

'; } return $o . '
'; } /** * Prüft die Angaben zu den Feldern des Formulars wie DeskHub selbst (DeskHub prüft beim Anlegen noch einmal). * * @param array $fields * @param array $input Feld-ID => abgeschickter Wert * @return array{0:string[],1:array} Fehlermeldungen und bereinigte Werte (Feld-ID => Wert) */ public static function validate_fields(array $fields, array $input): array { $errors = []; $values = []; foreach ($fields as $f) { $id = (int) $f['id']; $label = (string) $f['label']; $type = (string) $f['type']; $raw = isset($input[$id]) ? (string) $input[$id] : ''; if ($type === 'checkbox') { $val = in_array(strtolower(trim($raw)), ['1', 'on', 'yes', 'ja', 'true'], true) ? '1' : '0'; } else { $val = $type === 'textarea' ? sanitize_textarea_field($raw) : sanitize_text_field($raw); $val = mb_substr($val, 0, 10000); } if (!empty($f['required']) && $type !== 'checkbox' && $val === '') { $errors[] = sprintf(__('Bitte füllen Sie das Feld „%s“ aus.', 'deskhub-tickets'), $label); } elseif (!empty($f['required']) && $type === 'checkbox' && $val !== '1') { $errors[] = sprintf(__('Bitte bestätigen Sie „%s“.', 'deskhub-tickets'), $label); } elseif (in_array($type, ['select', 'radio'], true) && $val !== '' && !in_array($val, $f['options'], true)) { $errors[] = sprintf(__('Bitte wählen Sie bei „%s“ eine der angebotenen Möglichkeiten.', 'deskhub-tickets'), $label); } elseif ($type === 'number' && $val !== '' && !is_numeric($val)) { $errors[] = sprintf(__('Bitte geben Sie bei „%s“ eine Zahl an.', 'deskhub-tickets'), $label); } elseif ($type === 'date' && $val !== '' && !(preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $val, $m) && checkdate((int) $m[2], (int) $m[3], (int) $m[1]))) { $errors[] = sprintf(__('Bitte geben Sie bei „%s“ ein gültiges Datum an.', 'deskhub-tickets'), $label); } $values[$id] = $val; } return [$errors, $values]; } /** Ticket-Text aus den Feldern, wenn das Formular kein Beschreibungsfeld hat (wie im Kundenportal von DeskHub). */ public static function compose_message(array $fields, array $values): string { $lines = []; foreach ($fields as $f) { $v = $values[(int) $f['id']] ?? ''; if ($f['type'] === 'checkbox') { $v = $v === '1' ? __('Ja', 'deskhub-tickets') : __('Nein', 'deskhub-tickets'); } if ($v !== '') { $lines[] = $f['label'] . ': ' . $v; } } return $lines ? implode("\n", $lines) : __('Anfrage ohne weitere Angaben', 'deskhub-tickets'); } /** Verarbeitet das abgeschickte Formular (admin-post.php) und leitet auf die Seite zurück. */ public static function handle_submit(): void { $back = self::safe_back(isset($_POST['dht_back']) ? (string) wp_unslash($_POST['dht_back']) : ''); $opts = deskhub_tickets_options(); // 1) Unterschriebene Angaben prüfen (Abteilung, Thema, Priorität) $cfgB64 = isset($_POST['dht_cfg']) ? (string) wp_unslash($_POST['dht_cfg']) : ''; $sig = isset($_POST['dht_sig']) ? (string) wp_unslash($_POST['dht_sig']) : ''; $cfg = null; if (deskhub_tickets_verify_signature('cfg|' . $cfgB64, $sig)) { $decoded = json_decode((string) base64_decode($cfgB64, true), true); if (is_array($decoded)) { $cfg = $decoded; } } if ($cfg === null) { self::fail($back, [__('Das Formular ist abgelaufen. Bitte laden Sie die Seite neu und versuchen Sie es noch einmal.', 'deskhub-tickets')], []); } // 2) Eingaben bereinigen $postedFields = []; if (isset($_POST['dht_f']) && is_array($_POST['dht_f'])) { foreach ((array) wp_unslash($_POST['dht_f']) as $fid => $v) { if (is_scalar($v)) { $postedFields[absint($fid)] = (string) $v; } } } $values = [ 'name' => mb_substr(sanitize_text_field(isset($_POST['dht_name']) ? (string) wp_unslash($_POST['dht_name']) : ''), 0, 120), 'email' => sanitize_email(isset($_POST['dht_email']) ? (string) wp_unslash($_POST['dht_email']) : ''), 'subject' => mb_substr(sanitize_text_field(isset($_POST['dht_subject']) ? (string) wp_unslash($_POST['dht_subject']) : ''), 0, 255), 'message' => mb_substr(sanitize_textarea_field(isset($_POST['dht_message']) ? (string) wp_unslash($_POST['dht_message']) : ''), 0, self::MAX_MESSAGE), 'topic' => isset($_POST['dht_topic']) ? absint(wp_unslash($_POST['dht_topic'])) : 0, 'fields' => $postedFields, ]; // 3) Falle-Feld: Wer es ausfüllt, ist ein Roboter. Er bekommt eine Erfolgsmeldung, es wird aber nichts angelegt. if (isset($_POST['dht_website']) && trim((string) wp_unslash($_POST['dht_website'])) !== '') { self::succeed($back, ''); } // 4) Zeit seit dem Anzeigen des Formulars $age = deskhub_tickets_timestamp_age(isset($_POST['dht_ts']) ? (string) wp_unslash($_POST['dht_ts']) : ''); $minSeconds = (int) $opts['min_seconds']; if ($age === null || $age < $minSeconds) { self::fail($back, [__('Das ging etwas zu schnell. Bitte prüfen Sie Ihre Angaben und senden Sie noch einmal ab.', 'deskhub-tickets')], $values); } // 5) Thema und Formular (aus DeskHub; ohne Antwort das einfache Formular) $errors = []; $allowedTopics = isset($cfg['o']) && is_array($cfg['o']) ? array_map('intval', $cfg['o']) : []; $topicId = (int) ($cfg['t'] ?? 0); if ($allowedTopics) { if (!in_array($values['topic'], $allowedTopics, true)) { $errors[] = __('Bitte wählen Sie ein Thema aus.', 'deskhub-tickets'); } else { $topicId = $values['topic']; } } $client = Deskhub_Tickets_Client::from_settings(); if (!$client) { self::fail($back, [__('Der Support ist gerade nicht erreichbar. Bitte versuchen Sie es später noch einmal.', 'deskhub-tickets')], $values); } $form = null; if (!$errors) { $remote = $client->get_form($topicId ?: null, (int) ($cfg['d'] ?? 0) ?: null); if (!is_wp_error($remote)) { $form = self::normalize_form($remote); } } $showSubject = $form === null || $form['subject']['show']; $showMessage = $form === null || $form['message']['show']; // 6) Pflichtfelder if ($values['name'] === '') { $errors[] = __('Bitte geben Sie Ihren Namen an.', 'deskhub-tickets'); } if ($values['email'] === '' || !is_email($values['email'])) { $errors[] = __('Bitte geben Sie eine gültige E-Mail-Adresse an.', 'deskhub-tickets'); } if ($showSubject && $values['subject'] === '') { $errors[] = __('Bitte geben Sie einen Betreff an.', 'deskhub-tickets'); } if ($showMessage && trim($values['message']) === '') { $errors[] = __('Bitte beschreiben Sie Ihr Anliegen.', 'deskhub-tickets'); } $fieldValues = []; if ($form !== null) { [$fieldErrors, $fieldValues] = self::validate_fields($form['fields'], $postedFields); $errors = array_merge($errors, $fieldErrors); } // Dateianhänge (nur wenn das Formular sie anbietet) $attachments = []; if (!empty($_FILES['dht_files']) && is_array($_FILES['dht_files'])) { $policy = $form !== null ? $form['attachments'] : self::normalize_attachments(null); [$attErrors, $attachments] = self::collect_attachments($_FILES['dht_files'], $policy); $errors = array_merge($errors, $attErrors); } if (trim((string) $opts['consent_text']) !== '' && empty($_POST['dht_consent'])) { $errors[] = __('Bitte stimmen Sie der Verarbeitung Ihrer Angaben zu.', 'deskhub-tickets'); } if ($errors) { self::fail($back, $errors, $values); } // 7) Captcha und Mengenbegrenzung $captcha = self::verify_turnstile(); if ($captcha !== true) { self::fail($back, [$captcha], $values); } if (!deskhub_tickets_rate_allow('submit', (int) $opts['rate_limit_per_hour'], HOUR_IN_SECONDS)) { self::fail($back, [__('Sie haben in der letzten Stunde schon mehrere Tickets gesendet. Bitte versuchen Sie es später noch einmal.', 'deskhub-tickets')], $values); } // 8) Ticket anlegen $text = $showMessage ? $values['message'] : self::compose_message($form['fields'], $fieldValues); $message = $text . "\n\n— " . sprintf( /* translators: %s: Adresse der Seite mit dem Formular */ __('Gesendet über das Formular auf %s', 'deskhub-tickets'), $back ); $payload = [ 'name' => $values['name'], 'email' => $values['email'], 'subject' => $showSubject ? $values['subject'] : '', // ohne Betrefffeld setzt DeskHub „Anfrage “ 'message' => $message, 'helpTopicId' => $topicId, 'departmentId' => (int) ($cfg['d'] ?? 0), 'priorityId' => (int) ($cfg['p'] ?? 0), ]; if ($form !== null) { $payload['fields'] = $fieldValues; } if ($attachments) { $payload['attachments'] = $attachments; } $payload = apply_filters('deskhub_tickets_payload', $payload, $values); $result = $client->create_ticket($payload); if (is_wp_error($result)) { self::fail($back, [Deskhub_Tickets_Client::user_message($result)], $values); } do_action('deskhub_tickets_created', $result['number'], $payload); self::succeed($back, $result['number']); } // ───────────────────────────── Stand abfragen ───────────────────────────── /** * @param array|string $atts */ public static function render_status_form($atts = []): string { self::enqueue(); if (!Deskhub_Tickets_Client::from_settings()) { return current_user_can('manage_options') ? '

' . esc_html__('DeskHub Tickets ist noch nicht eingerichtet (Einstellungen → DeskHub Tickets). Diese Meldung sehen nur Administratoren.', 'deskhub-tickets') . '

' : ''; } $number = ''; $email = ''; $error = ''; $ticket = null; if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['dht_status_submit'])) { $number = trim(sanitize_text_field((string) wp_unslash($_POST['dht_number'] ?? ''))); $email = sanitize_email((string) wp_unslash($_POST['dht_status_email'] ?? '')); if (isset($_POST['dht_status_website']) && trim((string) wp_unslash($_POST['dht_status_website'])) !== '') { $error = __('Zu dieser Kombination aus Ticketnummer und E-Mail-Adresse wurde nichts gefunden.', 'deskhub-tickets'); } elseif (!preg_match('/^[A-Za-z0-9._-]{1,40}$/', $number) || !is_email($email)) { $error = __('Bitte geben Sie Ihre Ticketnummer und die E-Mail-Adresse an, mit der Sie das Ticket gesendet haben.', 'deskhub-tickets'); } elseif (!deskhub_tickets_rate_allow('status', 10, 10 * MINUTE_IN_SECONDS)) { // Bremst das Durchprobieren von Ticketnummern $error = __('Zu viele Abfragen. Bitte versuchen Sie es in einigen Minuten noch einmal.', 'deskhub-tickets'); } else { $client = Deskhub_Tickets_Client::from_settings(); $res = $client ? $client->get_ticket($number, $email) : new WP_Error('deskhub_config', 'x'); if (is_wp_error($res)) { $error = Deskhub_Tickets_Client::user_message($res); } else { $ticket = $res; } } } ob_start(); echo self::wrap_open('deskhub-status'); if ($error !== '') { echo ''; } if ($ticket) { echo self::render_ticket_status($ticket); } ?>

'; return (string) ob_get_clean(); } /** Zeigt Status und öffentlichen Verlauf eines Tickets. Alles wird maskiert; die Texte kommen vom Server als Klartext. */ public static function render_ticket_status(array $t): string { $labels = [ 'open' => __('Offen', 'deskhub-tickets'), 'pending' => __('Wartet auf Ihre Antwort', 'deskhub-tickets'), 'answered' => __('Beantwortet', 'deskhub-tickets'), 'on_hold' => __('In Bearbeitung', 'deskhub-tickets'), 'resolved' => __('Gelöst', 'deskhub-tickets'), 'closed' => __('Geschlossen', 'deskhub-tickets'), ]; $status = (string) ($t['status'] ?? ''); $fmt = function ($s): string { $ts = strtotime((string) $s); return $ts ? date_i18n(get_option('date_format') . ' ' . get_option('time_format'), $ts) : ''; }; $out = '
'; $out .= '

' . esc_html((string) ($t['subject'] ?? '')) . '

'; $out .= '

' . esc_html($labels[$status] ?? $status) . ' ' . esc_html(sprintf(__('Ticket %s', 'deskhub-tickets'), (string) ($t['number'] ?? ''))) . ' · ' . esc_html(sprintf(__('Zuletzt geändert: %s', 'deskhub-tickets'), $fmt($t['updatedAt'] ?? ''))) . '

'; $messages = isset($t['messages']) && is_array($t['messages']) ? $t['messages'] : []; foreach ($messages as $m) { if (!is_array($m)) { continue; } $mine = ($m['author'] ?? '') === 'customer'; $out .= '
' . '
' . esc_html($mine ? __('Sie', 'deskhub-tickets') : __('Support', 'deskhub-tickets')) . ' · ' . esc_html($fmt($m['createdAt'] ?? '')) . '
' . '
' . nl2br(esc_html((string) ($m['message'] ?? ''))) . '
'; } return $out . '
'; } // ───────────────────────────── Hilfsfunktionen ──────────────────────────── /** „Rechnung:3,Technik:4“ → [['label'=>'Rechnung','id'=>3], …] */ public static function parse_topics(string $spec): array { $out = []; foreach (explode(',', $spec) as $part) { $pos = strrpos($part, ':'); if ($pos === false) { continue; } $label = trim(sanitize_text_field(substr($part, 0, $pos))); $id = absint(trim(substr($part, $pos + 1))); if ($label !== '' && $id > 0) { $out[] = ['label' => $label, 'id' => $id]; } } return $out; } /** Adresse der Seite, auf der das Formular steht (dorthin geht es nach dem Absenden zurück). */ private static function current_url(): string { $url = get_permalink(); return $url ? (string) $url : home_url('/'); } /** Ziel für „zurück zur Seite“: nur Adressen dieser Website. */ private static function safe_back(string $url): string { $url = remove_query_arg('dht', $url); $safe = wp_validate_redirect($url, home_url('/')); return $safe ?: home_url('/'); } /** Merkt sich Fehler oder Erfolg kurz und leitet auf die Seite zurück (kein erneutes Senden beim Neuladen). */ private static function redirect_with_state(string $back, array $state): void { $id = bin2hex(random_bytes(8)); set_transient('dht_state_' . $id, $state, 10 * MINUTE_IN_SECONDS); wp_safe_redirect(add_query_arg('dht', $id, $back) . '#deskhub-ticket'); exit; } private static function fail(string $back, array $errors, array $values): void { // Die Eingaben bleiben erhalten, damit der Text der Nachricht nicht verloren geht self::redirect_with_state($back, ['errors' => $errors, 'values' => $values]); } private static function succeed(string $back, string $number): void { self::redirect_with_state($back, ['ok' => true, 'number' => $number]); } /** Liest den kurz gemerkten Zustand (Fehler/Erfolg) aus der Adresse und löscht ihn. */ private static function take_state(): ?array { $id = isset($_GET['dht']) ? (string) wp_unslash($_GET['dht']) : ''; if (!preg_match('/^[a-f0-9]{16}$/', $id)) { return null; } $state = get_transient('dht_state_' . $id); delete_transient('dht_state_' . $id); return is_array($state) ? $state : null; } // ───────────────────────────── Cloudflare Turnstile ─────────────────────── private static function turnstile_enabled(): bool { $o = deskhub_tickets_options(); return trim((string) $o['turnstile_site']) !== '' && trim((string) $o['turnstile_secret']) !== ''; } private static function turnstile_widget(): string { if (!self::turnstile_enabled()) { return ''; } $site = (string) deskhub_tickets_option('turnstile_site'); wp_enqueue_script('deskhub-turnstile', 'https://challenges.cloudflare.com/turnstile/v0/api.js', [], null, true); return '
'; } /** @return true|string true bei Erfolg, sonst die Meldung für den Besucher */ private static function verify_turnstile() { if (!self::turnstile_enabled()) { return true; } $token = isset($_POST['cf-turnstile-response']) ? (string) wp_unslash($_POST['cf-turnstile-response']) : ''; $fail = __('Die Sicherheitsprüfung ist fehlgeschlagen. Bitte versuchen Sie es noch einmal.', 'deskhub-tickets'); if ($token === '' || strlen($token) > 2048) { return $fail; } $res = wp_remote_post('https://challenges.cloudflare.com/turnstile/v0/siteverify', [ 'timeout' => 10, 'body' => ['secret' => (string) deskhub_tickets_option('turnstile_secret'), 'response' => $token, 'remoteip' => deskhub_tickets_client_ip()], ]); if (is_wp_error($res)) { error_log('DeskHub Tickets: Turnstile nicht erreichbar: ' . $res->get_error_message()); return $fail; } $data = json_decode((string) wp_remote_retrieve_body($res), true); return is_array($data) && !empty($data['success']) ? true : $fail; } }