Upload via GUI (396 Dateien)

This commit is contained in:
2026-11-04 07:55:19 +00:00
parent c55cc54ba2
commit 27b3e4dbaf
19 changed files with 1559 additions and 9 deletions
@@ -111,6 +111,59 @@ function public_form_fields_payload(array $fields): array
}
/** Das ganze Formular für die API-Antwort (Texte und Felder). */
/** Grenzen für Dateianhänge über die öffentliche Schnittstelle (die Datei kommt Base64-kodiert im JSON). */
const PUBLIC_ATTACHMENT_MAX_FILES = 5;
const PUBLIC_ATTACHMENT_FILE_BYTES = 5 * 1048576;
const PUBLIC_ATTACHMENT_TOTAL_BYTES = 10 * 1048576;
/** Dürfen Besucher bei diesem Formular Dateien anhängen, und mit welchen Grenzen? (Ein eigenes Formular kann Anhänge abschalten.) */
function public_attachment_policy(?array $own): array
{
$enabled = $own === null || !array_key_exists('attachment_show', $own) || !empty($own['attachment_show']);
return [
'enabled' => $enabled,
'maxFiles' => PUBLIC_ATTACHMENT_MAX_FILES,
'maxFileBytes' => min(PUBLIC_ATTACHMENT_FILE_BYTES, get_max_upload_size()),
'maxTotalBytes' => PUBLIC_ATTACHMENT_TOTAL_BYTES,
'allowedExtensions' => get_allowed_extensions(),
];
}
/**
* Prüft die Dateianhänge einer Anfrage und liefert sie dekodiert: [['name' => …, 'data' => Rohdaten], …]. Geprüft wird alles vor dem
* Anlegen des Tickets (Anzahl, Größe, Endung, Inhalt wie beim Upload im Formular).
*
* @throws AdminModelException mit Code INVALID_ATTACHMENT (die Meldung ist für den Besucher gedacht)
*/
function public_attachments_prepare($raw, ?array $own): array
{
if ($raw === null || $raw === [] || $raw === '') return [];
$policy = public_attachment_policy($own);
if (!is_array($raw) || (count($raw) > 0 && array_keys($raw) !== range(0, count($raw) - 1))) throw new AdminModelException('Die Dateianhänge haben ein ungültiges Format.', 'INVALID_ATTACHMENT');
if (!$policy['enabled']) throw new AdminModelException('Bei diesem Formular können keine Dateien angehängt werden.', 'INVALID_ATTACHMENT');
if (count($raw) > $policy['maxFiles']) throw new AdminModelException('Es können höchstens ' . $policy['maxFiles'] . ' Dateien angehängt werden.', 'INVALID_ATTACHMENT');
$out = [];
$total = 0;
foreach ($raw as $a) {
$name = is_array($a) ? trim((string) ($a['name'] ?? '')) : '';
$b64 = is_array($a) ? (string) ($a['data'] ?? '') : '';
if ($name === '' || mb_strlen($name) > 200 || $b64 === '') throw new AdminModelException('Jeder Anhang braucht einen Namen und Inhalt (Base64).', 'INVALID_ATTACHMENT');
$data = base64_decode(preg_replace('/\s+/', '', $b64) ?? '', true);
if ($data === false || $data === '') throw new AdminModelException('Der Anhang „' . mb_substr($name, 0, 80) . '“ ist nicht lesbar (Base64).', 'INVALID_ATTACHMENT');
if (strlen($data) > $policy['maxFileBytes']) throw new AdminModelException('Der Anhang „' . mb_substr($name, 0, 80) . '“ ist zu groß (höchstens ' . round($policy['maxFileBytes'] / 1048576, 1) . ' MB).', 'INVALID_ATTACHMENT');
$total += strlen($data);
if ($total > $policy['maxTotalBytes']) throw new AdminModelException('Die Anhänge sind zusammen zu groß (höchstens ' . round($policy['maxTotalBytes'] / 1048576) . ' MB).', 'INVALID_ATTACHMENT');
// Name, Endung und Inhalt wie beim Upload im Formular prüfen, ohne etwas zu speichern
$tmp = tempnam(sys_get_temp_dir(), 'dhpub');
if ($tmp === false || file_put_contents($tmp, $data) === false) throw new AdminModelException('Der Anhang konnte nicht verarbeitet werden.', 'INVALID_ATTACHMENT');
$check = handle_ticket_upload(['name' => basename(str_replace(chr(92), '/', $name)), 'type' => '', 'tmp_name' => $tmp, 'error' => UPLOAD_ERR_OK, 'size' => strlen($data)], false, [], true);
@unlink($tmp);
if (isset($check['error'])) throw new AdminModelException(strip_tags((string) $check['error']), 'INVALID_ATTACHMENT');
$out[] = ['name' => $name, 'data' => $data];
}
return $out;
}
function public_form_payload(array $def): array
{
$o = $def['own'];
@@ -125,6 +178,7 @@ function public_form_payload(array $def): array
'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']),
'attachments' => public_attachment_policy($o),
];
}
@@ -221,6 +275,8 @@ function public_ticket_create(PDO $pdo, array $in, string $clientIp, array $opts
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) : [];
// Dateianhänge (Base64 im JSON): ohne "fields" gilt das Standardformular, das Anhänge erlaubt
$attachmentList = public_attachments_prepare($in['attachments'] ?? null, $formDef['own'] ?? null);
$topicId = am_int_or_null($in, 'helpTopicId');
$deptId = am_int_or_null($in, 'departmentId');
@@ -262,6 +318,11 @@ 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]);
$threadId = (int) $pdo->lastInsertId();
foreach ($attachmentList as $att) {
$saved = store_ticket_attachment_data($pdo, $threadId, $att['name'], $att['data']);
if (!$saved['ok']) log_activity($pdo, $ticketId, null, 'Anhang nicht übernommen: ' . mb_substr($att['name'], 0, 80) . ' (' . mb_substr((string) ($saved['error'] ?? ''), 0, 100) . ')');
}
if ($fieldValues) {
$cfStmt = $pdo->prepare('INSERT INTO ticket_field_values (ticket_id, custom_field_id, value) VALUES (?,?,?)');
foreach ($fieldValues as $fieldId => $val) {