Upload via GUI (364 Dateien)

This commit is contained in:
2026-11-01 11:15:37 +00:00
parent 0945d61744
commit 9b1b7c0d6e
8 changed files with 262 additions and 27 deletions
+159 -1
View File
@@ -49,8 +49,138 @@ function api_key_verify(PDO $pdo, string $key, string $ip): ?array
return $row;
}
// ── Ticket über die öffentliche API anlegen ─────────────────────────────────
// ── Kundenformular für Fremdsysteme (eigene Felder, Formular der Abteilung) ──────────────────
/**
* Das Kundenformular für ein Hilfethema oder eine Abteilung, wie es ticket_new.php zeigt: Hat die Abteilung ein eigenes Formular,
* gelten dessen Texte und Felder; sonst die allgemeinen Felder und die des Hilfethemas.
*
* @return array{topicId:?int,departmentId:?int,own:?array,fields:array}
* @throws AdminModelException wenn das Hilfethema nicht existiert
*/
function public_form_definition(PDO $pdo, ?int $topicId, ?int $deptId): array
{
if ($topicId !== null) {
$t = $pdo->prepare('SELECT * FROM help_topics WHERE id = ? AND is_active = 1');
$t->execute([$topicId]);
$topic = $t->fetch();
if (!$topic) throw new AdminModelException('Das Hilfethema existiert nicht oder ist deaktiviert.', 'INVALID_TOPIC');
$deptId = $deptId ?? (int) $topic['department_id'];
}
$own = $deptId ? department_form($pdo, $deptId) : null;
if ($own) {
$fields = $own['fields'];
} else {
$all = $pdo->query('SELECT * FROM custom_fields WHERE is_active = 1 AND department_id IS NULL ORDER BY sort_order, label')->fetchAll();
$fields = array_values(array_filter($all, fn(array $f) => $f['help_topic_id'] === null || ($topicId !== null && (int) $f['help_topic_id'] === $topicId)));
}
return ['topicId' => $topicId, 'departmentId' => $deptId, 'own' => $own, 'fields' => $fields];
}
/** Die Felder eines Formulars für die API-Antwort. Hinweistexte kommen auch als sicheres HTML (Links mit absoluter Adresse). */
function public_form_fields_payload(array $fields): array
{
$base = base_url();
$out = [];
foreach ($fields as $f) {
$type = in_array($f['field_type'], ['text', 'textarea', 'select', 'radio', 'checkbox', 'number', 'date'], true) ? $f['field_type'] : 'text';
$options = [];
if (in_array($type, ['select', 'radio'], true)) {
foreach (preg_split('/\r?\n/', trim((string) ($f['options'] ?? ''))) ?: [] as $o) {
$o = trim($o);
if ($o !== '') $options[] = $o;
}
}
$help = (string) ($f['help_text'] ?? '');
$html = $help !== '' ? str_replace('href="/', 'href="' . $base . '/', form_help_html($help, (string) $f['label'])) : '';
$out[] = [
'id' => (int) $f['id'],
'label' => (string) $f['label'],
'type' => $type,
'required' => !empty($f['is_required']),
'options' => $options,
'help' => $help,
'helpHtml' => $html,
'width' => in_array($f['form_width'] ?? '', ['half', 'third', 'twothirds'], true) ? $f['form_width'] : 'full',
'section' => (string) ($f['_section'] ?? 'extra'),
'sectionTitle' => (string) ($f['_section_title'] ?? ''),
'radioInline' => !empty($f['radio_inline']),
];
}
return $out;
}
/** Das ganze Formular für die API-Antwort (Texte und Felder). */
function public_form_payload(array $def): array
{
$o = $def['own'];
return [
'helpTopicId' => $def['topicId'],
'departmentId' => $def['departmentId'],
'ownForm' => $o !== null,
'title' => (string) ($o['title'] ?? ''),
'intro' => (string) ($o['intro'] ?? ''),
'subject' => ['show' => $o ? (bool) $o['subject_show'] : true, 'label' => (string) ($o['subject_label'] ?? 'Betreff')],
'message' => ['show' => $o ? (bool) ($o['message_show'] ?? true) : true, 'label' => (string) ($o['message_label'] ?? 'Beschreibung'),
'hint' => (string) ($o['message_hint'] ?? 'Beschreiben Sie Ihr Anliegen so detailliert wie möglich…')],
'submitLabel' => (string) ($o['submit_label'] ?? 'Ticket erstellen'),
'fields' => public_form_fields_payload($def['fields']),
];
}
/** Wert eines Feldes aus der Anfrage, in der Form, wie ticket_new.php ihn speichert (Checkbox: "1" oder "0"). */
function public_form_field_value(array $f, array $input): string
{
$raw = $input[(int) $f['id']] ?? $input[(string) $f['id']] ?? null;
if ($f['field_type'] === 'checkbox') {
$norm = is_string($raw) ? strtolower(trim($raw)) : $raw;
return in_array($norm, [true, 1, '1', 'on', 'yes', 'ja', 'true'], true) ? '1' : '0';
}
return is_scalar($raw) ? trim((string) $raw) : '';
}
/**
* Prüft die gelieferten Feldwerte gegen das Formular (Pflichtfelder, Auswahlwerte, Zahl, Datum) wie ticket_new.php.
*
* @return array<int,string> Feld-ID => Wert
* @throws AdminModelException mit Code INVALID_FIELD (die Meldung ist für den Besucher gedacht)
*/
function public_form_validate_fields(array $fields, array $input): array
{
$values = [];
foreach ($fields as $f) {
$val = public_form_field_value($f, $input);
$label = (string) $f['label'];
$type = $f['field_type'];
if (mb_strlen($val) > 10000) throw new AdminModelException('Die Angabe bei „' . $label . '“ ist zu lang.', 'INVALID_FIELD');
if (!empty($f['is_required']) && $type !== 'checkbox' && $val === '') throw new AdminModelException('Bitte füllen Sie das Feld „' . $label . '“ aus.', 'INVALID_FIELD');
if (!empty($f['is_required']) && $type === 'checkbox' && $val !== '1') throw new AdminModelException('Bitte bestätigen Sie „' . $label . '“.', 'INVALID_FIELD');
if (in_array($type, ['select', 'radio'], true) && $val !== '' && !in_array($val, array_map('trim', preg_split('/\r?\n/', (string) $f['options']) ?: []), true)) {
throw new AdminModelException('Bitte wählen Sie bei „' . $label . '“ eine der angebotenen Möglichkeiten.', 'INVALID_FIELD');
}
if ($type === 'number' && $val !== '' && !is_numeric($val)) throw new AdminModelException('Bitte geben Sie bei „' . $label . '“ eine Zahl an.', 'INVALID_FIELD');
if ($type === 'date' && $val !== '' && !(preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $val, $d) && checkdate((int) $d[2], (int) $d[3], (int) $d[1]))) {
throw new AdminModelException('Bitte geben Sie bei „' . $label . '“ ein gültiges Datum an (JJJJ-MM-TT).', 'INVALID_FIELD');
}
$values[(int) $f['id']] = $val;
}
return $values;
}
/**
* Ist der Schlüssel gültig, wird aber nur wegen der IP-Beschränkung abgelehnt? So kann die API dem Aufrufer sagen, welche Adresse
* DeskHub sieht (hinter einem Proxy oder Cloudflare ist das oft nicht die erwartete). Verrät nichts, was der Aufrufer mit dem Schlüssel nicht ohnehin weiß.
*/
function api_key_ip_blocked(PDO $pdo, string $key, string $ip): bool
{
if (!preg_match('/^dh_[a-f0-9]{48}$/', $key)) return false;
$stmt = $pdo->prepare('SELECT ip_whitelist FROM api_keys WHERE key_hash = ? AND is_active = 1');
$stmt->execute([hash('sha256', $key)]);
$row = $stmt->fetch();
return $row && !empty($row['ip_whitelist']) && !in_array($ip, explode(',', $row['ip_whitelist']), true);
}
// ── Ticket über die öffentliche API anlegen ─────────────────────────────────
/**
* Legt ein Ticket für einen Fremdsystem-Aufruf an (Quelle "api"), wendet Ticket-Regeln und SLA-Pläne an,
* benachrichtigt Mitarbeiter und Kunde und löst den Webhook "ticket.created" aus.
@@ -64,11 +194,33 @@ function public_ticket_create(PDO $pdo, array $in, string $clientIp, array $opts
$email = trim((string) ($in['email'] ?? ''));
$subject = trim((string) ($in['subject'] ?? ''));
$message = trim((string) ($in['message'] ?? ''));
// Mit "fields" liefert der Aufrufer die Angaben eines Kundenformulars (eigene Felder, Formular der Abteilung). Ohne "fields" bleibt alles wie bisher.
$formDef = null;
$fieldInput = [];
if (isset($in['fields']) && is_array($in['fields'])) {
$fieldInput = $in['fields'];
$preTopic = am_int_or_null($in, 'helpTopicId');
$preDept = am_int_or_null($in, 'departmentId');
$formDef = public_form_definition($pdo, $preTopic, $preTopic === null ? ($preDept ?: (int) get_setting('default_department_id', '1')) : $preDept);
$own = $formDef['own'];
if ($own && !$own['subject_show'] && $subject === '') $subject = 'Anfrage ' . $own['department_name'];
if ($own && empty($own['message_show']) && $message === '') {
// Ohne Beschreibungsfeld besteht der Ticket-Text aus den Angaben der eigenen Felder
$lines = [];
foreach ($formDef['fields'] as $ff) {
$v = public_form_field_value($ff, $fieldInput);
if ($ff['field_type'] === 'checkbox') $v = $v === '1' ? 'Ja' : 'Nein';
if ($v !== '') $lines[] = $ff['label'] . ': ' . $v;
}
$message = $lines ? implode("\n", $lines) : 'Anfrage ohne weitere Angaben';
}
}
if ($name === '' || mb_strlen($name) > 120) throw new AdminModelException('Bitte einen Namen angeben (höchstens 120 Zeichen).', 'INVALID_NAME');
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) throw new AdminModelException('Bitte eine gültige E-Mail-Adresse angeben.', 'INVALID_EMAIL');
if ($subject === '' || mb_strlen($subject) > 255) throw new AdminModelException('Bitte einen Betreff angeben (höchstens 255 Zeichen).', 'INVALID_SUBJECT');
if ($message === '' || mb_strlen($message) > 50000) throw new AdminModelException('Bitte eine Nachricht angeben (höchstens 50.000 Zeichen).', 'INVALID_MESSAGE');
if (is_banned($email)) throw new AdminModelException('Diese Absenderadresse ist gesperrt.', 'BANNED', 403);
$fieldValues = $formDef ? public_form_validate_fields($formDef['fields'], $fieldInput) : [];
$topicId = am_int_or_null($in, 'helpTopicId');
$deptId = am_int_or_null($in, 'departmentId');
@@ -110,6 +262,12 @@ function public_ticket_create(PDO $pdo, array $in, string $clientIp, array $opts
$ticketId = (int) $pdo->lastInsertId();
$pdo->prepare('INSERT INTO ticket_threads (ticket_id, author_type, message, is_internal_note, created_at) VALUES (?,?,?,0,?)')
->execute([$ticketId, 'customer', $message, $now]);
if ($fieldValues) {
$cfStmt = $pdo->prepare('INSERT INTO ticket_field_values (ticket_id, custom_field_id, value) VALUES (?,?,?)');
foreach ($fieldValues as $fieldId => $val) {
if ($val !== '') $cfStmt->execute([$ticketId, $fieldId, $val]);
}
}
log_activity($pdo, $ticketId, null, (string) ($opts['activity'] ?? 'Ticket über die API erstellt'));
if (!empty($opts['tag'])) apply_tag_to_ticket($pdo, $ticketId, (string) $opts['tag']);