From 082dbc9944e6ee1f08b16ff445b48498edfdcc1f Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Wed, 22 Oct 2025 14:58:29 +0100 Subject: [PATCH] API: Started building comments API endpoints --- app/Activity/CommentRepo.php | 10 ++++- .../Controllers/CommentApiController.php | 43 +++++++++++++++++++ app/Activity/Models/Comment.php | 31 ++++++++++--- app/Entities/Models/Entity.php | 2 +- ...7_update_comments_relation_field_names.php | 30 +++++++++++++ routes/api.php | 6 ++- 6 files changed, 112 insertions(+), 10 deletions(-) create mode 100644 app/Activity/Controllers/CommentApiController.php create mode 100644 database/migrations/2025_10_22_134507_update_comments_relation_field_names.php diff --git a/app/Activity/CommentRepo.php b/app/Activity/CommentRepo.php index 7005f8fcf..1c2333cae 100644 --- a/app/Activity/CommentRepo.php +++ b/app/Activity/CommentRepo.php @@ -5,9 +5,9 @@ namespace BookStack\Activity; use BookStack\Activity\Models\Comment; use BookStack\Entities\Models\Entity; use BookStack\Exceptions\NotifyException; -use BookStack\Exceptions\PrettyException; use BookStack\Facades\Activity as ActivityService; use BookStack\Util\HtmlDescriptionFilter; +use Illuminate\Database\Eloquent\Builder; class CommentRepo { @@ -19,6 +19,14 @@ class CommentRepo return Comment::query()->findOrFail($id); } + /** + * Start a query for comments visible to the user. + */ + public function getQueryForVisible(): Builder + { + return Comment::query()->scopes('visible'); + } + /** * Create a new comment on an entity. */ diff --git a/app/Activity/Controllers/CommentApiController.php b/app/Activity/Controllers/CommentApiController.php new file mode 100644 index 000000000..25421077a --- /dev/null +++ b/app/Activity/Controllers/CommentApiController.php @@ -0,0 +1,43 @@ +commentRepo->getQueryForVisible(); + + return $this->apiListingResponse($query, [ + 'id', 'commentable_id', 'commentable_type', 'parent_id', 'local_id', 'content_ref', 'created_by', 'updated_by', 'created_at', 'updated_at' + ]); + } +} diff --git a/app/Activity/Models/Comment.php b/app/Activity/Models/Comment.php index 0cb83d61e..caca7809f 100644 --- a/app/Activity/Models/Comment.php +++ b/app/Activity/Models/Comment.php @@ -3,12 +3,15 @@ namespace BookStack\Activity\Models; use BookStack\App\Model; +use BookStack\Permissions\Models\JointPermission; +use BookStack\Permissions\PermissionApplicator; use BookStack\Users\Models\HasCreatorAndUpdater; use BookStack\Users\Models\OwnableInterface; -use BookStack\Users\Models\User; use BookStack\Util\HtmlContentFilter; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\MorphTo; /** @@ -17,8 +20,8 @@ use Illuminate\Database\Eloquent\Relations\MorphTo; * @property string $html * @property int|null $parent_id - Relates to local_id, not id * @property int $local_id - * @property string $entity_type - * @property int $entity_id + * @property string $commentable_type + * @property int $commentable_id * @property string $content_ref * @property bool $archived */ @@ -44,8 +47,8 @@ class Comment extends Model implements Loggable, OwnableInterface public function parent(): BelongsTo { return $this->belongsTo(Comment::class, 'parent_id', 'local_id', 'parent') - ->where('entity_type', '=', $this->entity_type) - ->where('entity_id', '=', $this->entity_id); + ->where('commentable_type', '=', $this->commentable_type) + ->where('commentable_id', '=', $this->commentable_id); } /** @@ -58,11 +61,27 @@ class Comment extends Model implements Loggable, OwnableInterface public function logDescriptor(): string { - return "Comment #{$this->local_id} (ID: {$this->id}) for {$this->entity_type} (ID: {$this->entity_id})"; + return "Comment #{$this->local_id} (ID: {$this->id}) for {$this->commentable_type} (ID: {$this->commentable_id})"; } public function safeHtml(): string { return HtmlContentFilter::removeScriptsFromHtmlString($this->html ?? ''); } + + public function jointPermissions(): HasMany + { + return $this->hasMany(JointPermission::class, 'entity_id', 'commentable_id') + ->whereColumn('joint_permissions.entity_type', '=', 'comments.commentable_type'); + } + + /** + * Scope the query to just the comments visible to the user based upon the + * user visibility of what has been commented on. + */ + public function scopeVisible(Builder $query): Builder + { + return app()->make(PermissionApplicator::class) + ->restrictEntityRelationQuery($query, 'comments', 'commentable_id', 'commentable_type'); + } } diff --git a/app/Entities/Models/Entity.php b/app/Entities/Models/Entity.php index b71016ea1..c6839c15a 100644 --- a/app/Entities/Models/Entity.php +++ b/app/Entities/Models/Entity.php @@ -240,7 +240,7 @@ abstract class Entity extends Model implements */ public function comments(bool $orderByCreated = true): MorphMany { - $query = $this->morphMany(Comment::class, 'entity'); + $query = $this->morphMany(Comment::class, 'commentable'); return $orderByCreated ? $query->orderBy('created_at', 'asc') : $query; } diff --git a/database/migrations/2025_10_22_134507_update_comments_relation_field_names.php b/database/migrations/2025_10_22_134507_update_comments_relation_field_names.php new file mode 100644 index 000000000..de13453de --- /dev/null +++ b/database/migrations/2025_10_22_134507_update_comments_relation_field_names.php @@ -0,0 +1,30 @@ +renameColumn('entity_id', 'commentable_id'); + $table->renameColumn('entity_type', 'commentable_type'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('comments', function (Blueprint $table) { + $table->renameColumn('commentable_id', 'entity_id'); + $table->renameColumn('commentable_type', 'entity_type'); + }); + } +}; diff --git a/routes/api.php b/routes/api.php index 99df24aed..b030ca7f7 100644 --- a/routes/api.php +++ b/routes/api.php @@ -6,7 +6,7 @@ * Controllers all end with "ApiController" */ -use BookStack\Activity\Controllers\AuditLogApiController; +use BookStack\Activity\Controllers as ActivityControllers; use BookStack\Api\ApiDocsController; use BookStack\App\SystemApiController; use BookStack\Entities\Controllers as EntityControllers; @@ -70,6 +70,8 @@ Route::delete('image-gallery/{id}', [ImageGalleryApiController::class, 'delete'] Route::get('search', [SearchApiController::class, 'all']); +Route::get('comments', [ActivityControllers\CommentApiController::class, 'list']); + Route::get('shelves', [EntityControllers\BookshelfApiController::class, 'list']); Route::post('shelves', [EntityControllers\BookshelfApiController::class, 'create']); Route::get('shelves/{id}', [EntityControllers\BookshelfApiController::class, 'read']); @@ -101,6 +103,6 @@ Route::delete('recycle-bin/{deletionId}', [EntityControllers\RecycleBinApiContro Route::get('content-permissions/{contentType}/{contentId}', [ContentPermissionApiController::class, 'read']); Route::put('content-permissions/{contentType}/{contentId}', [ContentPermissionApiController::class, 'update']); -Route::get('audit-log', [AuditLogApiController::class, 'list']); +Route::get('audit-log', [ActivityControllers\AuditLogApiController::class, 'list']); Route::get('system', [SystemApiController::class, 'read']);