use BookStack\Activity\Models\Comment;
use BookStack\Entities\Models\Entity;
+use BookStack\Entities\Models\Page;
use BookStack\Exceptions\NotifyException;
use BookStack\Facades\Activity as ActivityService;
use BookStack\Util\HtmlDescriptionFilter;
return Comment::query()->findOrFail($id);
}
+ /**
+ * Get a comment by ID, ensuring it is visible to the user based upon access to the page
+ * which the comment is attached to.
+ */
+ public function getVisibleById(int $id): Comment
+ {
+ return $this->getQueryForVisible()->findOrFail($id);
+ }
+
/**
* Start a query for comments visible to the user.
*/
*/
public function create(Entity $entity, string $html, ?int $parentId, string $contentRef): Comment
{
+ // Prevent comments being added to draft pages
+ if ($entity instanceof Page && $entity->draft) {
+ throw new \Exception(trans('errors.cannot_add_comment_to_draft'));
+ }
+
+ // Validate parent ID
+ if ($parentId !== null) {
+ $parentCommentExists = Comment::query()
+ ->where('entity_id', '=', $entity->id)
+ ->where('entity_type', '=', $entity->getMorphClass())
+ ->where('local_id', '=', $parentId)
+ ->exists();
+ if (!$parentCommentExists) {
+ $parentId = null;
+ }
+ }
+
$userId = user()->id;
$comment = new Comment();
/**
* Archive an existing comment.
*/
- public function archive(Comment $comment): Comment
+ public function archive(Comment $comment, bool $log = true): Comment
{
if ($comment->parent_id) {
throw new NotifyException('Only top-level comments can be archived.', '/', 400);
$comment->archived = true;
$comment->save();
- ActivityService::add(ActivityType::COMMENT_UPDATE, $comment);
+ if ($log) {
+ ActivityService::add(ActivityType::COMMENT_UPDATE, $comment);
+ }
return $comment;
}
/**
* Un-archive an existing comment.
*/
- public function unarchive(Comment $comment): Comment
+ public function unarchive(Comment $comment, bool $log = true): Comment
{
if ($comment->parent_id) {
throw new NotifyException('Only top-level comments can be un-archived.', '/', 400);
$comment->archived = false;
$comment->save();
- ActivityService::add(ActivityType::COMMENT_UPDATE, $comment);
+ if ($log) {
+ ActivityService::add(ActivityType::COMMENT_UPDATE, $comment);
+ }
return $comment;
}
use BookStack\Activity\CommentRepo;
use BookStack\Activity\Models\Comment;
+use BookStack\Entities\Queries\PageQueries;
use BookStack\Http\ApiController;
+use BookStack\Permissions\Permission;
use Illuminate\Http\JsonResponse;
+use Illuminate\Http\Request;
+use Illuminate\Http\Response;
/**
* The comment data model has a 'local_id' property, which is a unique integer ID
class CommentApiController extends ApiController
{
// TODO - Add tree-style comment listing to page-show responses.
- // TODO - create
- // TODO - update
- // TODO - delete
// TODO - Test visibility controls
// TODO - Test permissions of each action
+ protected array $rules = [
+ 'create' => [
+ 'page_id' => ['required', 'integer'],
+ 'reply_to' => ['nullable', 'integer'],
+ 'html' => ['required', 'string'],
+ 'content_ref' => ['string'],
+ ],
+ 'update' => [
+ 'html' => ['required', 'string'],
+ 'archived' => ['boolean'],
+ ]
+ ];
+
public function __construct(
protected CommentRepo $commentRepo,
+ protected PageQueries $pageQueries,
) {
}
]);
}
+ /**
+ * Create a new comment on a page.
+ * If commenting as a reply to an existing comment, the 'reply_to' parameter
+ * should be provided, set to the 'local_id' of the comment being replied to.
+ */
+ public function create(Request $request): JsonResponse
+ {
+ $this->checkPermission(Permission::CommentCreateAll);
+
+ $input = $this->validate($request, $this->rules()['create']);
+ $page = $this->pageQueries->findVisibleByIdOrFail($input['page_id']);
+
+ $comment = $this->commentRepo->create(
+ $page,
+ $input['html'],
+ $input['reply_to'] ?? null,
+ $input['content_ref'] ?? '',
+ );
+
+ return response()->json($comment);
+ }
+
/**
* Read the details of a single comment, along with its direct replies.
*/
public function read(string $id): JsonResponse
{
- $comment = $this->commentRepo->getQueryForVisible()
- ->where('id', '=', $id)->firstOrFail();
+ $comment = $this->commentRepo->getVisibleById(intval($id));
$replies = $this->commentRepo->getQueryForVisible()
->where('parent_id', '=', $comment->local_id)
return response()->json($comment);
}
+
+
+ /**
+ * Update the content or archived status of an existing comment.
+ *
+ * Only provide a new archived status if needing to actively change the archive state.
+ * Only top-level comments (non-replies) can be archived or unarchived.
+ */
+ public function update(Request $request, string $id): JsonResponse
+ {
+ $comment = $this->commentRepo->getVisibleById(intval($id));
+ $this->checkOwnablePermission(Permission::CommentUpdate, $comment);
+
+ $input = $this->validate($request, $this->rules()['update']);
+
+ if (isset($input['archived'])) {
+ $archived = $input['archived'];
+ if ($archived) {
+ $this->commentRepo->archive($comment, false);
+ } else {
+ $this->commentRepo->unarchive($comment, false);
+ }
+ }
+
+ $comment = $this->commentRepo->update($comment, $input['html']);
+
+ return response()->json($comment);
+ }
+
+ /**
+ * Delete a single comment from the system.
+ */
+ public function delete(string $id): Response
+ {
+ $comment = $this->commentRepo->getVisibleById(intval($id));
+ $this->checkOwnablePermission(Permission::CommentDelete, $comment);
+
+ $this->commentRepo->delete($comment);
+
+ return response('', 204);
+ }
}
/**
* Save a new comment for a Page.
*
- * @throws ValidationException
+ * @throws ValidationException|\Exception
*/
public function savePageComment(Request $request, int $pageId)
{
return response('Not found', 404);
}
- // Prevent adding comments to draft pages
- if ($page->draft) {
- return $this->jsonError(trans('errors.cannot_add_comment_to_draft'), 400);
- }
-
// Create a new comment.
$this->checkPermission(Permission::CommentCreateAll);
$contentRef = $input['content_ref'] ?? '';
abstract class ApiController extends Controller
{
+ /**
+ * The validation rules for this controller.
+ * Can alternative be defined in a rules() method is they need to be dynamic.
+ *
+ * @var array<string, string[]>
+ */
protected array $rules = [];
/**
case AttachmentUpdateAll = 'attachment-update-all';
case AttachmentUpdateOwn = 'attachment-update-own';
- case CommentCreate = 'comment-create';
case CommentCreateAll = 'comment-create-all';
- case CommentCreateOwn = 'comment-create-own';
case CommentDelete = 'comment-delete';
case CommentDeleteAll = 'comment-delete-all';
case CommentDeleteOwn = 'comment-delete-own';
--- /dev/null
+<?php
+
+namespace Activity;
+
+use BookStack\Activity\ActivityType;
+use BookStack\Facades\Activity;
+use Tests\Api\TestsApi;
+use Tests\TestCase;
+
+class CommentsApiTest extends TestCase
+{
+ use TestsApi;
+
+ public function test_endpoint_permission_controls()
+ {
+ // TODO
+ }
+
+ public function test_index()
+ {
+ // TODO
+ }
+
+ public function test_create()
+ {
+ // TODO
+ }
+
+ public function test_read()
+ {
+ // TODO
+ }
+
+ public function test_update()
+ {
+ // TODO
+ }
+
+ public function test_destroy()
+ {
+ // TODO
+ }
+}
use BookStack\Entities\Models\Book;
use BookStack\Entities\Repos\BaseRepo;
use Carbon\Carbon;
-use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class BooksApiTest extends TestCase