Skip to content

Commit cbf27d7

Browse files
committed
API: Added comment CUD endpoints, drafted tests
Move some checks and made some tweaks to the repo to support consistency between API and UI.
1 parent 3ad1e31 commit cbf27d7

File tree

7 files changed

+167
-18
lines changed

7 files changed

+167
-18
lines changed

app/Activity/CommentRepo.php

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use BookStack\Activity\Models\Comment;
66
use BookStack\Entities\Models\Entity;
7+
use BookStack\Entities\Models\Page;
78
use BookStack\Exceptions\NotifyException;
89
use BookStack\Facades\Activity as ActivityService;
910
use BookStack\Util\HtmlDescriptionFilter;
@@ -19,6 +20,15 @@ public function getById(int $id): Comment
1920
return Comment::query()->findOrFail($id);
2021
}
2122

23+
/**
24+
* Get a comment by ID, ensuring it is visible to the user based upon access to the page
25+
* which the comment is attached to.
26+
*/
27+
public function getVisibleById(int $id): Comment
28+
{
29+
return $this->getQueryForVisible()->findOrFail($id);
30+
}
31+
2232
/**
2333
* Start a query for comments visible to the user.
2434
*/
@@ -32,6 +42,23 @@ public function getQueryForVisible(): Builder
3242
*/
3343
public function create(Entity $entity, string $html, ?int $parentId, string $contentRef): Comment
3444
{
45+
// Prevent comments being added to draft pages
46+
if ($entity instanceof Page && $entity->draft) {
47+
throw new \Exception(trans('errors.cannot_add_comment_to_draft'));
48+
}
49+
50+
// Validate parent ID
51+
if ($parentId !== null) {
52+
$parentCommentExists = Comment::query()
53+
->where('entity_id', '=', $entity->id)
54+
->where('entity_type', '=', $entity->getMorphClass())
55+
->where('local_id', '=', $parentId)
56+
->exists();
57+
if (!$parentCommentExists) {
58+
$parentId = null;
59+
}
60+
}
61+
3562
$userId = user()->id;
3663
$comment = new Comment();
3764

@@ -67,7 +94,7 @@ public function update(Comment $comment, string $html): Comment
6794
/**
6895
* Archive an existing comment.
6996
*/
70-
public function archive(Comment $comment): Comment
97+
public function archive(Comment $comment, bool $log = true): Comment
7198
{
7299
if ($comment->parent_id) {
73100
throw new NotifyException('Only top-level comments can be archived.', '/', 400);
@@ -76,15 +103,17 @@ public function archive(Comment $comment): Comment
76103
$comment->archived = true;
77104
$comment->save();
78105

79-
ActivityService::add(ActivityType::COMMENT_UPDATE, $comment);
106+
if ($log) {
107+
ActivityService::add(ActivityType::COMMENT_UPDATE, $comment);
108+
}
80109

81110
return $comment;
82111
}
83112

84113
/**
85114
* Un-archive an existing comment.
86115
*/
87-
public function unarchive(Comment $comment): Comment
116+
public function unarchive(Comment $comment, bool $log = true): Comment
88117
{
89118
if ($comment->parent_id) {
90119
throw new NotifyException('Only top-level comments can be un-archived.', '/', 400);
@@ -93,7 +122,9 @@ public function unarchive(Comment $comment): Comment
93122
$comment->archived = false;
94123
$comment->save();
95124

96-
ActivityService::add(ActivityType::COMMENT_UPDATE, $comment);
125+
if ($log) {
126+
ActivityService::add(ActivityType::COMMENT_UPDATE, $comment);
127+
}
97128

98129
return $comment;
99130
}

app/Activity/Controllers/CommentApiController.php

Lines changed: 82 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,12 @@
66

77
use BookStack\Activity\CommentRepo;
88
use BookStack\Activity\Models\Comment;
9+
use BookStack\Entities\Queries\PageQueries;
910
use BookStack\Http\ApiController;
11+
use BookStack\Permissions\Permission;
1012
use Illuminate\Http\JsonResponse;
13+
use Illuminate\Http\Request;
14+
use Illuminate\Http\Response;
1115

1216
/**
1317
* The comment data model has a 'local_id' property, which is a unique integer ID
@@ -18,15 +22,26 @@
1822
class CommentApiController extends ApiController
1923
{
2024
// TODO - Add tree-style comment listing to page-show responses.
21-
// TODO - create
22-
// TODO - update
23-
// TODO - delete
2425

2526
// TODO - Test visibility controls
2627
// TODO - Test permissions of each action
2728

29+
protected array $rules = [
30+
'create' => [
31+
'page_id' => ['required', 'integer'],
32+
'reply_to' => ['nullable', 'integer'],
33+
'html' => ['required', 'string'],
34+
'content_ref' => ['string'],
35+
],
36+
'update' => [
37+
'html' => ['required', 'string'],
38+
'archived' => ['boolean'],
39+
]
40+
];
41+
2842
public function __construct(
2943
protected CommentRepo $commentRepo,
44+
protected PageQueries $pageQueries,
3045
) {
3146
}
3247

@@ -42,13 +57,34 @@ public function list(): JsonResponse
4257
]);
4358
}
4459

60+
/**
61+
* Create a new comment on a page.
62+
* If commenting as a reply to an existing comment, the 'reply_to' parameter
63+
* should be provided, set to the 'local_id' of the comment being replied to.
64+
*/
65+
public function create(Request $request): JsonResponse
66+
{
67+
$this->checkPermission(Permission::CommentCreateAll);
68+
69+
$input = $this->validate($request, $this->rules()['create']);
70+
$page = $this->pageQueries->findVisibleByIdOrFail($input['page_id']);
71+
72+
$comment = $this->commentRepo->create(
73+
$page,
74+
$input['html'],
75+
$input['reply_to'] ?? null,
76+
$input['content_ref'] ?? '',
77+
);
78+
79+
return response()->json($comment);
80+
}
81+
4582
/**
4683
* Read the details of a single comment, along with its direct replies.
4784
*/
4885
public function read(string $id): JsonResponse
4986
{
50-
$comment = $this->commentRepo->getQueryForVisible()
51-
->where('id', '=', $id)->firstOrFail();
87+
$comment = $this->commentRepo->getVisibleById(intval($id));
5288

5389
$replies = $this->commentRepo->getQueryForVisible()
5490
->where('parent_id', '=', $comment->local_id)
@@ -67,4 +103,45 @@ public function read(string $id): JsonResponse
67103

68104
return response()->json($comment);
69105
}
106+
107+
108+
/**
109+
* Update the content or archived status of an existing comment.
110+
*
111+
* Only provide a new archived status if needing to actively change the archive state.
112+
* Only top-level comments (non-replies) can be archived or unarchived.
113+
*/
114+
public function update(Request $request, string $id): JsonResponse
115+
{
116+
$comment = $this->commentRepo->getVisibleById(intval($id));
117+
$this->checkOwnablePermission(Permission::CommentUpdate, $comment);
118+
119+
$input = $this->validate($request, $this->rules()['update']);
120+
121+
if (isset($input['archived'])) {
122+
$archived = $input['archived'];
123+
if ($archived) {
124+
$this->commentRepo->archive($comment, false);
125+
} else {
126+
$this->commentRepo->unarchive($comment, false);
127+
}
128+
}
129+
130+
$comment = $this->commentRepo->update($comment, $input['html']);
131+
132+
return response()->json($comment);
133+
}
134+
135+
/**
136+
* Delete a single comment from the system.
137+
*/
138+
public function delete(string $id): Response
139+
{
140+
$comment = $this->commentRepo->getVisibleById(intval($id));
141+
$this->checkOwnablePermission(Permission::CommentDelete, $comment);
142+
143+
$this->commentRepo->delete($comment);
144+
145+
return response('', 204);
146+
}
70147
}

app/Activity/Controllers/CommentController.php

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public function __construct(
2222
/**
2323
* Save a new comment for a Page.
2424
*
25-
* @throws ValidationException
25+
* @throws ValidationException|\Exception
2626
*/
2727
public function savePageComment(Request $request, int $pageId)
2828
{
@@ -37,11 +37,6 @@ public function savePageComment(Request $request, int $pageId)
3737
return response('Not found', 404);
3838
}
3939

40-
// Prevent adding comments to draft pages
41-
if ($page->draft) {
42-
return $this->jsonError(trans('errors.cannot_add_comment_to_draft'), 400);
43-
}
44-
4540
// Create a new comment.
4641
$this->checkPermission(Permission::CommentCreateAll);
4742
$contentRef = $input['content_ref'] ?? '';

app/Http/ApiController.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@
88

99
abstract class ApiController extends Controller
1010
{
11+
/**
12+
* The validation rules for this controller.
13+
* Can alternative be defined in a rules() method is they need to be dynamic.
14+
*
15+
* @var array<string, string[]>
16+
*/
1117
protected array $rules = [];
1218

1319
/**

app/Permissions/Permission.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,7 @@ enum Permission: string
4848
case AttachmentUpdateAll = 'attachment-update-all';
4949
case AttachmentUpdateOwn = 'attachment-update-own';
5050

51-
case CommentCreate = 'comment-create';
5251
case CommentCreateAll = 'comment-create-all';
53-
case CommentCreateOwn = 'comment-create-own';
5452
case CommentDelete = 'comment-delete';
5553
case CommentDeleteAll = 'comment-delete-all';
5654
case CommentDeleteOwn = 'comment-delete-own';

tests/Activity/CommentsApiTest.php

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
namespace Activity;
4+
5+
use BookStack\Activity\ActivityType;
6+
use BookStack\Facades\Activity;
7+
use Tests\Api\TestsApi;
8+
use Tests\TestCase;
9+
10+
class CommentsApiTest extends TestCase
11+
{
12+
use TestsApi;
13+
14+
public function test_endpoint_permission_controls()
15+
{
16+
// TODO
17+
}
18+
19+
public function test_index()
20+
{
21+
// TODO
22+
}
23+
24+
public function test_create()
25+
{
26+
// TODO
27+
}
28+
29+
public function test_read()
30+
{
31+
// TODO
32+
}
33+
34+
public function test_update()
35+
{
36+
// TODO
37+
}
38+
39+
public function test_destroy()
40+
{
41+
// TODO
42+
}
43+
}

tests/Api/BooksApiTest.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
use BookStack\Entities\Models\Book;
66
use BookStack\Entities\Repos\BaseRepo;
77
use Carbon\Carbon;
8-
use Illuminate\Support\Facades\DB;
98
use Tests\TestCase;
109

1110
class BooksApiTest extends TestCase

0 commit comments

Comments
 (0)