]> BookStack Code Mirror - bookstack/commitdiff
Start recycle bin API endpoints: list, restore, delete
authorjulesdevops <redacted>
Wed, 6 Apr 2022 20:57:18 +0000 (22:57 +0200)
committerjulesdevops <redacted>
Thu, 7 Apr 2022 20:34:00 +0000 (22:34 +0200)
app/Entities/Repos/DeletionRepo.php [new file with mode: 0644]
app/Http/Controllers/Api/RecycleBinApiController.php [new file with mode: 0644]
app/Http/Controllers/RecycleBinController.php
routes/api.php
tests/Api/RecycleBinApiTest.php [new file with mode: 0644]

diff --git a/app/Entities/Repos/DeletionRepo.php b/app/Entities/Repos/DeletionRepo.php
new file mode 100644 (file)
index 0000000..8fad4e6
--- /dev/null
@@ -0,0 +1,34 @@
+<?php
+
+namespace BookStack\Entities\Repos;
+
+use BookStack\Actions\ActivityType;
+use BookStack\Entities\Models\Deletion;
+use BookStack\Entities\Tools\TrashCan;
+use BookStack\Facades\Activity;
+
+class DeletionRepo
+{
+    private TrashCan $trashCan;
+
+    public function __construct(TrashCan $trashCan)
+    {
+        $this->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 (file)
index 0000000..162b27a
--- /dev/null
@@ -0,0 +1,45 @@
+<?php
+
+namespace BookStack\Http\Controllers\Api;
+
+use BookStack\Actions\ActivityType;
+use BookStack\Entities\Models\Deletion;
+use BookStack\Entities\Repos\DeletionRepo;
+use BookStack\Entities\Tools\TrashCan;
+
+class RecycleBinApiController extends ApiController
+{
+    public function __construct()
+    {
+        $this->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
index 1cffb161cc8a3f94e68d8d6eaeb72ab2e10d7730..82e3f660bddb92475ee5f79f9cdcbd993b1550f3 100644 (file)
@@ -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]));
 
index a87169ee51b805144f2bbfdda85dc9ba5e1b827b..465f2392c831702f11cc1a3eadc41cae1475db91 100644 (file)
@@ -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 (file)
index 0000000..9371e06
--- /dev/null
@@ -0,0 +1,136 @@
+<?php
+
+namespace Tests\Api;
+
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Chapter;
+use BookStack\Entities\Models\Deletion;
+use BookStack\Entities\Models\Page;
+use Carbon\Carbon;
+use Illuminate\Support\Collection;
+use Illuminate\Support\Facades\DB;
+use Tests\TestCase;
+
+class RecycleBinApiTest extends TestCase
+{
+    use TestsApi;
+
+    protected string $baseEndpoint = '/api/recycle_bin';
+
+    protected array $endpointMap = [
+        ['get', '/api/recycle_bin'],
+        ['put', '/api/recycle_bin/1'],
+        ['delete', '/api/recycle_bin/1'],
+    ];
+
+    public function test_settings_manage_permission_needed_for_all_endpoints()
+    {
+        $editor = $this->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);
+    }
+}