Comments: Added wysiwyg link selector, updated tests, removed command

- Updated existing tests with recent back-end changes, mainly to use
  HTML data.
- Removed old comment regen command that's no longer required.
This commit is contained in:
Dan Brown 2024-01-31 14:22:04 +00:00
parent adf0baebb9
commit e9a19d5878
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
7 changed files with 58 additions and 120 deletions

View File

@ -1,49 +0,0 @@
<?php
namespace BookStack\Console\Commands;
use BookStack\Activity\CommentRepo;
use BookStack\Activity\Models\Comment;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class RegenerateCommentContentCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'bookstack:regenerate-comment-content
{--database= : The database connection to use}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Regenerate the stored HTML of all comments';
/**
* Execute the console command.
*/
public function handle(CommentRepo $commentRepo): int
{
$connection = DB::getDefaultConnection();
if ($this->option('database') !== null) {
DB::setDefaultConnection($this->option('database'));
}
Comment::query()->chunk(100, function ($comments) use ($commentRepo) {
foreach ($comments as $comment) {
$comment->html = $commentRepo->commentToHtml($comment->text);
$comment->save();
}
});
DB::setDefaultConnection($connection);
$this->comment('Comment HTML content has been regenerated');
return 0;
}
}

View File

@ -27,6 +27,7 @@ class CommentFactory extends Factory
'html' => $html, 'html' => $html,
'text' => $text, 'text' => $text,
'parent_id' => null, 'parent_id' => null,
'local_id' => 1,
]; ];
} }
} }

View File

@ -40,6 +40,9 @@
<script src="{{ versioned_asset('libs/tinymce/tinymce.min.js') }}" nonce="{{ $cspNonce }}"></script> <script src="{{ versioned_asset('libs/tinymce/tinymce.min.js') }}" nonce="{{ $cspNonce }}"></script>
@include('form.editor-translations') @include('form.editor-translations')
@endpush @endpush
@push('post-app-html')
@include('entities.selector-popup')
@endpush
@endif @endif
</section> </section>

View File

@ -68,7 +68,7 @@
</div> </div>
@yield('bottom') @yield('bottom')
@stack('post-app-html')
@if($cspNonce ?? false) @if($cspNonce ?? false)
<script src="{{ versioned_asset('dist/app.js') }}" nonce="{{ $cspNonce }}"></script> <script src="{{ versioned_asset('dist/app.js') }}" nonce="{{ $cspNonce }}"></script>

View File

@ -196,7 +196,7 @@ class WatchTest extends TestCase
$notifications = Notification::fake(); $notifications = Notification::fake();
$this->asAdmin()->post("/comment/{$entities['page']->id}", [ $this->asAdmin()->post("/comment/{$entities['page']->id}", [
'text' => 'My new comment' 'html' => '<p>My new comment</p>'
]); ]);
$notifications->assertSentTo($editor, CommentCreationNotification::class); $notifications->assertSentTo($editor, CommentCreationNotification::class);
} }
@ -217,12 +217,12 @@ class WatchTest extends TestCase
$notifications = Notification::fake(); $notifications = Notification::fake();
$this->actingAs($editor)->post("/comment/{$entities['page']->id}", [ $this->actingAs($editor)->post("/comment/{$entities['page']->id}", [
'text' => 'My new comment' 'html' => '<p>My new comment</p>'
]); ]);
$comment = $entities['page']->comments()->orderBy('id', 'desc')->first(); $comment = $entities['page']->comments()->orderBy('id', 'desc')->first();
$this->asAdmin()->post("/comment/{$entities['page']->id}", [ $this->asAdmin()->post("/comment/{$entities['page']->id}", [
'text' => 'My new comment response', 'html' => '<p>My new comment response</p>',
'parent_id' => $comment->local_id, 'parent_id' => $comment->local_id,
]); ]);
$notifications->assertSentTo($editor, CommentCreationNotification::class); $notifications->assertSentTo($editor, CommentCreationNotification::class);
@ -257,7 +257,7 @@ class WatchTest extends TestCase
// Comment post // Comment post
$this->actingAs($admin)->post("/comment/{$entities['page']->id}", [ $this->actingAs($admin)->post("/comment/{$entities['page']->id}", [
'text' => 'My new comment response', 'html' => '<p>My new comment response</p>',
]); ]);
$notifications->assertSentTo($editor, function (CommentCreationNotification $notification) use ($editor, $admin, $entities) { $notifications->assertSentTo($editor, function (CommentCreationNotification $notification) use ($editor, $admin, $entities) {
@ -376,7 +376,7 @@ class WatchTest extends TestCase
$this->permissions->disableEntityInheritedPermissions($page); $this->permissions->disableEntityInheritedPermissions($page);
$this->asAdmin()->post("/comment/{$page->id}", [ $this->asAdmin()->post("/comment/{$page->id}", [
'text' => 'My new comment response', 'html' => '<p>My new comment response</p>',
])->assertOk(); ])->assertOk();
$notifications->assertNothingSentTo($editor); $notifications->assertNothingSentTo($editor);

View File

@ -1,31 +0,0 @@
<?php
namespace Tests\Commands;
use BookStack\Activity\Models\Comment;
use Tests\TestCase;
class RegenerateCommentContentCommandTest extends TestCase
{
public function test_regenerate_comment_content_command()
{
Comment::query()->forceCreate([
'html' => 'some_old_content',
'text' => 'some_fresh_content',
]);
$this->assertDatabaseHas('comments', [
'html' => 'some_old_content',
]);
$exitCode = \Artisan::call('bookstack:regenerate-comment-content');
$this->assertTrue($exitCode === 0, 'Command executed successfully');
$this->assertDatabaseMissing('comments', [
'html' => 'some_old_content',
]);
$this->assertDatabaseHas('comments', [
'html' => "<p>some_fresh_content</p>\n",
]);
}
}

View File

@ -27,7 +27,7 @@ class CommentTest extends TestCase
'local_id' => 1, 'local_id' => 1,
'entity_id' => $page->id, 'entity_id' => $page->id,
'entity_type' => Page::newModelInstance()->getMorphClass(), 'entity_type' => Page::newModelInstance()->getMorphClass(),
'text' => $comment->text, 'text' => null,
'parent_id' => 2, 'parent_id' => 2,
]); ]);
@ -43,17 +43,17 @@ class CommentTest extends TestCase
$this->postJson("/comment/$page->id", $comment->getAttributes()); $this->postJson("/comment/$page->id", $comment->getAttributes());
$comment = $page->comments()->first(); $comment = $page->comments()->first();
$newText = 'updated text content'; $newHtml = '<p>updated text content</p>';
$resp = $this->putJson("/comment/$comment->id", [ $resp = $this->putJson("/comment/$comment->id", [
'text' => $newText, 'html' => $newHtml,
]); ]);
$resp->assertStatus(200); $resp->assertStatus(200);
$resp->assertSee($newText); $resp->assertSee($newHtml, false);
$resp->assertDontSee($comment->text); $resp->assertDontSee($comment->html, false);
$this->assertDatabaseHas('comments', [ $this->assertDatabaseHas('comments', [
'text' => $newText, 'html' => $newHtml,
'entity_id' => $page->id, 'entity_id' => $page->id,
]); ]);
@ -80,46 +80,28 @@ class CommentTest extends TestCase
$this->assertActivityExists(ActivityType::COMMENT_DELETE); $this->assertActivityExists(ActivityType::COMMENT_DELETE);
} }
public function test_comments_converts_markdown_input_to_html() public function test_scripts_cannot_be_injected_via_comment_html()
{
$page = $this->entities->page();
$this->asAdmin()->postJson("/comment/$page->id", [
'text' => '# My Title',
]);
$this->assertDatabaseHas('comments', [
'entity_id' => $page->id,
'entity_type' => $page->getMorphClass(),
'text' => '# My Title',
'html' => "<h1>My Title</h1>\n",
]);
$pageView = $this->get($page->getUrl());
$pageView->assertSee('<h1>My Title</h1>', false);
}
public function test_html_cannot_be_injected_via_comment_content()
{ {
$this->asAdmin(); $this->asAdmin();
$page = $this->entities->page(); $page = $this->entities->page();
$script = '<script>const a = "script";</script>\n\n# sometextinthecomment'; $script = '<script>const a = "script";</script><p onclick="1">My lovely comment</p>';
$this->postJson("/comment/$page->id", [ $this->postJson("/comment/$page->id", [
'text' => $script, 'html' => $script,
]); ]);
$pageView = $this->get($page->getUrl()); $pageView = $this->get($page->getUrl());
$pageView->assertDontSee($script, false); $pageView->assertDontSee($script, false);
$pageView->assertSee('sometextinthecomment'); $pageView->assertSee('<p>My lovely comment</p>', false);
$comment = $page->comments()->first(); $comment = $page->comments()->first();
$this->putJson("/comment/$comment->id", [ $this->putJson("/comment/$comment->id", [
'text' => $script . 'updated', 'html' => $script . '<p>updated</p>',
]); ]);
$pageView = $this->get($page->getUrl()); $pageView = $this->get($page->getUrl());
$pageView->assertDontSee($script, false); $pageView->assertDontSee($script, false);
$pageView->assertSee('sometextinthecommentupdated'); $pageView->assertSee('<p>My lovely comment</p><p>updated</p>');
} }
public function test_reply_comments_are_nested() public function test_reply_comments_are_nested()
@ -127,15 +109,17 @@ class CommentTest extends TestCase
$this->asAdmin(); $this->asAdmin();
$page = $this->entities->page(); $page = $this->entities->page();
$this->postJson("/comment/$page->id", ['text' => 'My new comment']); $this->postJson("/comment/$page->id", ['html' => '<p>My new comment</p>']);
$this->postJson("/comment/$page->id", ['text' => 'My new comment']); $this->postJson("/comment/$page->id", ['html' => '<p>My new comment</p>']);
$respHtml = $this->withHtml($this->get($page->getUrl())); $respHtml = $this->withHtml($this->get($page->getUrl()));
$respHtml->assertElementCount('.comment-branch', 3); $respHtml->assertElementCount('.comment-branch', 3);
$respHtml->assertElementNotExists('.comment-branch .comment-branch'); $respHtml->assertElementNotExists('.comment-branch .comment-branch');
$comment = $page->comments()->first(); $comment = $page->comments()->first();
$resp = $this->postJson("/comment/$page->id", ['text' => 'My nested comment', 'parent_id' => $comment->local_id]); $resp = $this->postJson("/comment/$page->id", [
'html' => '<p>My nested comment</p>', 'parent_id' => $comment->local_id
]);
$resp->assertStatus(200); $resp->assertStatus(200);
$respHtml = $this->withHtml($this->get($page->getUrl())); $respHtml = $this->withHtml($this->get($page->getUrl()));
@ -147,7 +131,7 @@ class CommentTest extends TestCase
{ {
$page = $this->entities->page(); $page = $this->entities->page();
$this->asAdmin()->postJson("/comment/$page->id", ['text' => 'My great comment to see in the editor']); $this->asAdmin()->postJson("/comment/$page->id", ['html' => '<p>My great comment to see in the editor</p>']);
$respHtml = $this->withHtml($this->get($page->getUrl('/edit'))); $respHtml = $this->withHtml($this->get($page->getUrl('/edit')));
$respHtml->assertElementContains('.comment-box .content', 'My great comment to see in the editor'); $respHtml->assertElementContains('.comment-box .content', 'My great comment to see in the editor');
@ -164,4 +148,34 @@ class CommentTest extends TestCase
$pageResp = $this->asAdmin()->get($page->getUrl()); $pageResp = $this->asAdmin()->get($page->getUrl());
$pageResp->assertSee('Wolfeschlegels…'); $pageResp->assertSee('Wolfeschlegels…');
} }
public function test_comment_editor_js_loaded_with_create_or_edit_permissions()
{
$editor = $this->users->editor();
$page = $this->entities->page();
$resp = $this->actingAs($editor)->get($page->getUrl());
$resp->assertSee('tinymce.min.js?', false);
$resp->assertSee('window.editor_translations', false);
$resp->assertSee('component="entity-selector"', false);
$this->permissions->removeUserRolePermissions($editor, ['comment-create-all']);
$this->permissions->grantUserRolePermissions($editor, ['comment-update-own']);
$resp = $this->actingAs($editor)->get($page->getUrl());
$resp->assertDontSee('tinymce.min.js?', false);
$resp->assertDontSee('window.editor_translations', false);
$resp->assertDontSee('component="entity-selector"', false);
Comment::factory()->create([
'created_by' => $editor->id,
'entity_type' => 'page',
'entity_id' => $page->id,
]);
$resp = $this->actingAs($editor)->get($page->getUrl());
$resp->assertSee('tinymce.min.js?', false);
$resp->assertSee('window.editor_translations', false);
$resp->assertSee('component="entity-selector"', false);
}
} }