]> BookStack Code Mirror - bookstack/commitdiff
API: Started building comments API endpoints
authorDan Brown <redacted>
Wed, 22 Oct 2025 13:58:29 +0000 (14:58 +0100)
committerDan Brown <redacted>
Wed, 22 Oct 2025 13:58:29 +0000 (14:58 +0100)
app/Activity/CommentRepo.php
app/Activity/Controllers/CommentApiController.php [new file with mode: 0644]
app/Activity/Models/Comment.php
app/Entities/Models/Entity.php
database/migrations/2025_10_22_134507_update_comments_relation_field_names.php [new file with mode: 0644]
routes/api.php

index 7005f8fcf83d9dc788262e32cf76bb6f153ea1b5..1c2333cae83ce64276ba18cedb06be74d432c420 100644 (file)
@@ -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 (file)
index 0000000..2542107
--- /dev/null
@@ -0,0 +1,43 @@
+<?php
+
+declare(strict_types=1);
+
+namespace BookStack\Activity\Controllers;
+
+use BookStack\Activity\CommentRepo;
+use BookStack\Http\ApiController;
+use Illuminate\Http\JsonResponse;
+
+class CommentApiController extends ApiController
+{
+    // TODO - Add tree-style comment listing to page-show responses.
+    // TODO - list
+    // TODO - create
+    // TODO - read
+    // TODO - update
+    // TODO - delete
+
+    // TODO - Test visibility controls
+    // TODO - Test permissions of each action
+
+    // TODO - Support intro block for API docs so we can explain the
+    //   properties for comments in a shared kind of way?
+
+    public function __construct(
+        protected CommentRepo $commentRepo,
+    ) {
+    }
+
+
+    /**
+     * Get a listing of comments visible to the user.
+     */
+    public function list(): JsonResponse
+    {
+        $query = $this->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'
+        ]);
+    }
+}
index 0cb83d61e8df4c875019986f7ef0c5bc2dca0fa2..caca7809f5a302177b515f1d4b7dddd1529723a6 100644 (file)
@@ -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');
+    }
 }
index b71016ea1c108e5cc6b31ba02ac794fb78e024cd..c6839c15aa0bf13d27c2b1f47b98f1a5b6441f4f 100644 (file)
@@ -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 (file)
index 0000000..de13453
--- /dev/null
@@ -0,0 +1,30 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     */
+    public function up(): void
+    {
+        Schema::table('comments', function (Blueprint $table) {
+            $table->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');
+        });
+    }
+};
index 99df24aed0a67ced8a61e72eef38e53bcd213d1a..b030ca7f73ba3ee5b2feb20da031bb53d6ae1e23 100644 (file)
@@ -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']);