]> BookStack Code Mirror - bookstack/blobdiff - app/Entities/Managers/TrashCan.php
LDAP: Added TLS support
[bookstack] / app / Entities / Managers / TrashCan.php
index 1a32294fc7500dec7102cd08d5063e706df65932..48768ab9375ee81acb1a99e6ce19a60e3de112d2 100644 (file)
@@ -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);