X-Git-Url: http://source.bookstackapp.com/bookstack/blobdiff_plain/e91ef54cc9f8ce6b264bced8191275b6a33e594f..refs/pull/2376/head:/app/Entities/Managers/TrashCan.php diff --git a/app/Entities/Managers/TrashCan.php b/app/Entities/Managers/TrashCan.php index 1a32294fc..48768ab93 100644 --- a/app/Entities/Managers/TrashCan.php +++ b/app/Entities/Managers/TrashCan.php @@ -3,7 +3,9 @@ use BookStack\Entities\Book; use BookStack\Entities\Bookshelf; use BookStack\Entities\Chapter; +use BookStack\Entities\Deletion; use BookStack\Entities\Entity; +use BookStack\Entities\EntityProvider; use BookStack\Entities\HasCoverImage; use BookStack\Entities\Page; use BookStack\Exceptions\NotifyException; @@ -11,46 +13,68 @@ use BookStack\Facades\Activity; use BookStack\Uploads\AttachmentService; use BookStack\Uploads\ImageService; use Exception; -use Illuminate\Contracts\Container\BindingResolutionException; +use Illuminate\Support\Carbon; 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); + Deletion::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) { + Deletion::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) { + Deletion::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) { + Deletion::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 +84,72 @@ class TrashCan setting()->remove('app-homepage'); } + $page->delete(); + } + + /** + * Remove a bookshelf from the system. + * @throws Exception + */ + protected function destroyShelf(Bookshelf $shelf): int + { + $this->destroyCommonRelations($shelf); + $shelf->forceDelete(); + return 1; + } + + /** + * Remove a book from the system. + * Destroys any child chapters and pages. + * @throws Exception + */ + protected function destroyBook(Book $book): int + { + $count = 0; + $pages = $book->pages()->withTrashed()->get(); + foreach ($pages as $page) { + $this->destroyPage($page); + $count++; + } + + $chapters = $book->chapters()->withTrashed()->get(); + foreach ($chapters as $chapter) { + $this->destroyChapter($chapter); + $count++; + } + + $this->destroyCommonRelations($book); + $book->forceDelete(); + return $count + 1; + } + + /** + * Remove a chapter from the system. + * Destroys all pages within. + * @throws Exception + */ + protected function destroyChapter(Chapter $chapter): int + { + $count = 0; + $pages = $chapter->pages()->withTrashed()->get(); + if (count($pages)) { + foreach ($pages as $page) { + $this->destroyPage($page); + $count++; + } + } + + $this->destroyCommonRelations($chapter); + $chapter->forceDelete(); + return $count + 1; + } + + /** + * Remove a page from the system. + * @throws Exception + */ + protected function destroyPage(Page $page): int + { $this->destroyCommonRelations($page); // Delete Attached Files @@ -68,24 +158,150 @@ class TrashCan $attachmentService->deleteFile($attachment); } - $page->delete(); + $page->forceDelete(); + return 1; } /** - * Remove a chapter from the system. + * Get the total counts of those that have been trashed + * but not yet fully deleted (In recycle bin). + */ + public function getTrashedCounts(): array + { + $provider = app(EntityProvider::class); + $counts = []; + + /** @var Entity $instance */ + foreach ($provider->all() as $key => $instance) { + $counts[$key] = $instance->newQuery()->onlyTrashed()->count(); + } + + return $counts; + } + + /** + * Destroy all items that have pending deletions. * @throws Exception */ - public function destroyChapter(Chapter $chapter) + public function empty(): int { - if (count($chapter->pages) > 0) { - foreach ($chapter->pages as $page) { - $page->chapter_id = 0; - $page->save(); + $deletions = Deletion::all(); + $deleteCount = 0; + foreach ($deletions as $deletion) { + $deleteCount += $this->destroyFromDeletion($deletion); + } + return $deleteCount; + } + + /** + * Destroy an element from the given deletion model. + * @throws Exception + */ + public function destroyFromDeletion(Deletion $deletion): int + { + // We directly load the deletable element here just to ensure it still + // exists in the event it has already been destroyed during this request. + $entity = $deletion->deletable()->first(); + $count = 0; + if ($entity) { + $count = $this->destroyEntity($deletion->deletable); + } + $deletion->delete(); + return $count; + } + + /** + * Restore the content within the given deletion. + * @throws Exception + */ + public function restoreFromDeletion(Deletion $deletion): int + { + $shouldRestore = true; + $restoreCount = 0; + $parent = $deletion->deletable->getParent(); + + if ($parent && $parent->trashed()) { + $shouldRestore = false; + } + + if ($shouldRestore) { + $restoreCount = $this->restoreEntity($deletion->deletable); + } + + $deletion->delete(); + return $restoreCount; + } + + /** + * Automatically clear old content from the recycle bin + * depending on the configured lifetime. + * Returns the total number of deleted elements. + * @throws Exception + */ + public function autoClearOld(): int + { + $lifetime = intval(config('app.recycle_bin_lifetime')); + if ($lifetime < 0) { + return 0; + } + + $clearBeforeDate = Carbon::now()->addSeconds(10)->subDays($lifetime); + $deleteCount = 0; + + $deletionsToRemove = Deletion::query()->where('created_at', '<', $clearBeforeDate)->get(); + foreach ($deletionsToRemove as $deletion) { + $deleteCount += $this->destroyFromDeletion($deletion); + } + + return $deleteCount; + } + + /** + * Restore an entity so it is essentially un-deleted. + * Deletions on restored child elements will be removed during this restoration. + */ + protected function restoreEntity(Entity $entity): int + { + $count = 1; + $entity->restore(); + + $restoreAction = function ($entity) use (&$count) { + if ($entity->deletions_count > 0) { + $entity->deletions()->delete(); } + + $entity->restore(); + $count++; + }; + + if ($entity->isA('chapter') || $entity->isA('book')) { + $entity->pages()->withTrashed()->withCount('deletions')->get()->each($restoreAction); } - $this->destroyCommonRelations($chapter); - $chapter->delete(); + if ($entity->isA('book')) { + $entity->chapters()->withTrashed()->withCount('deletions')->get()->each($restoreAction); + } + + return $count; + } + + /** + * Destroy the given entity. + */ + protected function destroyEntity(Entity $entity): int + { + if ($entity->isA('page')) { + return $this->destroyPage($entity); + } + if ($entity->isA('chapter')) { + return $this->destroyChapter($entity); + } + if ($entity->isA('book')) { + return $this->destroyBook($entity); + } + if ($entity->isA('shelf')) { + return $this->destroyShelf($entity); + } } /** @@ -100,6 +316,7 @@ class TrashCan $entity->comments()->delete(); $entity->jointPermissions()->delete(); $entity->searchTerms()->delete(); + $entity->deletions()->delete(); if ($entity instanceof HasCoverImage && $entity->cover) { $imageService = app()->make(ImageService::class);