fixed tag rendering
This commit is contained in:
@@ -54,10 +54,10 @@ final class TicketCommentStoreController extends Controller
|
||||
->where('is_active', true)
|
||||
->findOrFail((int) $validated['canned_response_id']);
|
||||
|
||||
$body = trim($response->body);
|
||||
$body = trim($renderer->render($response->body, $ticket, $user));
|
||||
}
|
||||
|
||||
$body = $this->normaliseHtmlBody($renderer->render($body, $ticket, $user));
|
||||
$body = $this->normaliseHtmlBody($body);
|
||||
|
||||
validator(['commentBody' => $body], [
|
||||
'commentBody' => ['required', 'string', 'min:2'],
|
||||
@@ -84,12 +84,40 @@ final class TicketCommentStoreController extends Controller
|
||||
*/
|
||||
private function normaliseHtmlBody(string $body): string
|
||||
{
|
||||
$body = html_entity_decode($body, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||
$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, '<p><br><strong><b><em><i><u><ul><ol><li><blockquote><pre><code><a>'));
|
||||
|
||||
return trim(strip_tags($body, '<p><br><strong><b><em><i><u><ul><ol><li><blockquote><pre><code><a>'));
|
||||
if ($body === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
if ($body === strip_tags($body)) {
|
||||
return collect(preg_split('/\R{2,}/', $body) ?: [])
|
||||
->map(static fn (string $paragraph): string => trim($paragraph))
|
||||
->filter(static fn (string $paragraph): bool => $paragraph !== '')
|
||||
->map(static fn (string $paragraph): string => '<p>'.nl2br(e($paragraph), false).'</p>')
|
||||
->implode('');
|
||||
}
|
||||
|
||||
return $body;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -133,6 +133,36 @@
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@php
|
||||
$cannedResponseBodies = $cannedResponses->mapWithKeys(function ($response) use ($ticket, $currentUser) {
|
||||
$body = (string) $response->body;
|
||||
|
||||
$body = strtr($body, [
|
||||
'{{ ticket.reference }}' => e((string) ($ticket->reference ?? '')),
|
||||
'{{ ticket.subject }}' => e((string) ($ticket->subject ?? '')),
|
||||
'{{ ticket.status }}' => e((string) ($ticket->status?->name ?? '')),
|
||||
'{{ ticket.priority }}' => e((string) ($ticket->priority?->name ?? '')),
|
||||
'{{ ticket.category }}' => e((string) ($ticket->category?->name ?? '')),
|
||||
'{{ ticket.project }}' => e((string) ($ticket->project?->title ?? '')),
|
||||
'{{ ticket.team }}' => e((string) ($ticket->team?->name ?? '')),
|
||||
'{{ requester.name }}' => e((string) ($ticket->owner?->name ?? $ticket->user?->name ?? '')),
|
||||
'{{ agent.name }}' => e((string) ($ticket->agent?->name ?? $currentUser?->name ?? '')),
|
||||
'{{ actor.name }}' => e((string) ($currentUser?->name ?? '')),
|
||||
'{{ ticket.url }}' => e(route('helpdesk.tickets.show', ['ticket' => $ticket->getKey()])),
|
||||
]);
|
||||
|
||||
$body = html_entity_decode($body, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||
$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 = strip_tags($body, '<p><br><strong><b><em><i><u><ul><ol><li><blockquote><pre><code><a>');
|
||||
|
||||
return [(string) $response->id => trim($body)];
|
||||
})->all();
|
||||
@endphp
|
||||
|
||||
<script type="application/json" id="helpdesk-canned-response-bodies">@json($cannedResponseBodies)</script>
|
||||
|
||||
@if ($cannedResponses->isNotEmpty())
|
||||
<div class="mb-4 grid gap-3 md:grid-cols-[1fr_auto]">
|
||||
<div>
|
||||
@@ -140,7 +170,7 @@
|
||||
<select id="ticket-canned-response" name="canned_response_id" class="hd-select block w-full rounded-xl border border-zinc-300 bg-white p-2.5 text-sm font-semibold text-zinc-900 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100" data-canned-response-select>
|
||||
<option value="">Choose a saved response...</option>
|
||||
@foreach ($cannedResponses as $response)
|
||||
<option value="{{ $response->id }}" data-body="{{ e($response->body) }}">{{ $response->name }}</option>
|
||||
<option value="{{ $response->id }}">{{ $response->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
@@ -212,12 +242,11 @@
|
||||
const editor = form?.querySelector('[data-helpdesk-rich-editor]');
|
||||
const textarea = form?.querySelector('textarea[name="commentBody"]');
|
||||
const selected = select?.selectedOptions?.[0];
|
||||
const body = normaliseCannedHtml(selected?.dataset?.body || '');
|
||||
const cannedResponses = getCannedResponseBodies();
|
||||
const body = normaliseCannedHtml(cannedResponses[String(selected?.value || '')] || selected?.dataset?.body || '');
|
||||
|
||||
if (editor && textarea && body.trim() !== '') {
|
||||
editor.innerHTML = editor.innerHTML.trim() === ''
|
||||
? body
|
||||
: editor.innerHTML.replace(/\s+$/, '') + '<br><br>' + body;
|
||||
insertHtmlAtEnd(editor, body);
|
||||
syncRichEditor(editor);
|
||||
editor.focus();
|
||||
}
|
||||
@@ -343,6 +372,33 @@
|
||||
collapseButton.classList.toggle('inline-flex', hasExpandedComments && ! hasExpandableComments);
|
||||
}
|
||||
|
||||
function getCannedResponseBodies() {
|
||||
const source = document.getElementById('helpdesk-canned-response-bodies');
|
||||
|
||||
if (! source) {
|
||||
return {};
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(source.textContent || '{}') || {};
|
||||
} catch (error) {
|
||||
console.error('Unable to read canned response bodies.', error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function insertHtmlAtEnd(editor, html) {
|
||||
const current = editor.innerHTML.trim();
|
||||
const cleaned = html.trim();
|
||||
|
||||
if (current === '' || current === '<br>') {
|
||||
editor.innerHTML = cleaned;
|
||||
return;
|
||||
}
|
||||
|
||||
editor.innerHTML = current + '<p><br></p>' + cleaned;
|
||||
}
|
||||
|
||||
function decodeHtmlEntities(value) {
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.innerHTML = value;
|
||||
@@ -350,9 +406,13 @@
|
||||
}
|
||||
|
||||
function normaliseCannedHtml(value) {
|
||||
let decoded = decodeHtmlEntities(value);
|
||||
let decoded = decodeHtmlEntities(value || '');
|
||||
|
||||
for (let i = 0; i < 5; i += 1) {
|
||||
if (! decoded.includes('<') && ! decoded.includes('>') && ! decoded.includes('&lt;')) {
|
||||
break;
|
||||
}
|
||||
|
||||
while (decoded.includes('<') || decoded.includes('>') || decoded.includes('&lt;')) {
|
||||
const next = decodeHtmlEntities(decoded);
|
||||
|
||||
if (next === decoded) {
|
||||
@@ -362,7 +422,22 @@
|
||||
decoded = next;
|
||||
}
|
||||
|
||||
return decoded;
|
||||
decoded = decoded.trim();
|
||||
|
||||
if (decoded === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (/<[a-z][\s\S]*>/i.test(decoded)) {
|
||||
return decoded;
|
||||
}
|
||||
|
||||
return decoded
|
||||
.split(/\n{2,}/)
|
||||
.map((paragraph) => paragraph.trim())
|
||||
.filter((paragraph) => paragraph !== '')
|
||||
.map((paragraph) => '<p>' + escapeHtml(paragraph).replace(/\n/g, '<br>') + '</p>')
|
||||
.join('');
|
||||
}
|
||||
|
||||
function syncRichEditor(editor) {
|
||||
|
||||
@@ -64,7 +64,7 @@ final class HelpdeskTicketBrowserTest extends DuskTestCase
|
||||
'name' => 'Browser canned response',
|
||||
'slug' => 'browser-canned-response',
|
||||
'visibility' => 'agents',
|
||||
'body' => '<p>Inserted canned browser response</p>',
|
||||
'body' => '<p>Hello {{ requester.name }}</p><p>Inserted canned browser response for {{ ticket.reference }}</p>',
|
||||
'is_active' => true,
|
||||
'sort_order' => 1,
|
||||
]);
|
||||
@@ -76,19 +76,19 @@ final class HelpdeskTicketBrowserTest extends DuskTestCase
|
||||
->assertSee('Browser response ticket')
|
||||
->select('canned_response_id', (string) $cannedResponse->id)
|
||||
->press('@insert-canned-response')
|
||||
->assertInputValue('commentBody', '<p>Inserted canned browser response</p>');
|
||||
|
||||
$this->setHelpdeskEditor($browser, 'ticket-comment-editor', '<p>First browser response</p>');
|
||||
|
||||
$browser
|
||||
->waitForText('Hello Response Browser User', 10)
|
||||
->assertSee('Inserted canned browser response')
|
||||
->assertDontSee('<p>Hello')
|
||||
->assertInputValue('commentBody', '<p>Hello Response Browser User</p><p>Inserted canned browser response for '.$ticket->reference.'</p>')
|
||||
->press('@post-comment')
|
||||
->waitForText('First browser response', 10)
|
||||
->assertSee('First browser response');
|
||||
->waitForText('Hello Response Browser User', 10)
|
||||
->assertSee('Inserted canned browser response')
|
||||
->assertDontSee('<p>Hello');
|
||||
|
||||
$comment = Comment::query()
|
||||
->where('ticket_id', $ticket->id)
|
||||
->whereNull('parent_id')
|
||||
->where('body', '<p>First browser response</p>')
|
||||
->where('body', 'like', '%Hello Response Browser User%')
|
||||
->firstOrFail();
|
||||
|
||||
$browser->press('@reply-to-comment-'.$comment->id)
|
||||
@@ -102,13 +102,13 @@ final class HelpdeskTicketBrowserTest extends DuskTestCase
|
||||
$this->assertDatabaseHas('helpdesk_comments', [
|
||||
'ticket_id' => $ticket->id,
|
||||
'parent_id' => null,
|
||||
'body' => '<p>First browser response</p>',
|
||||
'body' => '<p>Hello Response Browser User</p><p>Inserted canned browser response for '.$ticket->reference.'</p>',
|
||||
]);
|
||||
|
||||
$parentId = Comment::query()
|
||||
->where('ticket_id', $ticket->id)
|
||||
->whereNull('parent_id')
|
||||
->where('body', '<p>First browser response</p>')
|
||||
->where('body', 'like', '%Inserted canned browser response%')
|
||||
->value('id');
|
||||
|
||||
$this->assertDatabaseHas('helpdesk_comments', [
|
||||
|
||||
Reference in New Issue
Block a user