Files
relaticle-comments/tests/Feature/ContentSanitizationTest.php
ilyapashayan 541d11ab90 fix: preserve mention data attributes through HTML sanitization
Filament's sanitizer strips data-id, data-label and data-char from
mention spans, breaking both display (unstyled @mention) and editing
(@-only shown in RichEditor). Register a package-scoped sanitizer that
explicitly allows these attributes on span elements.

Also fix double-replacement bug in renderBodyWithMentions() where both
the rich-editor regex and str_replace fallback could run on the same
mention, producing nested styled spans.
2026-04-01 01:10:05 +04:00

193 lines
6.6 KiB
PHP

<?php
use Livewire\Livewire;
use Relaticle\Comments\Livewire\Comments;
use Relaticle\Comments\Models\Comment;
use Relaticle\Comments\Tests\Models\Post;
use Relaticle\Comments\Tests\Models\User;
it('strips script tags from comment body on create', function () {
$user = User::factory()->create();
$post = Post::factory()->create();
$comment = Comment::factory()->create([
'commentable_id' => $post->id,
'commentable_type' => $post->getMorphClass(),
'commenter_id' => $user->getKey(),
'commenter_type' => $user->getMorphClass(),
'body' => '<p>Hello</p><script>alert(1)</script>',
]);
expect($comment->body)->not->toContain('<script>');
expect($comment->body)->toContain('<p>Hello</p>');
});
it('strips event handler attributes from comment body', function () {
$user = User::factory()->create();
$post = Post::factory()->create();
$comment = Comment::factory()->create([
'commentable_id' => $post->id,
'commentable_type' => $post->getMorphClass(),
'commenter_id' => $user->getKey(),
'commenter_type' => $user->getMorphClass(),
'body' => '<img onerror="alert(1)" src="x">',
]);
expect($comment->body)->not->toContain('onerror');
expect($comment->body)->toContain('src="x"');
});
it('strips style tags from comment body', function () {
$user = User::factory()->create();
$post = Post::factory()->create();
$comment = Comment::factory()->create([
'commentable_id' => $post->id,
'commentable_type' => $post->getMorphClass(),
'commenter_id' => $user->getKey(),
'commenter_type' => $user->getMorphClass(),
'body' => '<p>Hi</p><style>body{display:none}</style>',
]);
expect($comment->body)->not->toContain('<style>');
expect($comment->body)->toContain('<p>Hi</p>');
});
it('strips iframe tags from comment body', function () {
$user = User::factory()->create();
$post = Post::factory()->create();
$comment = Comment::factory()->create([
'commentable_id' => $post->id,
'commentable_type' => $post->getMorphClass(),
'commenter_id' => $user->getKey(),
'commenter_type' => $user->getMorphClass(),
'body' => '<p>Hi</p><iframe src="evil.com"></iframe>',
]);
expect($comment->body)->not->toContain('<iframe');
expect($comment->body)->toContain('<p>Hi</p>');
});
it('preserves safe HTML formatting through sanitization', function () {
$user = User::factory()->create();
$post = Post::factory()->create();
$safeHtml = '<p>Hello <strong>bold</strong> and <em>italic</em> text</p>'
.'<a href="https://example.com">link</a>'
.'<ul><li>item one</li><li>item two</li></ul>'
.'<pre><code>echo "hello";</code></pre>'
.'<blockquote>quoted text</blockquote>'
.'<h1>heading one</h1>'
.'<h2>heading two</h2>';
$comment = Comment::factory()->create([
'commentable_id' => $post->id,
'commentable_type' => $post->getMorphClass(),
'commenter_id' => $user->getKey(),
'commenter_type' => $user->getMorphClass(),
'body' => $safeHtml,
]);
expect($comment->body)->toContain('<strong>bold</strong>');
expect($comment->body)->toContain('<em>italic</em>');
expect($comment->body)->toContain('<a href="https://example.com">link</a>');
expect($comment->body)->toContain('<ul>');
expect($comment->body)->toContain('<li>item one</li>');
expect($comment->body)->toContain('<pre><code>');
expect($comment->body)->toContain('<blockquote>quoted text</blockquote>');
expect($comment->body)->toContain('<h1>heading one</h1>');
expect($comment->body)->toContain('<h2>heading two</h2>');
});
it('sanitizes comment body on update', function () {
$user = User::factory()->create();
$post = Post::factory()->create();
$comment = Comment::factory()->create([
'commentable_id' => $post->id,
'commentable_type' => $post->getMorphClass(),
'commenter_id' => $user->getKey(),
'commenter_type' => $user->getMorphClass(),
'body' => '<p>Clean content</p>',
]);
$comment->update([
'body' => '<p>Updated</p><script>document.cookie</script>',
]);
$comment->refresh();
expect($comment->body)->not->toContain('<script>');
expect($comment->body)->toContain('<p>Updated</p>');
});
it('strips javascript protocol from link href', function () {
$user = User::factory()->create();
$post = Post::factory()->create();
$comment = Comment::factory()->create([
'commentable_id' => $post->id,
'commentable_type' => $post->getMorphClass(),
'commenter_id' => $user->getKey(),
'commenter_type' => $user->getMorphClass(),
'body' => '<a href="javascript:alert(1)">click me</a>',
]);
expect($comment->body)->not->toContain('javascript:');
expect($comment->body)->toContain('click me');
});
it('strips onclick handler from elements', function () {
$user = User::factory()->create();
$post = Post::factory()->create();
$comment = Comment::factory()->create([
'commentable_id' => $post->id,
'commentable_type' => $post->getMorphClass(),
'commenter_id' => $user->getKey(),
'commenter_type' => $user->getMorphClass(),
'body' => '<div onclick="alert(1)">click me</div>',
]);
expect($comment->body)->not->toContain('onclick');
expect($comment->body)->toContain('click me');
});
it('preserves mention data attributes in comment body', function () {
$user = User::factory()->create();
$post = Post::factory()->create();
$body = '<span data-type="mention" data-id="1" data-label="max" data-char="@">@max</span>';
$comment = Comment::factory()->create([
'commentable_id' => $post->id,
'commentable_type' => $post->getMorphClass(),
'commenter_id' => $user->getKey(),
'commenter_type' => $user->getMorphClass(),
'body' => $body,
]);
expect($comment->body)->toContain('data-type="mention"');
expect($comment->body)->toContain('data-id="1"');
expect($comment->body)->toContain('data-label="max"');
expect($comment->body)->toContain('data-char="@"');
});
it('sanitizes content submitted through livewire component', function () {
$user = User::factory()->create();
$post = Post::factory()->create();
$this->actingAs($user);
Livewire::test(Comments::class, ['model' => $post])
->set('commentData.body', '<p>Hello</p><script>alert("xss")</script>')
->call('addComment');
$comment = Comment::first();
expect($comment->body)->not->toContain('<script>');
expect($comment->body)->toContain('<p>Hello</p>');
});