]> BookStack Code Mirror - bookstack/blob - app/Activity/Controllers/CommentApiController.php
Merge pull request #5917 from BookStackApp/copy_references
[bookstack] / app / Activity / Controllers / CommentApiController.php
1 <?php
2
3 declare(strict_types=1);
4
5 namespace BookStack\Activity\Controllers;
6
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;
15
16 /**
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'.
21  *
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.
24  */
25 class CommentApiController extends ApiController
26 {
27     protected array $rules = [
28         'create' => [
29             'page_id' => ['required', 'integer'],
30             'reply_to' => ['nullable', 'integer'],
31             'html' => ['required', 'string'],
32             'content_ref' => ['string'],
33         ],
34         'update' => [
35             'html' => ['string'],
36             'archived' => ['boolean'],
37         ]
38     ];
39
40     public function __construct(
41         protected CommentRepo $commentRepo,
42         protected PageQueries $pageQueries,
43     ) {
44     }
45
46     /**
47      * Get a listing of comments visible to the user.
48      */
49     public function list(): JsonResponse
50     {
51         $query = $this->commentRepo->getQueryForVisible();
52
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'
55         ]);
56     }
57
58     /**
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.
62      */
63     public function create(Request $request): JsonResponse
64     {
65         $this->checkPermission(Permission::CommentCreateAll);
66
67         $input = $this->validate($request, $this->rules()['create']);
68         $page = $this->pageQueries->findVisibleByIdOrFail($input['page_id']);
69
70         $comment = $this->commentRepo->create(
71             $page,
72             $input['html'],
73             $input['reply_to'] ?? null,
74             $input['content_ref'] ?? '',
75         );
76
77         return response()->json($comment);
78     }
79
80     /**
81      * Read the details of a single comment, along with its direct replies.
82      */
83     public function read(string $id): JsonResponse
84     {
85         $comment = $this->commentRepo->getVisibleById(intval($id));
86         $comment->load('createdBy', 'updatedBy');
87
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)
92             ->get();
93
94         /** @var Comment[] $toProcess */
95         $toProcess = [$comment, ...$replies];
96         foreach ($toProcess as $commentToProcess) {
97             $commentToProcess->setAttribute('html', $commentToProcess->safeHtml());
98             $commentToProcess->makeVisible('html');
99         }
100
101         $comment->setRelation('replies', $replies);
102
103         return response()->json($comment);
104     }
105
106
107     /**
108      * Update the content or archived status of an existing comment.
109      *
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.
112      */
113     public function update(Request $request, string $id): JsonResponse
114     {
115         $comment = $this->commentRepo->getVisibleById(intval($id));
116         $this->checkOwnablePermission(Permission::CommentUpdate, $comment);
117
118         $input = $this->validate($request, $this->rules()['update']);
119         $hasHtml = isset($input['html']);
120
121         if (isset($input['archived'])) {
122             if ($input['archived']) {
123                 $this->commentRepo->archive($comment, !$hasHtml);
124             } else {
125                 $this->commentRepo->unarchive($comment, !$hasHtml);
126             }
127         }
128
129         if ($hasHtml) {
130             $comment = $this->commentRepo->update($comment, $input['html']);
131         }
132
133         return response()->json($comment);
134     }
135
136     /**
137      * Delete a single comment from the system.
138      */
139     public function delete(string $id): Response
140     {
141         $comment = $this->commentRepo->getVisibleById(intval($id));
142         $this->checkOwnablePermission(Permission::CommentDelete, $comment);
143
144         $this->commentRepo->delete($comment);
145
146         return response('', 204);
147     }
148 }