diff --git a/app/Actions/ViewService.php b/app/Actions/ViewService.php index 324bfaa4e..aa75abb72 100644 --- a/app/Actions/ViewService.php +++ b/app/Actions/ViewService.php @@ -79,29 +79,26 @@ class ViewService /** * Get all recently viewed entities for the current user. - * @param int $count - * @param int $page - * @param Entity|bool $filterModel - * @return mixed */ - public function getUserRecentlyViewed($count = 10, $page = 0, $filterModel = false) + public function getUserRecentlyViewed(int $count = 10, int $page = 1) { $user = user(); if ($user === null || $user->isDefault()) { return collect(); } - $query = $this->permissionService - ->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type'); - - if ($filterModel) { - $query = $query->where('viewable_type', '=', $filterModel->getMorphClass()); + $all = collect(); + /** @var Entity $instance */ + foreach ($this->entityProvider->all() as $name => $instance) { + $items = $instance::visible()->withLastView() + ->orderBy('last_viewed_at', 'desc') + ->skip($count * ($page - 1)) + ->take($count) + ->get(); + $all = $all->concat($items); } - $query = $query->where('user_id', '=', $user->id); - $viewables = $query->with('viewable')->orderBy('updated_at', 'desc') - ->skip($count * $page)->take($count)->get()->pluck('viewable'); - return $viewables; + return $all->sortByDesc('last_viewed_at')->slice(0, $count); } /** diff --git a/app/Auth/Permissions/PermissionService.php b/app/Auth/Permissions/PermissionService.php index 97cc1ca24..2609779bf 100644 --- a/app/Auth/Permissions/PermissionService.php +++ b/app/Auth/Permissions/PermissionService.php @@ -51,11 +51,6 @@ class PermissionService /** * PermissionService constructor. - * @param JointPermission $jointPermission - * @param EntityPermission $entityPermission - * @param Role $role - * @param Connection $db - * @param EntityProvider $entityProvider */ public function __construct( JointPermission $jointPermission, @@ -176,7 +171,7 @@ class PermissionService }); // Chunk through all bookshelves - $this->entityProvider->bookshelf->newQuery()->select(['id', 'restricted', 'created_by']) + $this->entityProvider->bookshelf->newQuery()->withTrashed()->select(['id', 'restricted', 'created_by']) ->chunk(50, function ($shelves) use ($roles) { $this->buildJointPermissionsForShelves($shelves, $roles); }); @@ -188,11 +183,11 @@ class PermissionService */ protected function bookFetchQuery() { - return $this->entityProvider->book->newQuery() + return $this->entityProvider->book->withTrashed()->newQuery() ->select(['id', 'restricted', 'created_by'])->with(['chapters' => function ($query) { - $query->select(['id', 'restricted', 'created_by', 'book_id']); + $query->withTrashed()->select(['id', 'restricted', 'created_by', 'book_id']); }, 'pages' => function ($query) { - $query->select(['id', 'restricted', 'created_by', 'book_id', 'chapter_id']); + $query->withTrashed()->select(['id', 'restricted', 'created_by', 'book_id', 'chapter_id']); }]); } diff --git a/app/Entities/DeleteRecord.php b/app/Entities/DeleteRecord.php new file mode 100644 index 000000000..84b37f5a3 --- /dev/null +++ b/app/Entities/DeleteRecord.php @@ -0,0 +1,41 @@ +morphTo(); + } + + /** + * The the user that performed the deletion. + */ + public function deletedBy(): BelongsTo + { + return $this->belongsTo(User::class); + } + + /** + * Create a new deletion record for the provided entity. + */ + public static function createForEntity(Entity $entity): DeleteRecord + { + $record = (new self())->forceFill([ + 'deleted_by' => user()->id, + 'deletable_type' => $entity->getMorphClass(), + 'deletable_id' => $entity->id, + ]); + $record->save(); + return $record; + } + +} diff --git a/app/Entities/Entity.php b/app/Entities/Entity.php index cc7df46d4..d1a8664e4 100644 --- a/app/Entities/Entity.php +++ b/app/Entities/Entity.php @@ -12,6 +12,7 @@ use Carbon\Carbon; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Relations\MorphMany; +use Illuminate\Database\Eloquent\SoftDeletes; /** * Class Entity @@ -36,6 +37,7 @@ use Illuminate\Database\Eloquent\Relations\MorphMany; */ class Entity extends Ownable { + use SoftDeletes; /** * @var string - Name of property where the main text content is found @@ -193,13 +195,20 @@ class Entity extends Ownable /** * Get the entity jointPermissions this is connected to. - * @return MorphMany */ - public function jointPermissions() + public function jointPermissions(): MorphMany { return $this->morphMany(JointPermission::class, 'entity'); } + /** + * Get the related delete records for this entity. + */ + public function deleteRecords(): MorphMany + { + return $this->morphMany(DeleteRecord::class, 'deletable'); + } + /** * Check if this instance or class is a certain type of entity. * Examples of $type are 'page', 'book', 'chapter' diff --git a/app/Entities/Managers/TrashCan.php b/app/Entities/Managers/TrashCan.php index 1a32294fc..9a21f5e2c 100644 --- a/app/Entities/Managers/TrashCan.php +++ b/app/Entities/Managers/TrashCan.php @@ -3,6 +3,7 @@ use BookStack\Entities\Book; use BookStack\Entities\Bookshelf; use BookStack\Entities\Chapter; +use BookStack\Entities\DeleteRecord; use BookStack\Entities\Entity; use BookStack\Entities\HasCoverImage; use BookStack\Entities\Page; @@ -11,46 +12,67 @@ use BookStack\Facades\Activity; use BookStack\Uploads\AttachmentService; use BookStack\Uploads\ImageService; use Exception; -use Illuminate\Contracts\Container\BindingResolutionException; class TrashCan { /** - * Remove a bookshelf from the system. - * @throws Exception + * Send a shelf to the recycle bin. */ - public function destroyShelf(Bookshelf $shelf) + public function softDestroyShelf(Bookshelf $shelf) { - $this->destroyCommonRelations($shelf); + DeleteRecord::createForEntity($shelf); $shelf->delete(); } /** - * Remove a book from the system. - * @throws NotifyException - * @throws BindingResolutionException + * Send a book to the recycle bin. + * @throws Exception */ - public function destroyBook(Book $book) + public function softDestroyBook(Book $book) { + DeleteRecord::createForEntity($book); + foreach ($book->pages as $page) { - $this->destroyPage($page); + $this->softDestroyPage($page, false); } foreach ($book->chapters as $chapter) { - $this->destroyChapter($chapter); + $this->softDestroyChapter($chapter, false); } - $this->destroyCommonRelations($book); $book->delete(); } /** - * Remove a page from the system. - * @throws NotifyException + * Send a chapter to the recycle bin. + * @throws Exception */ - public function destroyPage(Page $page) + public function softDestroyChapter(Chapter $chapter, bool $recordDelete = true) { + if ($recordDelete) { + DeleteRecord::createForEntity($chapter); + } + + if (count($chapter->pages) > 0) { + foreach ($chapter->pages as $page) { + $this->softDestroyPage($page, false); + } + } + + $chapter->delete(); + } + + /** + * Send a page to the recycle bin. + * @throws Exception + */ + public function softDestroyPage(Page $page, bool $recordDelete = true) + { + if ($recordDelete) { + DeleteRecord::createForEntity($page); + } + // Check if set as custom homepage & remove setting if not used or throw error if active $customHome = setting('app-homepage', '0:'); if (intval($page->id) === intval(explode(':', $customHome)[0])) { @@ -60,6 +82,64 @@ class TrashCan setting()->remove('app-homepage'); } + $page->delete(); + } + + /** + * Remove a bookshelf from the system. + * @throws Exception + */ + public function destroyShelf(Bookshelf $shelf) + { + $this->destroyCommonRelations($shelf); + $shelf->forceDelete(); + } + + /** + * Remove a book from the system. + * Destroys any child chapters and pages. + * @throws Exception + */ + public function destroyBook(Book $book) + { + $pages = $book->pages()->withTrashed()->get(); + foreach ($pages as $page) { + $this->destroyPage($page); + } + + $chapters = $book->chapters()->withTrashed()->get(); + foreach ($chapters as $chapter) { + $this->destroyChapter($chapter); + } + + $this->destroyCommonRelations($book); + $book->forceDelete(); + } + + /** + * Remove a chapter from the system. + * Destroys all pages within. + * @throws Exception + */ + public function destroyChapter(Chapter $chapter) + { + $pages = $chapter->pages()->withTrashed()->get(); + if (count($pages)) { + foreach ($pages as $page) { + $this->destroyPage($page); + } + } + + $this->destroyCommonRelations($chapter); + $chapter->forceDelete(); + } + + /** + * Remove a page from the system. + * @throws Exception + */ + public function destroyPage(Page $page) + { $this->destroyCommonRelations($page); // Delete Attached Files @@ -68,24 +148,7 @@ class TrashCan $attachmentService->deleteFile($attachment); } - $page->delete(); - } - - /** - * Remove a chapter from the system. - * @throws Exception - */ - public function destroyChapter(Chapter $chapter) - { - if (count($chapter->pages) > 0) { - foreach ($chapter->pages as $page) { - $page->chapter_id = 0; - $page->save(); - } - } - - $this->destroyCommonRelations($chapter); - $chapter->delete(); + $page->forceDelete(); } /** @@ -100,6 +163,7 @@ class TrashCan $entity->comments()->delete(); $entity->jointPermissions()->delete(); $entity->searchTerms()->delete(); + $entity->deleteRecords()->delete(); if ($entity instanceof HasCoverImage && $entity->cover) { $imageService = app()->make(ImageService::class); diff --git a/app/Entities/Repos/BookRepo.php b/app/Entities/Repos/BookRepo.php index 70db0fa65..bb1895b36 100644 --- a/app/Entities/Repos/BookRepo.php +++ b/app/Entities/Repos/BookRepo.php @@ -123,12 +123,11 @@ class BookRepo /** * Remove a book from the system. - * @throws NotifyException - * @throws BindingResolutionException + * @throws Exception */ public function destroy(Book $book) { $trashCan = new TrashCan(); - $trashCan->destroyBook($book); + $trashCan->softDestroyBook($book); } } diff --git a/app/Entities/Repos/BookshelfRepo.php b/app/Entities/Repos/BookshelfRepo.php index ba687c6f6..eb1536da7 100644 --- a/app/Entities/Repos/BookshelfRepo.php +++ b/app/Entities/Repos/BookshelfRepo.php @@ -174,6 +174,6 @@ class BookshelfRepo public function destroy(Bookshelf $shelf) { $trashCan = new TrashCan(); - $trashCan->destroyShelf($shelf); + $trashCan->softDestroyShelf($shelf); } } diff --git a/app/Entities/Repos/ChapterRepo.php b/app/Entities/Repos/ChapterRepo.php index c6f3a2d2f..c1573f5db 100644 --- a/app/Entities/Repos/ChapterRepo.php +++ b/app/Entities/Repos/ChapterRepo.php @@ -6,10 +6,7 @@ use BookStack\Entities\Managers\BookContents; use BookStack\Entities\Managers\TrashCan; use BookStack\Exceptions\MoveOperationException; use BookStack\Exceptions\NotFoundException; -use BookStack\Exceptions\NotifyException; use Exception; -use Illuminate\Contracts\Container\BindingResolutionException; -use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Collection; class ChapterRepo @@ -19,7 +16,6 @@ class ChapterRepo /** * ChapterRepo constructor. - * @param $baseRepo */ public function __construct(BaseRepo $baseRepo) { @@ -77,7 +73,7 @@ class ChapterRepo public function destroy(Chapter $chapter) { $trashCan = new TrashCan(); - $trashCan->destroyChapter($chapter); + $trashCan->softDestroyChapter($chapter); } /** diff --git a/app/Entities/Repos/PageRepo.php b/app/Entities/Repos/PageRepo.php index e5f13463c..87839192b 100644 --- a/app/Entities/Repos/PageRepo.php +++ b/app/Entities/Repos/PageRepo.php @@ -12,6 +12,7 @@ use BookStack\Exceptions\MoveOperationException; use BookStack\Exceptions\NotFoundException; use BookStack\Exceptions\NotifyException; use BookStack\Exceptions\PermissionsException; +use Exception; use Illuminate\Database\Eloquent\Builder; use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Support\Collection; @@ -259,12 +260,12 @@ class PageRepo /** * Destroy a page from the system. - * @throws NotifyException + * @throws Exception */ public function destroy(Page $page) { $trashCan = new TrashCan(); - $trashCan->destroyPage($page); + $trashCan->softDestroyPage($page); } /** diff --git a/app/Entities/SearchService.php b/app/Entities/SearchService.php index 11b731cd0..7da8192cc 100644 --- a/app/Entities/SearchService.php +++ b/app/Entities/SearchService.php @@ -287,9 +287,12 @@ class SearchService foreach ($this->entityProvider->all() as $entityModel) { $selectFields = ['id', 'name', $entityModel->textField]; - $entityModel->newQuery()->select($selectFields)->chunk(1000, function ($entities) { - $this->indexEntities($entities); - }); + $entityModel->newQuery() + ->withTrashed() + ->select($selectFields) + ->chunk(1000, function ($entities) { + $this->indexEntities($entities); + }); } } diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php index 60d2664d0..3d68b8bcd 100644 --- a/app/Http/Controllers/HomeController.php +++ b/app/Http/Controllers/HomeController.php @@ -29,7 +29,7 @@ class HomeController extends Controller $recentFactor = count($draftPages) > 0 ? 0.5 : 1; $recents = $this->isSignedIn() ? - Views::getUserRecentlyViewed(12*$recentFactor, 0) + Views::getUserRecentlyViewed(12*$recentFactor, 1) : Book::visible()->orderBy('created_at', 'desc')->take(12 * $recentFactor)->get(); $recentlyUpdatedPages = Page::visible()->where('draft', false) ->orderBy('updated_at', 'desc')->take(12)->get(); diff --git a/database/migrations/2020_09_27_210059_add_entity_soft_deletes.php b/database/migrations/2020_09_27_210059_add_entity_soft_deletes.php new file mode 100644 index 000000000..d2b63e8d0 --- /dev/null +++ b/database/migrations/2020_09_27_210059_add_entity_soft_deletes.php @@ -0,0 +1,50 @@ +softDeletes(); + }); + Schema::table('books', function(Blueprint $table) { + $table->softDeletes(); + }); + Schema::table('chapters', function(Blueprint $table) { + $table->softDeletes(); + }); + Schema::table('pages', function(Blueprint $table) { + $table->softDeletes(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('bookshelves', function(Blueprint $table) { + $table->dropSoftDeletes(); + }); + Schema::table('books', function(Blueprint $table) { + $table->dropSoftDeletes(); + }); + Schema::table('chapters', function(Blueprint $table) { + $table->dropSoftDeletes(); + }); + Schema::table('pages', function(Blueprint $table) { + $table->dropSoftDeletes(); + }); + } +} diff --git a/database/migrations/2020_09_27_210528_create_delete_records_table.php b/database/migrations/2020_09_27_210528_create_delete_records_table.php new file mode 100644 index 000000000..cdb18ced6 --- /dev/null +++ b/database/migrations/2020_09_27_210528_create_delete_records_table.php @@ -0,0 +1,38 @@ +increments('id'); + $table->integer('deleted_by'); + $table->string('deletable_type', 100); + $table->integer('deletable_id'); + $table->timestamps(); + + $table->index('deleted_by'); + $table->index('deletable_type'); + $table->index('deletable_id'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('delete_records'); + } +}