diff --git a/app/Http/Controllers/Api/ContentPermissionsController.php b/app/Http/Controllers/Api/ContentPermissionApiController.php similarity index 98% rename from app/Http/Controllers/Api/ContentPermissionsController.php rename to app/Http/Controllers/Api/ContentPermissionApiController.php index ef17af8ad..47a0d3782 100644 --- a/app/Http/Controllers/Api/ContentPermissionsController.php +++ b/app/Http/Controllers/Api/ContentPermissionApiController.php @@ -7,7 +7,7 @@ use BookStack\Entities\Models\Entity; use BookStack\Entities\Tools\PermissionsUpdater; use Illuminate\Http\Request; -class ContentPermissionsController extends ApiController +class ContentPermissionApiController extends ApiController { public function __construct( protected PermissionsUpdater $permissionsUpdater, diff --git a/app/Http/Controllers/Api/ImageGalleryApiController.php b/app/Http/Controllers/Api/ImageGalleryApiController.php new file mode 100644 index 000000000..85c0c3cef --- /dev/null +++ b/app/Http/Controllers/Api/ImageGalleryApiController.php @@ -0,0 +1,127 @@ + [ + 'type' => ['required', 'string', 'in:gallery,drawio'], + 'uploaded_to' => ['required', 'integer', 'exists:pages,id'], + 'image' => ['required', 'file', ...$this->getImageValidationRules()], + 'name' => ['string', 'max:180'], + ], + 'update' => [ + 'name' => ['string', 'max:180'], + ] + ]; + } + + /** + * Get a listing of gallery images and drawings in the system. + * Requires visibility of the content they're originally uploaded to. + */ + public function list() + { + $images = Image::query()->scopes(['visible']) + ->select($this->fieldsToExpose) + ->whereIn('type', ['gallery', 'drawio']); + + return $this->apiListingResponse($images, [ + ...$this->fieldsToExpose + ]); + } + + /** + * Create a new image in the system. + */ + public function create(Request $request) + { + $data = $this->validate($request, $this->rules()['create']); + + $image = $this->imageRepo->saveNew($data['image'], $data['type'], $data['uploaded_to']); + + return response()->json($this->formatForSingleResponse($image)); + } + + /** + * View the details of a single image. + */ + public function read(string $id) + { + $image = $this->imageRepo->getById($id); + $this->checkOwnablePermission('page-view', $image->getPage()); + + return response()->json($this->formatForSingleResponse($image)); + } + + /** + * Update an existing image in the system. + */ + public function update(Request $request, string $id) + { + $data = $this->validate($request, $this->rules()['update']); + $image = $this->imageRepo->getById($id); + $this->checkOwnablePermission('page-view', $image->getPage()); + $this->checkOwnablePermission('image-update', $image); + + $this->imageRepo->updateImageDetails($image, $data); + + return response()->json($this->formatForSingleResponse($image)); + } + + /** + * Delete an image from the system. + */ + public function delete(string $id) + { + $image = $this->imageRepo->getById($id); + $this->checkOwnablePermission('page-view', $image->getPage()); + $this->checkOwnablePermission('image-delete', $image); + $this->imageRepo->destroyImage($image); + + return response('', 204); + } + + /** + * Format the given image model for single-result display. + */ + protected function formatForSingleResponse(Image $image): array + { + $this->imageRepo->loadThumbs($image); + $data = $image->getAttributes(); + $data['created_by'] = $image->createdBy; + $data['updated_by'] = $image->updatedBy; + $data['content'] = []; + + $escapedUrl = htmlentities($image->url); + $escapedName = htmlentities($image->name); + if ($image->type === 'drawio') { + $data['content']['html'] = "
id}\">
"; + $data['content']['markdown'] = $data['content']['html']; + } else { + $escapedDisplayThumb = htmlentities($image->thumbs['display']); + $data['content']['html'] = "\"{$escapedName}\""; + $mdEscapedName = str_replace(']', '', str_replace('[', '', $image->name)); + $mdEscapedThumb = str_replace(']', '', str_replace('[', '', $image->thumbs['display'])); + $data['content']['markdown'] = "![{$mdEscapedName}]({$mdEscapedThumb})"; + } + + return $data; + } +} diff --git a/app/Http/Controllers/Api/RoleApiController.php b/app/Http/Controllers/Api/RoleApiController.php index 4f78455e0..6986c73f7 100644 --- a/app/Http/Controllers/Api/RoleApiController.php +++ b/app/Http/Controllers/Api/RoleApiController.php @@ -88,10 +88,10 @@ class RoleApiController extends ApiController */ public function read(string $id) { - $user = $this->permissionsRepo->getRoleById($id); - $this->singleFormatter($user); + $role = $this->permissionsRepo->getRoleById($id); + $this->singleFormatter($role); - return response()->json($user); + return response()->json($role); } /** diff --git a/app/Http/Controllers/Images/GalleryImageController.php b/app/Http/Controllers/Images/GalleryImageController.php index 5484411d3..3f2f56265 100644 --- a/app/Http/Controllers/Images/GalleryImageController.php +++ b/app/Http/Controllers/Images/GalleryImageController.php @@ -10,14 +10,9 @@ use Illuminate\Validation\ValidationException; class GalleryImageController extends Controller { - protected $imageRepo; - - /** - * GalleryImageController constructor. - */ - public function __construct(ImageRepo $imageRepo) - { - $this->imageRepo = $imageRepo; + public function __construct( + protected ImageRepo $imageRepo + ) { } /** diff --git a/app/Uploads/Image.php b/app/Uploads/Image.php index c21a3b03f..038e7c199 100644 --- a/app/Uploads/Image.php +++ b/app/Uploads/Image.php @@ -3,9 +3,11 @@ namespace BookStack\Uploads; use BookStack\Auth\Permissions\JointPermission; +use BookStack\Auth\Permissions\PermissionApplicator; use BookStack\Entities\Models\Page; use BookStack\Model; use BookStack\Traits\HasCreatorAndUpdater; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\HasMany; @@ -33,6 +35,15 @@ class Image extends Model ->where('joint_permissions.entity_type', '=', 'page'); } + /** + * Scope the query to just the images visible to the user based upon the + * user visibility of the uploaded_to page. + */ + public function scopeVisible(Builder $query): Builder + { + return app()->make(PermissionApplicator::class)->restrictPageRelationQuery($query, 'images', 'uploaded_to'); + } + /** * Get a thumbnail for this image. * diff --git a/routes/api.php b/routes/api.php index 1b852fed7..c809cdb3a 100644 --- a/routes/api.php +++ b/routes/api.php @@ -13,7 +13,8 @@ use BookStack\Http\Controllers\Api\BookExportApiController; use BookStack\Http\Controllers\Api\BookshelfApiController; use BookStack\Http\Controllers\Api\ChapterApiController; use BookStack\Http\Controllers\Api\ChapterExportApiController; -use BookStack\Http\Controllers\Api\ContentPermissionsController; +use BookStack\Http\Controllers\Api\ContentPermissionApiController; +use BookStack\Http\Controllers\Api\ImageGalleryApiController; use BookStack\Http\Controllers\Api\PageApiController; use BookStack\Http\Controllers\Api\PageExportApiController; use BookStack\Http\Controllers\Api\RecycleBinApiController; @@ -63,6 +64,12 @@ Route::get('pages/{id}/export/pdf', [PageExportApiController::class, 'exportPdf' Route::get('pages/{id}/export/plaintext', [PageExportApiController::class, 'exportPlainText']); Route::get('pages/{id}/export/markdown', [PageExportApiController::class, 'exportMarkdown']); +Route::get('image-gallery', [ImageGalleryApiController::class, 'list']); +Route::post('image-gallery', [ImageGalleryApiController::class, 'create']); +Route::get('image-gallery/{id}', [ImageGalleryApiController::class, 'read']); +Route::put('image-gallery/{id}', [ImageGalleryApiController::class, 'update']); +Route::delete('image-gallery/{id}', [ImageGalleryApiController::class, 'delete']); + Route::get('search', [SearchApiController::class, 'all']); Route::get('shelves', [BookshelfApiController::class, 'list']); @@ -87,5 +94,5 @@ Route::get('recycle-bin', [RecycleBinApiController::class, 'list']); Route::put('recycle-bin/{deletionId}', [RecycleBinApiController::class, 'restore']); Route::delete('recycle-bin/{deletionId}', [RecycleBinApiController::class, 'destroy']); -Route::get('content-permissions/{contentType}/{contentId}', [ContentPermissionsController::class, 'read']); -Route::put('content-permissions/{contentType}/{contentId}', [ContentPermissionsController::class, 'update']); +Route::get('content-permissions/{contentType}/{contentId}', [ContentPermissionApiController::class, 'read']); +Route::put('content-permissions/{contentType}/{contentId}', [ContentPermissionApiController::class, 'update']);