mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-11-06 00:50:36 +03:00
API: Added comment CUD endpoints, drafted tests
Move some checks and made some tweaks to the repo to support consistency between API and UI.
This commit is contained in:
@@ -4,6 +4,7 @@ namespace BookStack\Activity;
|
|||||||
|
|
||||||
use BookStack\Activity\Models\Comment;
|
use BookStack\Activity\Models\Comment;
|
||||||
use BookStack\Entities\Models\Entity;
|
use BookStack\Entities\Models\Entity;
|
||||||
|
use BookStack\Entities\Models\Page;
|
||||||
use BookStack\Exceptions\NotifyException;
|
use BookStack\Exceptions\NotifyException;
|
||||||
use BookStack\Facades\Activity as ActivityService;
|
use BookStack\Facades\Activity as ActivityService;
|
||||||
use BookStack\Util\HtmlDescriptionFilter;
|
use BookStack\Util\HtmlDescriptionFilter;
|
||||||
@@ -19,6 +20,15 @@ class CommentRepo
|
|||||||
return Comment::query()->findOrFail($id);
|
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.
|
* Start a query for comments visible to the user.
|
||||||
*/
|
*/
|
||||||
@@ -32,6 +42,23 @@ class CommentRepo
|
|||||||
*/
|
*/
|
||||||
public function create(Entity $entity, string $html, ?int $parentId, string $contentRef): Comment
|
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;
|
$userId = user()->id;
|
||||||
$comment = new Comment();
|
$comment = new Comment();
|
||||||
|
|
||||||
@@ -67,7 +94,7 @@ class CommentRepo
|
|||||||
/**
|
/**
|
||||||
* Archive an existing comment.
|
* Archive an existing comment.
|
||||||
*/
|
*/
|
||||||
public function archive(Comment $comment): Comment
|
public function archive(Comment $comment, bool $log = true): Comment
|
||||||
{
|
{
|
||||||
if ($comment->parent_id) {
|
if ($comment->parent_id) {
|
||||||
throw new NotifyException('Only top-level comments can be archived.', '/', 400);
|
throw new NotifyException('Only top-level comments can be archived.', '/', 400);
|
||||||
@@ -76,7 +103,9 @@ class CommentRepo
|
|||||||
$comment->archived = true;
|
$comment->archived = true;
|
||||||
$comment->save();
|
$comment->save();
|
||||||
|
|
||||||
ActivityService::add(ActivityType::COMMENT_UPDATE, $comment);
|
if ($log) {
|
||||||
|
ActivityService::add(ActivityType::COMMENT_UPDATE, $comment);
|
||||||
|
}
|
||||||
|
|
||||||
return $comment;
|
return $comment;
|
||||||
}
|
}
|
||||||
@@ -84,7 +113,7 @@ class CommentRepo
|
|||||||
/**
|
/**
|
||||||
* Un-archive an existing 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) {
|
if ($comment->parent_id) {
|
||||||
throw new NotifyException('Only top-level comments can be un-archived.', '/', 400);
|
throw new NotifyException('Only top-level comments can be un-archived.', '/', 400);
|
||||||
@@ -93,7 +122,9 @@ class CommentRepo
|
|||||||
$comment->archived = false;
|
$comment->archived = false;
|
||||||
$comment->save();
|
$comment->save();
|
||||||
|
|
||||||
ActivityService::add(ActivityType::COMMENT_UPDATE, $comment);
|
if ($log) {
|
||||||
|
ActivityService::add(ActivityType::COMMENT_UPDATE, $comment);
|
||||||
|
}
|
||||||
|
|
||||||
return $comment;
|
return $comment;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,12 @@ namespace BookStack\Activity\Controllers;
|
|||||||
|
|
||||||
use BookStack\Activity\CommentRepo;
|
use BookStack\Activity\CommentRepo;
|
||||||
use BookStack\Activity\Models\Comment;
|
use BookStack\Activity\Models\Comment;
|
||||||
|
use BookStack\Entities\Queries\PageQueries;
|
||||||
use BookStack\Http\ApiController;
|
use BookStack\Http\ApiController;
|
||||||
|
use BookStack\Permissions\Permission;
|
||||||
use Illuminate\Http\JsonResponse;
|
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
|
* The comment data model has a 'local_id' property, which is a unique integer ID
|
||||||
@@ -18,15 +22,26 @@ use Illuminate\Http\JsonResponse;
|
|||||||
class CommentApiController extends ApiController
|
class CommentApiController extends ApiController
|
||||||
{
|
{
|
||||||
// TODO - Add tree-style comment listing to page-show responses.
|
// TODO - Add tree-style comment listing to page-show responses.
|
||||||
// TODO - create
|
|
||||||
// TODO - update
|
|
||||||
// TODO - delete
|
|
||||||
|
|
||||||
// TODO - Test visibility controls
|
// TODO - Test visibility controls
|
||||||
// TODO - Test permissions of each action
|
// 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(
|
public function __construct(
|
||||||
protected CommentRepo $commentRepo,
|
protected CommentRepo $commentRepo,
|
||||||
|
protected PageQueries $pageQueries,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,13 +57,34 @@ class CommentApiController extends ApiController
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
* Read the details of a single comment, along with its direct replies.
|
||||||
*/
|
*/
|
||||||
public function read(string $id): JsonResponse
|
public function read(string $id): JsonResponse
|
||||||
{
|
{
|
||||||
$comment = $this->commentRepo->getQueryForVisible()
|
$comment = $this->commentRepo->getVisibleById(intval($id));
|
||||||
->where('id', '=', $id)->firstOrFail();
|
|
||||||
|
|
||||||
$replies = $this->commentRepo->getQueryForVisible()
|
$replies = $this->commentRepo->getQueryForVisible()
|
||||||
->where('parent_id', '=', $comment->local_id)
|
->where('parent_id', '=', $comment->local_id)
|
||||||
@@ -67,4 +103,45 @@ class CommentApiController extends ApiController
|
|||||||
|
|
||||||
return response()->json($comment);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class CommentController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Save a new comment for a Page.
|
* Save a new comment for a Page.
|
||||||
*
|
*
|
||||||
* @throws ValidationException
|
* @throws ValidationException|\Exception
|
||||||
*/
|
*/
|
||||||
public function savePageComment(Request $request, int $pageId)
|
public function savePageComment(Request $request, int $pageId)
|
||||||
{
|
{
|
||||||
@@ -37,11 +37,6 @@ class CommentController extends Controller
|
|||||||
return response('Not found', 404);
|
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.
|
// Create a new comment.
|
||||||
$this->checkPermission(Permission::CommentCreateAll);
|
$this->checkPermission(Permission::CommentCreateAll);
|
||||||
$contentRef = $input['content_ref'] ?? '';
|
$contentRef = $input['content_ref'] ?? '';
|
||||||
|
|||||||
@@ -8,6 +8,12 @@ use Illuminate\Http\JsonResponse;
|
|||||||
|
|
||||||
abstract class ApiController extends Controller
|
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 = [];
|
protected array $rules = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -48,9 +48,7 @@ enum Permission: string
|
|||||||
case AttachmentUpdateAll = 'attachment-update-all';
|
case AttachmentUpdateAll = 'attachment-update-all';
|
||||||
case AttachmentUpdateOwn = 'attachment-update-own';
|
case AttachmentUpdateOwn = 'attachment-update-own';
|
||||||
|
|
||||||
case CommentCreate = 'comment-create';
|
|
||||||
case CommentCreateAll = 'comment-create-all';
|
case CommentCreateAll = 'comment-create-all';
|
||||||
case CommentCreateOwn = 'comment-create-own';
|
|
||||||
case CommentDelete = 'comment-delete';
|
case CommentDelete = 'comment-delete';
|
||||||
case CommentDeleteAll = 'comment-delete-all';
|
case CommentDeleteAll = 'comment-delete-all';
|
||||||
case CommentDeleteOwn = 'comment-delete-own';
|
case CommentDeleteOwn = 'comment-delete-own';
|
||||||
|
|||||||
43
tests/Activity/CommentsApiTest.php
Normal file
43
tests/Activity/CommentsApiTest.php
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<?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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,6 @@ namespace Tests\Api;
|
|||||||
use BookStack\Entities\Models\Book;
|
use BookStack\Entities\Models\Book;
|
||||||
use BookStack\Entities\Repos\BaseRepo;
|
use BookStack\Entities\Repos\BaseRepo;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Illuminate\Support\Facades\DB;
|
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
|
|
||||||
class BooksApiTest extends TestCase
|
class BooksApiTest extends TestCase
|
||||||
|
|||||||
Reference in New Issue
Block a user