3 declare(strict_types=1);
5 namespace BookStack\Activity\Controllers;
7 use BookStack\Activity\CommentRepo;
8 use BookStack\Activity\Models\Comment;
9 use BookStack\Entities\Queries\PageQueries;
10 use BookStack\Http\ApiController;
11 use BookStack\Permissions\Permission;
12 use Illuminate\Http\JsonResponse;
13 use Illuminate\Http\Request;
14 use Illuminate\Http\Response;
17 * The comment data model has a 'local_id' property, which is a unique integer ID
18 * scoped to the page which the comment is on. The 'parent_id' is used for replies
19 * and refers to the 'local_id' of the parent comment on the same page, not the main
20 * globally unique 'id'.
22 * If you want to get all comments for a page in a tree-like structure, as reflected in
23 * the UI, then that is provided on pages-read API responses.
25 class CommentApiController extends ApiController
27 protected array $rules = [
29 'page_id' => ['required', 'integer'],
30 'reply_to' => ['nullable', 'integer'],
31 'html' => ['required', 'string'],
32 'content_ref' => ['string'],
36 'archived' => ['boolean'],
40 public function __construct(
41 protected CommentRepo $commentRepo,
42 protected PageQueries $pageQueries,
47 * Get a listing of comments visible to the user.
49 public function list(): JsonResponse
51 $query = $this->commentRepo->getQueryForVisible();
53 return $this->apiListingResponse($query, [
54 'id', 'commentable_id', 'commentable_type', 'parent_id', 'local_id', 'content_ref', 'created_by', 'updated_by', 'created_at', 'updated_at'
59 * Create a new comment on a page.
60 * If commenting as a reply to an existing comment, the 'reply_to' parameter
61 * should be provided, set to the 'local_id' of the comment being replied to.
63 public function create(Request $request): JsonResponse
65 $this->checkPermission(Permission::CommentCreateAll);
67 $input = $this->validate($request, $this->rules()['create']);
68 $page = $this->pageQueries->findVisibleByIdOrFail($input['page_id']);
70 $comment = $this->commentRepo->create(
73 $input['reply_to'] ?? null,
74 $input['content_ref'] ?? '',
77 return response()->json($comment);
81 * Read the details of a single comment, along with its direct replies.
83 public function read(string $id): JsonResponse
85 $comment = $this->commentRepo->getVisibleById(intval($id));
86 $comment->load('createdBy', 'updatedBy');
88 $replies = $this->commentRepo->getQueryForVisible()
89 ->where('parent_id', '=', $comment->local_id)
90 ->where('commentable_id', '=', $comment->commentable_id)
91 ->where('commentable_type', '=', $comment->commentable_type)
94 /** @var Comment[] $toProcess */
95 $toProcess = [$comment, ...$replies];
96 foreach ($toProcess as $commentToProcess) {
97 $commentToProcess->setAttribute('html', $commentToProcess->safeHtml());
98 $commentToProcess->makeVisible('html');
101 $comment->setRelation('replies', $replies);
103 return response()->json($comment);
108 * Update the content or archived status of an existing comment.
110 * Only provide a new archived status if needing to actively change the archive state.
111 * Only top-level comments (non-replies) can be archived or unarchived.
113 public function update(Request $request, string $id): JsonResponse
115 $comment = $this->commentRepo->getVisibleById(intval($id));
116 $this->checkOwnablePermission(Permission::CommentUpdate, $comment);
118 $input = $this->validate($request, $this->rules()['update']);
119 $hasHtml = isset($input['html']);
121 if (isset($input['archived'])) {
122 if ($input['archived']) {
123 $this->commentRepo->archive($comment, !$hasHtml);
125 $this->commentRepo->unarchive($comment, !$hasHtml);
130 $comment = $this->commentRepo->update($comment, $input['html']);
133 return response()->json($comment);
137 * Delete a single comment from the system.
139 public function delete(string $id): Response
141 $comment = $this->commentRepo->getVisibleById(intval($id));
142 $this->checkOwnablePermission(Permission::CommentDelete, $comment);
144 $this->commentRepo->delete($comment);
146 return response('', 204);