validate([ 'subject' => ['nullable', 'string', 'max:180'], 'commentBody' => ['nullable', 'string'], 'internal' => ['nullable', 'boolean'], 'parent_id' => [ 'nullable', 'integer', Rule::exists('helpdesk_comments', 'id')->where('ticket_id', $ticket->id), ], 'canned_response_id' => ['nullable', 'integer', Rule::exists('helpdesk_canned_responses', 'id')], 'attachments' => ['nullable', 'array'], 'attachments.*' => ['file', 'max:'.$maxUploadKilobytes, 'mimes:'.implode(',', $allowedMimes)], ]); $user = Auth::user(); abort_if($user === null, 403); $body = trim((string) ($validated['commentBody'] ?? '')); $subject = trim((string) ($validated['subject'] ?? '')); if (filled($validated['canned_response_id'] ?? null)) { $response = CannedResponse::query() ->where('is_active', true) ->findOrFail((int) $validated['canned_response_id']); if ($body === '') { $body = trim($renderer->render($response->body, $ticket, $user)); } if ($subject === '') { $subject = (string) $response->name; } } $body = $this->normaliseHtmlBody($body); $subject = $this->normaliseSubject($subject, $body, filled($validated['parent_id'] ?? null)); validator(['commentBody' => $body], [ 'commentBody' => ['required', 'string', 'min:2'], ])->validate(); $uploadedFiles = $request->file('attachments', []); $uploadedFiles = is_array($uploadedFiles) ? $uploadedFiles : [$uploadedFiles]; $uploadedFiles = array_values(array_filter($uploadedFiles, static fn ($file): bool => $file instanceof \Illuminate\Http\UploadedFile && $file->isValid())); $tickets->comment( ticket: $ticket, user: $user, body: $body, internal: (bool) ($validated['internal'] ?? false) && $this->userCanCreateInternalNote($user), files: $uploadedFiles, parentId: filled($validated['parent_id'] ?? null) ? (int) $validated['parent_id'] : null, subject: $subject, ); return redirect() ->route('helpdesk.tickets.show', $ticket) ->with('ticket-comment-added', filled($validated['parent_id'] ?? null) ? 'Reply posted.' : 'Comment posted.'); } /** * Derive a user-friendly Lotus Notes style response subject. */ private function normaliseSubject(string $subject, string $body, bool $isReply): string { $subject = trim(strip_tags(html_entity_decode($subject, ENT_QUOTES | ENT_HTML5, 'UTF-8'))); if ($subject !== '') { return Str::limit($subject, 180, ''); } $plain = trim(preg_replace('/\s+/', ' ', strip_tags(html_entity_decode($body, ENT_QUOTES | ENT_HTML5, 'UTF-8'))) ?: ''); if ($plain === '') { return $isReply ? 'RE: Response' : 'Response'; } return ($isReply ? 'RE: ' : '').Str::limit($plain, 120, ''); } /** * Keep supported formatting tags but strip scripts and unsafe inline handlers. */ private function normaliseHtmlBody(string $body): string { $body = trim($body); for ($i = 0; $i < 5; $i++) { if (! str_contains($body, '<') && ! str_contains($body, '>') && ! str_contains($body, '&lt;')) { break; } $decoded = html_entity_decode($body, ENT_QUOTES | ENT_HTML5, 'UTF-8'); if ($decoded === $body) { break; } $body = $decoded; } $body = preg_replace('/<\s*(script|style)[^>]*>.*?<\s*\/\s*\1\s*>/is', '', $body) ?: ''; $body = preg_replace('/\s+on[a-z]+\s*=\s*("[^"]*"|\'[^\']*\'|[^\s>]+)/i', '', $body) ?: ''; $body = preg_replace('/javascript\s*:/i', '', $body) ?: ''; $body = trim(strip_tags($body, '