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;
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])) {
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
$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);
+ }
}
/**
$entity->comments()->delete();
$entity->jointPermissions()->delete();
$entity->searchTerms()->delete();
+ $entity->deletions()->delete();
if ($entity instanceof HasCoverImage && $entity->cover) {
$imageService = app()->make(ImageService::class);