From d9eec6d82caf2c63c8535f6842612fc6939d5d0e Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 14 Mar 2023 12:19:19 +0000 Subject: [PATCH 1/3] Started Image API build --- ...php => ContentPermissionApiController.php} | 2 +- .../Api/ImageGalleryApiController.php | 127 ++++++++++++++++++ .../Controllers/Api/RoleApiController.php | 6 +- .../Images/GalleryImageController.php | 11 +- app/Uploads/Image.php | 11 ++ routes/api.php | 13 +- 6 files changed, 155 insertions(+), 15 deletions(-) rename app/Http/Controllers/Api/{ContentPermissionsController.php => ContentPermissionApiController.php} (98%) create mode 100644 app/Http/Controllers/Api/ImageGalleryApiController.php 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']); From 3a808fd76859a90cda0d6a4085bed053d7cabde1 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 14 Mar 2023 19:29:08 +0000 Subject: [PATCH 2/3] Added phpunit tests to cover image API endpoints --- .../Api/ImageGalleryApiController.php | 13 +- app/Uploads/Image.php | 2 +- tests/Api/ImageGalleryApiTest.php | 347 ++++++++++++++++++ tests/Api/TestsApi.php | 21 +- 4 files changed, 375 insertions(+), 8 deletions(-) create mode 100644 tests/Api/ImageGalleryApiTest.php diff --git a/app/Http/Controllers/Api/ImageGalleryApiController.php b/app/Http/Controllers/Api/ImageGalleryApiController.php index 85c0c3cef..a9fb3b103 100644 --- a/app/Http/Controllers/Api/ImageGalleryApiController.php +++ b/app/Http/Controllers/Api/ImageGalleryApiController.php @@ -2,6 +2,7 @@ namespace BookStack\Http\Controllers\Api; +use BookStack\Entities\Models\Page; use BookStack\Uploads\Image; use BookStack\Uploads\ImageRepo; use Illuminate\Http\Request; @@ -22,7 +23,7 @@ class ImageGalleryApiController extends ApiController return [ 'create' => [ 'type' => ['required', 'string', 'in:gallery,drawio'], - 'uploaded_to' => ['required', 'integer', 'exists:pages,id'], + 'uploaded_to' => ['required', 'integer'], 'image' => ['required', 'file', ...$this->getImageValidationRules()], 'name' => ['string', 'max:180'], ], @@ -52,10 +53,17 @@ class ImageGalleryApiController extends ApiController */ public function create(Request $request) { + $this->checkPermission('image-create-all'); $data = $this->validate($request, $this->rules()['create']); + Page::visible()->findOrFail($data['uploaded_to']); $image = $this->imageRepo->saveNew($data['image'], $data['type'], $data['uploaded_to']); + if (isset($data['name'])) { + $image->refresh(); + $image->update(['name' => $data['name']]); + } + return response()->json($this->formatForSingleResponse($image)); } @@ -64,8 +72,7 @@ class ImageGalleryApiController extends ApiController */ public function read(string $id) { - $image = $this->imageRepo->getById($id); - $this->checkOwnablePermission('page-view', $image->getPage()); + $image = Image::query()->scopes(['visible'])->findOrFail($id); return response()->json($this->formatForSingleResponse($image)); } diff --git a/app/Uploads/Image.php b/app/Uploads/Image.php index 038e7c199..0ab0b612a 100644 --- a/app/Uploads/Image.php +++ b/app/Uploads/Image.php @@ -49,7 +49,7 @@ class Image extends Model * * @throws \Exception */ - public function getThumb(int $width, int $height, bool $keepRatio = false): string + public function getThumb(?int $width, ?int $height, bool $keepRatio = false): string { return app()->make(ImageService::class)->getThumbnail($this, $width, $height, $keepRatio); } diff --git a/tests/Api/ImageGalleryApiTest.php b/tests/Api/ImageGalleryApiTest.php new file mode 100644 index 000000000..17c90518c --- /dev/null +++ b/tests/Api/ImageGalleryApiTest.php @@ -0,0 +1,347 @@ +actingAsApiAdmin(); + $imagePage = $this->entities->page(); + $data = $this->files->uploadGalleryImageToPage($this, $imagePage); + $image = Image::findOrFail($data['response']->id); + + $resp = $this->getJson($this->baseEndpoint . '?count=1&sort=+id'); + $resp->assertJson(['data' => [ + [ + 'id' => $image->id, + 'name' => $image->name, + 'url' => $image->url, + 'path' => $image->path, + 'type' => 'gallery', + 'uploaded_to' => $imagePage->id, + 'created_by' => $this->users->admin()->id, + 'updated_by' => $this->users->admin()->id, + ], + ]]); + + $resp->assertJson(['total' => Image::query()->count()]); + } + + public function test_index_endpoint_doesnt_show_images_for_those_uploaded_to_non_visible_pages() + { + $this->actingAsApiEditor(); + $imagePage = $this->entities->page(); + $data = $this->files->uploadGalleryImageToPage($this, $imagePage); + $image = Image::findOrFail($data['response']->id); + + $resp = $this->getJson($this->baseEndpoint . '?filter[id]=' . $image->id); + $resp->assertJsonCount(1, 'data'); + $resp->assertJson(['total' => 1]); + + $this->permissions->disableEntityInheritedPermissions($imagePage); + + $resp = $this->getJson($this->baseEndpoint . '?filter[id]=' . $image->id); + $resp->assertJsonCount(0, 'data'); + $resp->assertJson(['total' => 0]); + } + + public function test_index_endpoint_doesnt_show_other_image_types() + { + $this->actingAsApiEditor(); + $imagePage = $this->entities->page(); + $data = $this->files->uploadGalleryImageToPage($this, $imagePage); + $image = Image::findOrFail($data['response']->id); + + $typesByCountExpectation = [ + 'cover_book' => 0, + 'drawio' => 1, + 'gallery' => 1, + 'user' => 0, + 'system' => 0, + ]; + + foreach ($typesByCountExpectation as $type => $count) { + $image->type = $type; + $image->save(); + + $resp = $this->getJson($this->baseEndpoint . '?filter[id]=' . $image->id); + $resp->assertJsonCount($count, 'data'); + $resp->assertJson(['total' => $count]); + } + } + + public function test_create_endpoint() + { + $this->actingAsApiAdmin(); + + $imagePage = $this->entities->page(); + $resp = $this->call('POST', $this->baseEndpoint, [ + 'type' => 'gallery', + 'uploaded_to' => $imagePage->id, + 'name' => 'My awesome image!', + ], [], [ + 'image' => $this->files->uploadedImage('my-cool-image.png'), + ]); + + $resp->assertStatus(200); + + $image = Image::query()->where('uploaded_to', '=', $imagePage->id)->first(); + $expectedUser = [ + 'id' => $this->users->admin()->id, + 'name' => $this->users->admin()->name, + 'slug' => $this->users->admin()->slug, + ]; + $resp->assertJson([ + 'id' => $image->id, + 'name' => 'My awesome image!', + 'url' => $image->url, + 'path' => $image->path, + 'type' => 'gallery', + 'uploaded_to' => $imagePage->id, + 'created_by' => $expectedUser, + 'updated_by' => $expectedUser, + ]); + } + + public function test_create_endpoint_requires_image_create_permissions() + { + $user = $this->users->editor(); + $this->actingAsForApi($user); + $this->permissions->removeUserRolePermissions($user, ['image-create-all']); + + $makeRequest = function () { + return $this->call('POST', $this->baseEndpoint, []); + }; + + $resp = $makeRequest(); + $resp->assertStatus(403); + + $this->permissions->grantUserRolePermissions($user, ['image-create-all']); + + $resp = $makeRequest(); + $resp->assertStatus(422); + } + + public function test_create_fails_if_uploaded_to_not_visible_or_not_exists() + { + $this->actingAsApiEditor(); + + $makeRequest = function (int $uploadedTo) { + return $this->call('POST', $this->baseEndpoint, [ + 'type' => 'gallery', + 'uploaded_to' => $uploadedTo, + 'name' => 'My awesome image!', + ], [], [ + 'image' => $this->files->uploadedImage('my-cool-image.png'), + ]); + }; + + $page = $this->entities->page(); + $this->permissions->disableEntityInheritedPermissions($page); + $resp = $makeRequest($page->id); + $resp->assertStatus(404); + + $resp = $makeRequest(Page::query()->max('id') + 55); + $resp->assertStatus(404); + } + + public function test_create_has_restricted_types() + { + $this->actingAsApiEditor(); + + $typesByStatusExpectation = [ + 'cover_book' => 422, + 'drawio' => 200, + 'gallery' => 200, + 'user' => 422, + 'system' => 422, + ]; + + $makeRequest = function (string $type) { + return $this->call('POST', $this->baseEndpoint, [ + 'type' => $type, + 'uploaded_to' => $this->entities->page()->id, + 'name' => 'My awesome image!', + ], [], [ + 'image' => $this->files->uploadedImage('my-cool-image.png'), + ]); + }; + + foreach ($typesByStatusExpectation as $type => $status) { + $resp = $makeRequest($type); + $resp->assertStatus($status); + } + } + + public function test_create_will_use_file_name_if_no_name_provided_in_request() + { + $this->actingAsApiEditor(); + + $imagePage = $this->entities->page(); + $resp = $this->call('POST', $this->baseEndpoint, [ + 'type' => 'gallery', + 'uploaded_to' => $imagePage->id, + ], [], [ + 'image' => $this->files->uploadedImage('my-cool-image.png'), + ]); + $resp->assertStatus(200); + + $this->assertDatabaseHas('images', [ + 'type' => 'gallery', + 'uploaded_to' => $imagePage->id, + 'name' => 'my-cool-image.png', + ]); + } + + public function test_read_endpoint() + { + $this->actingAsApiAdmin(); + $imagePage = $this->entities->page(); + $data = $this->files->uploadGalleryImageToPage($this, $imagePage); + $image = Image::findOrFail($data['response']->id); + + $resp = $this->getJson($this->baseEndpoint . "/{$image->id}"); + $resp->assertStatus(200); + + $expectedUser = [ + 'id' => $this->users->admin()->id, + 'name' => $this->users->admin()->name, + 'slug' => $this->users->admin()->slug, + ]; + + $displayUrl = $image->getThumb(1680, null, true); + $resp->assertJson([ + 'id' => $image->id, + 'name' => $image->name, + 'url' => $image->url, + 'path' => $image->path, + 'type' => 'gallery', + 'uploaded_to' => $imagePage->id, + 'created_by' => $expectedUser, + 'updated_by' => $expectedUser, + 'content' => [ + 'html' => "url}\" target=\"_blank\">\"{$image-name}\">", + 'markdown' => "![{$image->name}]({$displayUrl})", + ], + ]); + $this->assertStringStartsWith('http://', $resp->json('thumbs.gallery')); + $this->assertStringStartsWith('http://', $resp->json('thumbs.display')); + } + + public function test_read_endpoint_provides_different_content_for_drawings() + { + $this->actingAsApiAdmin(); + $imagePage = $this->entities->page(); + $data = $this->files->uploadGalleryImageToPage($this, $imagePage); + $image = Image::findOrFail($data['response']->id); + + $image->type = 'drawio'; + $image->save(); + + $resp = $this->getJson($this->baseEndpoint . "/{$image->id}"); + $resp->assertStatus(200); + + $drawing = "
id}\">url}\">
"; + $resp->assertJson([ + 'id' => $image->id, + 'content' => [ + 'html' => $drawing, + 'markdown' => $drawing, + ], + ]); + } + + public function test_read_endpoint_does_not_show_if_no_permissions_for_related_page() + { + $this->actingAsApiEditor(); + $imagePage = $this->entities->page(); + $data = $this->files->uploadGalleryImageToPage($this, $imagePage); + $image = Image::findOrFail($data['response']->id); + + $this->permissions->disableEntityInheritedPermissions($imagePage); + + $resp = $this->getJson($this->baseEndpoint . "/{$image->id}"); + $resp->assertStatus(404); + } + + public function test_update_endpoint() + { + $this->actingAsApiAdmin(); + $imagePage = $this->entities->page(); + $data = $this->files->uploadGalleryImageToPage($this, $imagePage); + $image = Image::findOrFail($data['response']->id); + + $resp = $this->putJson($this->baseEndpoint . "/{$image->id}", [ + 'name' => 'My updated image name!', + ]); + + $resp->assertStatus(200); + $resp->assertJson([ + 'id' => $image->id, + 'name' => 'My updated image name!', + ]); + $this->assertDatabaseHas('images', [ + 'id' => $image->id, + 'name' => 'My updated image name!', + ]); + } + + public function test_update_endpoint_requires_image_delete_permission() + { + $user = $this->users->editor(); + $this->actingAsForApi($user); + $imagePage = $this->entities->page(); + $this->permissions->removeUserRolePermissions($user, ['image-update-all', 'image-update-own']); + $data = $this->files->uploadGalleryImageToPage($this, $imagePage); + $image = Image::findOrFail($data['response']->id); + + $resp = $this->putJson($this->baseEndpoint . "/{$image->id}", ['name' => 'My new name']); + $resp->assertStatus(403); + $resp->assertJson($this->permissionErrorResponse()); + + $this->permissions->grantUserRolePermissions($user, ['image-update-all']); + $resp = $this->putJson($this->baseEndpoint . "/{$image->id}", ['name' => 'My new name']); + $resp->assertStatus(200); + } + + public function test_delete_endpoint() + { + $this->actingAsApiAdmin(); + $imagePage = $this->entities->page(); + $data = $this->files->uploadGalleryImageToPage($this, $imagePage); + $image = Image::findOrFail($data['response']->id); + $this->assertDatabaseHas('images', ['id' => $image->id]); + + $resp = $this->deleteJson($this->baseEndpoint . "/{$image->id}"); + + $resp->assertStatus(204); + $this->assertDatabaseMissing('images', ['id' => $image->id]); + } + + public function test_delete_endpoint_requires_image_delete_permission() + { + $user = $this->users->editor(); + $this->actingAsForApi($user); + $imagePage = $this->entities->page(); + $this->permissions->removeUserRolePermissions($user, ['image-delete-all', 'image-delete-own']); + $data = $this->files->uploadGalleryImageToPage($this, $imagePage); + $image = Image::findOrFail($data['response']->id); + + $resp = $this->deleteJson($this->baseEndpoint . "/{$image->id}"); + $resp->assertStatus(403); + $resp->assertJson($this->permissionErrorResponse()); + + $this->permissions->grantUserRolePermissions($user, ['image-delete-all']); + $resp = $this->deleteJson($this->baseEndpoint . "/{$image->id}"); + $resp->assertStatus(204); + } +} diff --git a/tests/Api/TestsApi.php b/tests/Api/TestsApi.php index 501f28754..c566fd8de 100644 --- a/tests/Api/TestsApi.php +++ b/tests/Api/TestsApi.php @@ -2,15 +2,28 @@ namespace Tests\Api; +use BookStack\Auth\User; + trait TestsApi { - protected $apiTokenId = 'apitoken'; - protected $apiTokenSecret = 'password'; + protected string $apiTokenId = 'apitoken'; + protected string $apiTokenSecret = 'password'; + + /** + * Set the given user as the current logged-in user via the API driver. + * This does not ensure API access. The user may still lack required role permissions. + */ + protected function actingAsForApi(User $user): static + { + parent::actingAs($user, 'api'); + + return $this; + } /** * Set the API editor role as the current user via the API driver. */ - protected function actingAsApiEditor() + protected function actingAsApiEditor(): static { $this->actingAs($this->users->editor(), 'api'); @@ -20,7 +33,7 @@ trait TestsApi /** * Set the API admin role as the current user via the API driver. */ - protected function actingAsApiAdmin() + protected function actingAsApiAdmin(): static { $this->actingAs($this->users->admin(), 'api'); From 402eb845abe3312f6e6fe7611acd41541d8be245 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Wed, 15 Mar 2023 11:37:03 +0000 Subject: [PATCH 3/3] Added examples, updated docs for image gallery api endpoints --- .../Api/ImageGalleryApiController.php | 18 ++++++-- dev/api/requests/image-gallery-update.json | 3 ++ dev/api/responses/image-gallery-create.json | 28 +++++++++++++ dev/api/responses/image-gallery-list.json | 41 +++++++++++++++++++ dev/api/responses/image-gallery-read.json | 28 +++++++++++++ dev/api/responses/image-gallery-update.json | 28 +++++++++++++ .../api-docs/parts/getting-started.blade.php | 4 +- 7 files changed, 145 insertions(+), 5 deletions(-) create mode 100644 dev/api/requests/image-gallery-update.json create mode 100644 dev/api/responses/image-gallery-create.json create mode 100644 dev/api/responses/image-gallery-list.json create mode 100644 dev/api/responses/image-gallery-read.json create mode 100644 dev/api/responses/image-gallery-update.json diff --git a/app/Http/Controllers/Api/ImageGalleryApiController.php b/app/Http/Controllers/Api/ImageGalleryApiController.php index a9fb3b103..3dba3d464 100644 --- a/app/Http/Controllers/Api/ImageGalleryApiController.php +++ b/app/Http/Controllers/Api/ImageGalleryApiController.php @@ -34,8 +34,8 @@ class ImageGalleryApiController extends ApiController } /** - * Get a listing of gallery images and drawings in the system. - * Requires visibility of the content they're originally uploaded to. + * Get a listing of images in the system. Includes gallery (page content) images and drawings. + * Requires visibility of the page they're originally uploaded to. */ public function list() { @@ -50,6 +50,11 @@ class ImageGalleryApiController extends ApiController /** * Create a new image in the system. + * Since "image" is expected to be a file, this needs to be a 'multipart/form-data' type request. + * The provided "uploaded_to" should be an existing page ID in the system. + * If the "name" parameter is omitted, the filename of the provided image file will be used instead. + * The "type" parameter should be 'gallery' for page content images, and 'drawio' should only be used + * when the file is a PNG file with diagrams.net image data embedded within. */ public function create(Request $request) { @@ -69,6 +74,10 @@ class ImageGalleryApiController extends ApiController /** * View the details of a single image. + * The "thumbs" response property contains links to scaled variants that BookStack may use in its UI. + * The "content" response property provides HTML and Markdown content, in the format that BookStack + * would typically use by default to add the image in page content, as a convenience. + * Actual image file data is not provided but can be fetched via the "url" response property. */ public function read(string $id) { @@ -78,7 +87,8 @@ class ImageGalleryApiController extends ApiController } /** - * Update an existing image in the system. + * Update the details of an existing image in the system. + * Only allows updating of the image name at this time. */ public function update(Request $request, string $id) { @@ -94,6 +104,8 @@ class ImageGalleryApiController extends ApiController /** * Delete an image from the system. + * Will also delete thumbnails for the image. + * Does not check or handle image usage so this could leave pages with broken image references. */ public function delete(string $id) { diff --git a/dev/api/requests/image-gallery-update.json b/dev/api/requests/image-gallery-update.json new file mode 100644 index 000000000..e332e3a8f --- /dev/null +++ b/dev/api/requests/image-gallery-update.json @@ -0,0 +1,3 @@ +{ + "name": "My updated image name" +} \ No newline at end of file diff --git a/dev/api/responses/image-gallery-create.json b/dev/api/responses/image-gallery-create.json new file mode 100644 index 000000000..e27824491 --- /dev/null +++ b/dev/api/responses/image-gallery-create.json @@ -0,0 +1,28 @@ +{ + "name": "cute-cat-image.png", + "path": "\/uploads\/images\/gallery\/2023-03\/cute-cat-image.png", + "url": "https:\/\/bookstack.example.com\/uploads\/images\/gallery\/2023-03\/cute-cat-image.png", + "type": "gallery", + "uploaded_to": 1, + "created_by": { + "id": 1, + "name": "Admin", + "slug": "admin" + }, + "updated_by": { + "id": 1, + "name": "Admin", + "slug": "admin" + }, + "updated_at": "2023-03-15 08:17:37", + "created_at": "2023-03-15 08:17:37", + "id": 618, + "thumbs": { + "gallery": "https:\/\/bookstack.example.com\/uploads\/images\/gallery\/2023-03\/thumbs-150-150\/cute-cat-image.png", + "display": "https:\/\/bookstack.example.com\/uploads\/images\/gallery\/2023-03\/scaled-1680-\/cute-cat-image.png" + }, + "content": { + "html": "\"cute-cat-image.png\"<\/a>", + "markdown": "![cute-cat-image.png](https:\/\/bookstack.example.com\/uploads\/images\/gallery\/2023-03\/scaled-1680-\/cute-cat-image.png)" + } +} \ No newline at end of file diff --git a/dev/api/responses/image-gallery-list.json b/dev/api/responses/image-gallery-list.json new file mode 100644 index 000000000..054d68a15 --- /dev/null +++ b/dev/api/responses/image-gallery-list.json @@ -0,0 +1,41 @@ +{ + "data": [ + { + "id": 1, + "name": "My cat scribbles", + "url": "https:\/\/bookstack.example.com\/uploads\/images\/gallery\/2023-02\/scribbles.jpg", + "path": "\/uploads\/images\/gallery\/2023-02\/scribbles.jpg", + "type": "gallery", + "uploaded_to": 1, + "created_by": 1, + "updated_by": 1, + "created_at": "2023-02-12T16:34:57.000000Z", + "updated_at": "2023-02-12T16:34:57.000000Z" + }, + { + "id": 2, + "name": "Drawing-1.png", + "url": "https:\/\/bookstack.example.com\/uploads\/images\/drawio\/2023-02\/drawing-1.png", + "path": "\/uploads\/images\/drawio\/2023-02\/drawing-1.png", + "type": "drawio", + "uploaded_to": 2, + "created_by": 2, + "updated_by": 2, + "created_at": "2023-02-12T16:39:19.000000Z", + "updated_at": "2023-02-12T16:39:19.000000Z" + }, + { + "id": 8, + "name": "beans.jpg", + "url": "https:\/\/bookstack.example.com\/uploads\/images\/gallery\/2023-02\/beans.jpg", + "path": "\/uploads\/images\/gallery\/2023-02\/beans.jpg", + "type": "gallery", + "uploaded_to": 6, + "created_by": 1, + "updated_by": 1, + "created_at": "2023-02-15T19:37:44.000000Z", + "updated_at": "2023-02-15T19:37:44.000000Z" + } + ], + "total": 3 +} \ No newline at end of file diff --git a/dev/api/responses/image-gallery-read.json b/dev/api/responses/image-gallery-read.json new file mode 100644 index 000000000..c6c468daa --- /dev/null +++ b/dev/api/responses/image-gallery-read.json @@ -0,0 +1,28 @@ +{ + "id": 618, + "name": "cute-cat-image.png", + "url": "https:\/\/bookstack.example.com\/uploads\/images\/gallery\/2023-03\/cute-cat-image.png", + "created_at": "2023-03-15 08:17:37", + "updated_at": "2023-03-15 08:17:37", + "created_by": { + "id": 1, + "name": "Admin", + "slug": "admin" + }, + "updated_by": { + "id": 1, + "name": "Admin", + "slug": "admin" + }, + "path": "\/uploads\/images\/gallery\/2023-03\/cute-cat-image.png", + "type": "gallery", + "uploaded_to": 1, + "thumbs": { + "gallery": "https:\/\/bookstack.example.com\/uploads\/images\/gallery\/2023-03\/thumbs-150-150\/cute-cat-image.png", + "display": "https:\/\/bookstack.example.com\/uploads\/images\/gallery\/2023-03\/scaled-1680-\/cute-cat-image.png" + }, + "content": { + "html": "\"cute-cat-image.png\"<\/a>", + "markdown": "![cute-cat-image.png](https:\/\/bookstack.example.com\/uploads\/images\/gallery\/2023-03\/scaled-1680-\/cute-cat-image.png)" + } +} \ No newline at end of file diff --git a/dev/api/responses/image-gallery-update.json b/dev/api/responses/image-gallery-update.json new file mode 100644 index 000000000..6e6168a1b --- /dev/null +++ b/dev/api/responses/image-gallery-update.json @@ -0,0 +1,28 @@ +{ + "id": 618, + "name": "My updated image name", + "url": "https:\/\/bookstack.example.com\/uploads\/images\/gallery\/2023-03\/cute-cat-image.png", + "created_at": "2023-03-15 08:17:37", + "updated_at": "2023-03-15 08:24:50", + "created_by": { + "id": 1, + "name": "Admin", + "slug": "admin" + }, + "updated_by": { + "id": 1, + "name": "Admin", + "slug": "admin" + }, + "path": "\/uploads\/images\/gallery\/2023-03\/cute-cat-image.png", + "type": "gallery", + "uploaded_to": 1, + "thumbs": { + "gallery": "https:\/\/bookstack.example.com\/uploads\/images\/gallery\/2023-03\/thumbs-150-150\/cute-cat-image.png", + "display": "https:\/\/bookstack.example.com\/uploads\/images\/gallery\/2023-03\/scaled-1680-\/cute-cat-image.png" + }, + "content": { + "html": "\"My<\/a>", + "markdown": "![My updated image name](https:\/\/bookstack.example.com\/uploads\/images\/gallery\/2023-03\/scaled-1680-\/cute-cat-image.png)" + } +} \ No newline at end of file diff --git a/resources/views/api-docs/parts/getting-started.blade.php b/resources/views/api-docs/parts/getting-started.blade.php index 7358b5cd7..75b71c6be 100644 --- a/resources/views/api-docs/parts/getting-started.blade.php +++ b/resources/views/api-docs/parts/getting-started.blade.php @@ -14,11 +14,11 @@ HTTP POST calls upon events occurring in BookStack.
  • - Visual Theme System - + Visual Theme System - Methods to override views, translations and icons within BookStack.
  • - Logical Theme System - + Logical Theme System - Methods to extend back-end functionality within BookStack.