From 55e52e45fb11f5b733eacb010bd23aa9716466fa Mon Sep 17 00:00:00 2001 From: julesdevops Date: Wed, 6 Apr 2022 22:57:18 +0200 Subject: [PATCH] Start recycle bin API endpoints: list, restore, delete --- app/Entities/Repos/DeletionRepo.php | 34 +++++ .../Api/RecycleBinApiController.php | 45 ++++++ app/Http/Controllers/RecycleBinController.php | 15 +- routes/api.php | 5 + tests/Api/RecycleBinApiTest.php | 136 ++++++++++++++++++ 5 files changed, 225 insertions(+), 10 deletions(-) create mode 100644 app/Entities/Repos/DeletionRepo.php create mode 100644 app/Http/Controllers/Api/RecycleBinApiController.php create mode 100644 tests/Api/RecycleBinApiTest.php diff --git a/app/Entities/Repos/DeletionRepo.php b/app/Entities/Repos/DeletionRepo.php new file mode 100644 index 000000000..8fad4e6b0 --- /dev/null +++ b/app/Entities/Repos/DeletionRepo.php @@ -0,0 +1,34 @@ +trashCan = $trashCan; + } + + public function restore(int $id): int + { + /** @var Deletion $deletion */ + $deletion = Deletion::query()->findOrFail($id); + Activity::add(ActivityType::RECYCLE_BIN_RESTORE, $deletion); + return $this->trashCan->restoreFromDeletion($deletion); + } + + public function destroy(int $id): int + { + /** @var Deletion $deletion */ + $deletion = Deletion::query()->findOrFail($id); + Activity::add(ActivityType::RECYCLE_BIN_DESTROY, $deletion); + return $this->trashCan->destroyFromDeletion($deletion); + } +} diff --git a/app/Http/Controllers/Api/RecycleBinApiController.php b/app/Http/Controllers/Api/RecycleBinApiController.php new file mode 100644 index 000000000..162b27adb --- /dev/null +++ b/app/Http/Controllers/Api/RecycleBinApiController.php @@ -0,0 +1,45 @@ +middleware(function ($request, $next) { + $this->checkPermission('settings-manage'); + $this->checkPermission('restrictions-manage-all'); + + return $next($request); + }); + } + + public function list() + { + return $this->apiListingResponse(Deletion::query(), [ + 'id', + 'deleted_by', + 'created_at', + 'updated_at', + 'deletable_type', + 'deletable_id' + ]); + } + + public function restore(DeletionRepo $deletionRepo, string $id) + { + $restoreCount = $deletionRepo->restore((int) $id); + return response()->json(['restore_count' => $restoreCount]); + } + + public function destroy(DeletionRepo $deletionRepo, string $id) + { + $deleteCount = $deletionRepo->destroy((int) $id); + return response()->json(['delete_count' => $deleteCount]); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/RecycleBinController.php b/app/Http/Controllers/RecycleBinController.php index 1cffb161c..82e3f660b 100644 --- a/app/Http/Controllers/RecycleBinController.php +++ b/app/Http/Controllers/RecycleBinController.php @@ -5,6 +5,7 @@ namespace BookStack\Http\Controllers; use BookStack\Actions\ActivityType; use BookStack\Entities\Models\Deletion; use BookStack\Entities\Models\Entity; +use BookStack\Entities\Repos\DeletionRepo; use BookStack\Entities\Tools\TrashCan; class RecycleBinController extends Controller @@ -73,12 +74,9 @@ class RecycleBinController extends Controller * * @throws \Exception */ - public function restore(string $id) + public function restore(DeletionRepo $deletionRepo, string $id) { - /** @var Deletion $deletion */ - $deletion = Deletion::query()->findOrFail($id); - $this->logActivity(ActivityType::RECYCLE_BIN_RESTORE, $deletion); - $restoreCount = (new TrashCan())->restoreFromDeletion($deletion); + $restoreCount = $deletionRepo->restore((int) $id); $this->showSuccessNotification(trans('settings.recycle_bin_restore_notification', ['count' => $restoreCount])); @@ -103,12 +101,9 @@ class RecycleBinController extends Controller * * @throws \Exception */ - public function destroy(string $id) + public function destroy(DeletionRepo $deletionRepo, string $id) { - /** @var Deletion $deletion */ - $deletion = Deletion::query()->findOrFail($id); - $this->logActivity(ActivityType::RECYCLE_BIN_DESTROY, $deletion); - $deleteCount = (new TrashCan())->destroyFromDeletion($deletion); + $deleteCount = $deletionRepo->destroy((int) $id); $this->showSuccessNotification(trans('settings.recycle_bin_destroy_notification', ['count' => $deleteCount])); diff --git a/routes/api.php b/routes/api.php index a87169ee5..465f2392c 100644 --- a/routes/api.php +++ b/routes/api.php @@ -9,6 +9,7 @@ use BookStack\Http\Controllers\Api\ChapterApiController; use BookStack\Http\Controllers\Api\ChapterExportApiController; use BookStack\Http\Controllers\Api\PageApiController; use BookStack\Http\Controllers\Api\PageExportApiController; +use BookStack\Http\Controllers\Api\RecycleBinApiController; use BookStack\Http\Controllers\Api\SearchApiController; use BookStack\Http\Controllers\Api\UserApiController; use Illuminate\Support\Facades\Route; @@ -72,3 +73,7 @@ Route::post('users', [UserApiController::class, 'create']); Route::get('users/{id}', [UserApiController::class, 'read']); Route::put('users/{id}', [UserApiController::class, 'update']); Route::delete('users/{id}', [UserApiController::class, 'delete']); + +Route::get('recycle_bin', [RecycleBinApiController::class, 'list']); +Route::put('recycle_bin/{id}', [RecycleBinApiController::class, 'restore']); +Route::delete('recycle_bin/{id}', [RecycleBinApiController::class, 'destroy']); diff --git a/tests/Api/RecycleBinApiTest.php b/tests/Api/RecycleBinApiTest.php new file mode 100644 index 000000000..9371e06e8 --- /dev/null +++ b/tests/Api/RecycleBinApiTest.php @@ -0,0 +1,136 @@ +getEditor(); + $this->giveUserPermissions($editor, ['settings-manage']); + $this->actingAs($editor); + + foreach ($this->endpointMap as [$method, $uri]) { + $resp = $this->json($method, $uri); + $resp->assertStatus(403); + $resp->assertJson($this->permissionErrorResponse()); + } + } + + public function test_restrictions_manage_all_permission_neeed_for_all_endpoints() + { + $editor = $this->getEditor(); + $this->giveUserPermissions($editor, ['restrictions-manage-all']); + $this->actingAs($editor); + + foreach ($this->endpointMap as [$method, $uri]) { + $resp = $this->json($method, $uri); + $resp->assertStatus(403); + $resp->assertJson($this->permissionErrorResponse()); + } + } + + public function test_index_endpoint_returns_expected_page() + { + $this->actingAsAuthorizedUser(); + + $page = Page::query()->first(); + $book = Book::query()->whereHas('pages')->whereHas('chapters')->withCount(['pages', 'chapters'])->first(); + $editor = $this->getEditor(); + $this->actingAs($editor)->delete($page->getUrl()); + $this->actingAs($editor)->delete($book->getUrl()); + + $deletions = Deletion::query()->orderBy('id')->get(); + + $resp = $this->getJson($this->baseEndpoint); + + $expectedData = $deletions + ->zip([$page, $book]) + ->map(function (Collection $data) use ($editor) { + return [ + 'id' => $data[0]->id, + 'deleted_by' => $editor->getKey(), + 'created_at' => $data[0]->created_at->toJson(), + 'updated_at' => $data[0]->updated_at->toJson(), + 'deletable_type' => $data[1]->getMorphClass(), + 'deletable_id' => $data[1]->getKey() + ]; + }); + + $resp->assertJson([ + 'data' => $expectedData->values()->all(), + 'total' => 2 + ]); + } + + public function test_restore_endpoint() + { + $this->actingAsAuthorizedUser(); + + $page = Page::query()->first(); + $editor = $this->getEditor(); + $this->actingAs($editor)->delete($page->getUrl()); + $page->refresh(); + + $deletion = Deletion::query()->orderBy('id')->first(); + + $this->assertDatabaseHas('pages', [ + 'id' => $page->getKey(), + 'deleted_at' => $page->deleted_at + ]); + + $this->putJson($this->baseEndpoint . '/' . $deletion->getKey()); + + $this->assertDatabaseHas('pages', [ + 'id' => $page->getKey(), + 'deleted_at' => null + ]); + } + + public function test_destroy_endpoint() + { + $this->actingAsAuthorizedUser(); + + $page = Page::query()->first(); + $editor = $this->getEditor(); + $this->actingAs($editor)->delete($page->getUrl()); + $page->refresh(); + + $deletion = Deletion::query()->orderBy('id')->first(); + + $this->assertDatabaseHas('pages', [ + 'id' => $page->getKey(), + 'deleted_at' => $page->deleted_at + ]); + + $this->deleteJson($this->baseEndpoint . '/' . $deletion->getKey()); + $this->assertDatabaseMissing('pages', ['id' => $page->getKey()]); + } + + private function actingAsAuthorizedUser() + { + $editor = $this->getEditor(); + $this->giveUserPermissions($editor, ['restrictions-manage-all']); + $this->giveUserPermissions($editor, ['settings-manage']); + $this->actingAs($editor); + } +}