use Illuminate\Database\Eloquent\Relations\MorphTo;
/**
+ * @property int $id
+ * @property int $deleted_by
+ * @property string $deletable_type
+ * @property int $deletable_id
* @property Deletable $deletable
*/
class Deletion extends Model implements Loggable
{
+ protected $hidden = [];
+
/**
* Get the related deletable record.
*/
namespace BookStack\Http\Controllers\Api;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\BookChild;
+use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Deletion;
use BookStack\Entities\Repos\DeletionRepo;
use Closure;
+use Illuminate\Database\Eloquent\Builder;
class RecycleBinApiController extends ApiController
{
- protected $fieldsToExpose = [
- 'id', 'deleted_by', 'created_at', 'updated_at', 'deletable_type', 'deletable_id',
- ];
-
public function __construct()
{
$this->middleware(function ($request, $next) {
/**
* Get a top-level listing of the items in the recycle bin.
- * Requires the permission to manage settings and restrictions.
+ * The "deletable" property will reflect the main item deleted.
+ * For books and chapters, counts of child pages/chapters will
+ * be loaded within this "deletable" data.
+ * For chapters & pages, the parent item will be loaded within this "deletable" data.
+ * Requires permission to manage both system settings and permissions.
*/
public function list()
{
/**
* Restore a single deletion from the recycle bin.
- * You must provide the deletion id, not the id of the corresponding deleted item.
+ * Requires permission to manage both system settings and permissions.
*/
- public function restore(DeletionRepo $deletionRepo, string $id)
+ public function restore(DeletionRepo $deletionRepo, string $deletionId)
{
- $restoreCount = $deletionRepo->restore((int) $id);
+ $restoreCount = $deletionRepo->restore(intval($deletionId));
return response()->json(['restore_count' => $restoreCount]);
}
/**
* Remove a single deletion from the recycle bin.
* Use this endpoint carefully as it will entirely remove the underlying deleted items from the system.
- * You must provide the deletion id, not the id of the corresponding deleted item.
+ * Requires permission to manage both system settings and permissions.
*/
- public function destroy(DeletionRepo $deletionRepo, string $id)
+ public function destroy(DeletionRepo $deletionRepo, string $deletionId)
{
- $deleteCount = $deletionRepo->destroy((int) $id);
+ $deleteCount = $deletionRepo->destroy(intval($deletionId));
return response()->json(['delete_count' => $deleteCount]);
}
+ /**
+ * Load some related details for the deletion listing.
+ */
protected function listFormatter(Deletion $deletion)
{
- $deletion->makeVisible($this->fieldsToExpose);
- $deletion->makeHidden('deletable');
-
$deletable = $deletion->deletable;
- $isBook = $deletion->deletable_type === "BookStack\Book";
- $parent = null;
- $children = null;
+ $withTrashedQuery = fn(Builder $query) => $query->withTrashed();
- if ($isBook) {
- $chapterCount = $deletable->chapters()->withTrashed()->count();
- $children['BookStack\Chapter'] = $chapterCount;
+ if ($deletable instanceof BookChild) {
+ $parent = $deletable->getParent();
+ $parent->setAttribute('type', $parent->getType());
+ $deletable->setRelation('parent', $parent);
}
- if ($isBook || $deletion->deletable_type === "BookStack\Chapter") {
- $pageCount = $deletable->pages()->withTrashed()->count();
- $children['BookStack\Page'] = $pageCount;
+ if ($deletable instanceof Book || $deletable instanceof Chapter) {
+ $countsToLoad = ['pages' => $withTrashedQuery];
+ if ($deletable instanceof Book) {
+ $countsToLoad['chapters'] = $withTrashedQuery;
+ }
+ $deletable->loadCount($countsToLoad);
}
-
- $parentEntity = $deletable->getParent();
- $parent = null;
-
- if ($parentEntity) {
- $parent['type'] = $parentEntity->getMorphClass();
- $parent['id'] = $parentEntity->getKey();
- }
-
- $deletion->setAttribute('parent', $parent);
- $deletion->setAttribute('children', $children);
}
}
--- /dev/null
+{
+ "data": [
+ {
+ "id": 18,
+ "deleted_by": 1,
+ "created_at": "2022-04-20T12:57:46.000000Z",
+ "updated_at": "2022-04-20T12:57:46.000000Z",
+ "deletable_type": "page",
+ "deletable_id": 2582,
+ "deletable": {
+ "id": 2582,
+ "book_id": 25,
+ "chapter_id": 0,
+ "name": "A Wonderful Page",
+ "slug": "a-wonderful-page",
+ "priority": 9,
+ "created_at": "2022-02-08T00:44:45.000000Z",
+ "updated_at": "2022-04-20T12:57:46.000000Z",
+ "created_by": 1,
+ "updated_by": 1,
+ "draft": false,
+ "revision_count": 1,
+ "template": false,
+ "owned_by": 1,
+ "editor": "wysiwyg",
+ "book_slug": "a-great-book",
+ "parent": {
+ "id": 25,
+ "name": "A Great Book",
+ "slug": "a-great-book",
+ "description": "",
+ "created_at": "2022-01-24T16:14:28.000000Z",
+ "updated_at": "2022-03-06T15:14:50.000000Z",
+ "created_by": 1,
+ "updated_by": 1,
+ "owned_by": 1,
+ "type": "book"
+ }
+ }
+ },
+ {
+ "id": 19,
+ "deleted_by": 1,
+ "created_at": "2022-04-25T16:07:46.000000Z",
+ "updated_at": "2022-04-25T16:07:46.000000Z",
+ "deletable_type": "book",
+ "deletable_id": 13,
+ "deletable": {
+ "id": 13,
+ "name": "A Big Book!",
+ "slug": "a-big-book",
+ "description": "This is a very large book with loads of cool stuff in it!",
+ "created_at": "2021-11-08T11:26:43.000000Z",
+ "updated_at": "2022-04-25T16:07:47.000000Z",
+ "created_by": 27,
+ "updated_by": 1,
+ "owned_by": 1,
+ "pages_count": 208,
+ "chapters_count": 50
+ }
+ }
+ ],
+ "total": 2
+}
\ No newline at end of file
+++ /dev/null
-{
- "data": [
- {
- "id": 25,
- "deleted_by": 1,
- "created_at": "2022-04-24T07:59:34.000000Z",
- "updated_at": "2022-04-24T07:59:34.000000Z",
- "deletable_type": "BookStack\\Book",
- "deletable_id": 4,
- "parent": {
- "type": "BookStack\\Book",
- "id": 25
- },
- "children": {
- "BookStack\\Chapter": 0,
- "BookStack\\Page": 1
- }
- },
- {
- "id": 26,
- "deleted_by": 1,
- "created_at": "2022-04-24T07:59:35.000000Z",
- "updated_at": "2022-04-24T07:59:35.000000Z",
- "deletable_type": "BookStack\\Book",
- "deletable_id": 3,
- "parent": [],
- "children": {
- "BookStack\\Chapter": 1,
- "BookStack\\Page": 1
- }
- }
- ],
- "total": 2
-}
\ No newline at end of file
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']);
+Route::get('recycle-bin', [RecycleBinApiController::class, 'list']);
+Route::put('recycle-bin/{deletionId}', [RecycleBinApiController::class, 'restore']);
+Route::delete('recycle-bin/{deletionId}', [RecycleBinApiController::class, 'destroy']);
{
use TestsApi;
- protected string $baseEndpoint = '/api/recycle_bin';
+ protected string $baseEndpoint = '/api/recycle-bin';
protected array $endpointMap = [
- ['get', '/api/recycle_bin'],
- ['put', '/api/recycle_bin/1'],
- ['delete', '/api/recycle_bin/1'],
+ ['get', '/api/recycle-bin'],
+ ['put', '/api/recycle-bin/1'],
+ ['delete', '/api/recycle-bin/1'],
];
public function test_settings_manage_permission_needed_for_all_endpoints()
public function test_index_endpoint_returns_expected_page()
{
- $this->actingAsAuthorizedUser();
+ $admin = $this->getAdmin();
$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());
+ $book = Book::query()->first();
+ $this->actingAs($admin)->delete($page->getUrl());
+ $this->delete($book->getUrl());
$deletions = Deletion::query()->orderBy('id')->get();
$expectedData = $deletions
->zip([$page, $book])
- ->map(function (Collection $data) use ($editor) {
+ ->map(function (Collection $data) use ($admin) {
return [
'id' => $data[0]->id,
- 'deleted_by' => $editor->getKey(),
+ 'deleted_by' => $admin->id,
'created_at' => $data[0]->created_at->toJson(),
'updated_at' => $data[0]->updated_at->toJson(),
'deletable_type' => $data[1]->getMorphClass(),
- 'deletable_id' => $data[1]->getKey(),
+ 'deletable_id' => $data[1]->id,
+ 'deletable' => [
+ 'name' => $data[1]->name,
+ ],
];
});
]);
}
- public function test_index_endpoint_returns_children()
+ public function test_index_endpoint_returns_children_count()
{
- $this->actingAsAuthorizedUser();
+ $admin = $this->getAdmin();
$book = Book::query()->whereHas('pages')->whereHas('chapters')->withCount(['pages', 'chapters'])->first();
- $editor = $this->getEditor();
- $this->actingAs($editor)->delete($book->getUrl());
+ $this->actingAs($admin)->delete($book->getUrl());
$deletion = Deletion::query()->orderBy('id')->first();
$expectedData = [
[
- 'id' => $deletion->getKey(),
- 'deleted_by' => $editor->getKey(),
- 'created_at' => $deletion->created_at->toJson(),
- 'updated_at' => $deletion->updated_at->toJson(),
- 'deletable_type' => $book->getMorphClass(),
- 'deletable_id' => $book->getKey(),
- 'children' => [
- 'BookStack\Page' => $book->pages_count,
- 'BookStack\Chapter' => $book->chapters_count,
+ 'id' => $deletion->id,
+ 'deletable' => [
+ 'pages_count' => $book->pages_count,
+ 'chapters_count' => $book->chapters_count,
],
- 'parent' => null,
],
];
public function test_index_endpoint_returns_parent()
{
- $this->actingAsAuthorizedUser();
-
+ $admin = $this->getAdmin();
$page = Page::query()->whereHas('chapter')->with('chapter')->first();
- $editor = $this->getEditor();
- $this->actingAs($editor)->delete($page->getUrl());
-
+ $this->actingAs($admin)->delete($page->getUrl());
$deletion = Deletion::query()->orderBy('id')->first();
$resp = $this->getJson($this->baseEndpoint);
$expectedData = [
[
- 'id' => $deletion->getKey(),
- 'deleted_by' => $editor->getKey(),
- 'created_at' => $deletion->created_at->toJson(),
- 'updated_at' => $deletion->updated_at->toJson(),
- 'deletable_type' => $page->getMorphClass(),
- 'deletable_id' => $page->getKey(),
- 'parent' => [
- 'type' => 'BookStack\Chapter',
- 'id' => $page->chapter->getKey(),
- ],
- 'children' => null,
+ 'id' => $deletion->id,
+ 'deletable' => [
+ 'parent' => [
+ 'id' => $page->chapter->id,
+ 'name' => $page->chapter->name,
+ 'type' => 'chapter'
+ ]
+ ]
],
];
public function test_restore_endpoint()
{
- $this->actingAsAuthorizedUser();
-
$page = Page::query()->first();
- $editor = $this->getEditor();
- $this->actingAs($editor)->delete($page->getUrl());
+ $this->asAdmin()->delete($page->getUrl());
$page->refresh();
$deletion = Deletion::query()->orderBy('id')->first();
$this->assertDatabaseHas('pages', [
- 'id' => $page->getKey(),
+ 'id' => $page->id,
'deleted_at' => $page->deleted_at,
]);
- $this->putJson($this->baseEndpoint . '/' . $deletion->getKey());
+ $resp = $this->putJson($this->baseEndpoint . '/' . $deletion->id);
+ $resp->assertJson([
+ 'restore_count' => 1
+ ]);
$this->assertDatabaseHas('pages', [
- 'id' => $page->getKey(),
+ 'id' => $page->id,
'deleted_at' => null,
]);
}
public function test_destroy_endpoint()
{
- $this->actingAsAuthorizedUser();
-
$page = Page::query()->first();
- $editor = $this->getEditor();
- $this->actingAs($editor)->delete($page->getUrl());
+ $this->asAdmin()->delete($page->getUrl());
$page->refresh();
$deletion = Deletion::query()->orderBy('id')->first();
$this->assertDatabaseHas('pages', [
- 'id' => $page->getKey(),
+ 'id' => $page->id,
'deleted_at' => $page->deleted_at,
]);
- $this->deleteJson($this->baseEndpoint . '/' . $deletion->getKey());
- $this->assertDatabaseMissing('pages', ['id' => $page->getKey()]);
- }
+ $resp = $this->deleteJson($this->baseEndpoint . '/' . $deletion->id);
+ $resp->assertJson([
+ 'delete_count' => 1
+ ]);
- private function actingAsAuthorizedUser()
- {
- $editor = $this->getEditor();
- $this->giveUserPermissions($editor, ['restrictions-manage-all']);
- $this->giveUserPermissions($editor, ['settings-manage']);
- $this->actingAs($editor);
+ $this->assertDatabaseMissing('pages', ['id' => $page->id]);
}
}