]> BookStack Code Mirror - bookstack/blobdiff - app/Entities/Tools/TrashCan.php
Perms: Fixed some issues made when adding transactions
[bookstack] / app / Entities / Tools / TrashCan.php
index 62d373e97eba0861e45c3beb68f483e13561d3f5..5e8a9371942ea246098bf066851956401e942c1c 100644 (file)
@@ -1,38 +1,52 @@
-<?php namespace BookStack\Entities\Tools;
+<?php
+
+namespace BookStack\Entities\Tools;
 
-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\Entities\Models\Book;
+use BookStack\Entities\Models\Bookshelf;
+use BookStack\Entities\Models\Chapter;
+use BookStack\Entities\Models\Deletion;
+use BookStack\Entities\Models\Entity;
+use BookStack\Entities\Models\HasCoverImage;
+use BookStack\Entities\Models\Page;
+use BookStack\Entities\Queries\EntityQueries;
 use BookStack\Exceptions\NotifyException;
 use BookStack\Facades\Activity;
 use BookStack\Uploads\AttachmentService;
 use BookStack\Uploads\ImageService;
+use BookStack\Util\DatabaseTransaction;
 use Exception;
+use Illuminate\Database\Eloquent\Builder;
 use Illuminate\Support\Carbon;
 
 class TrashCan
 {
+    public function __construct(
+        protected EntityQueries $queries,
+    ) {
+    }
 
     /**
      * Send a shelf to the recycle bin.
+     *
+     * @throws NotifyException
      */
     public function softDestroyShelf(Bookshelf $shelf)
     {
+        $this->ensureDeletable($shelf);
         Deletion::createForEntity($shelf);
         $shelf->delete();
     }
 
     /**
      * Send a book to the recycle bin.
+     *
      * @throws Exception
      */
     public function softDestroyBook(Book $book)
     {
+        $this->ensureDeletable($book);
         Deletion::createForEntity($book);
 
         foreach ($book->pages as $page) {
@@ -48,11 +62,13 @@ class TrashCan
 
     /**
      * Send a chapter to the recycle bin.
+     *
      * @throws Exception
      */
     public function softDestroyChapter(Chapter $chapter, bool $recordDelete = true)
     {
         if ($recordDelete) {
+            $this->ensureDeletable($chapter);
             Deletion::createForEntity($chapter);
         }
 
@@ -67,40 +83,72 @@ class TrashCan
 
     /**
      * Send a page to the recycle bin.
+     *
      * @throws Exception
      */
     public function softDestroyPage(Page $page, bool $recordDelete = true)
     {
         if ($recordDelete) {
+            $this->ensureDeletable($page);
             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])) {
-            if (setting('app-homepage-type') === 'page') {
-                throw new NotifyException(trans('errors.page_custom_home_deletion'), $page->getUrl());
+        $page->delete();
+    }
+
+    /**
+     * Ensure the given entity is deletable.
+     * Is not for permissions, but logical conditions within the application.
+     * Will throw if not deletable.
+     *
+     * @throws NotifyException
+     */
+    protected function ensureDeletable(Entity $entity): void
+    {
+        $customHomeId = intval(explode(':', setting('app-homepage', '0:'))[0]);
+        $customHomeActive = setting('app-homepage-type') === 'page';
+        $removeCustomHome = false;
+
+        // Check custom homepage usage for pages
+        if ($entity instanceof Page && $entity->id === $customHomeId) {
+            if ($customHomeActive) {
+                throw new NotifyException(trans('errors.page_custom_home_deletion'), $entity->getUrl());
             }
-            setting()->remove('app-homepage');
+            $removeCustomHome = true;
         }
 
-        $page->delete();
+        // Check custom homepage usage within chapters or books
+        if ($entity instanceof Chapter || $entity instanceof Book) {
+            if ($entity->pages()->where('id', '=', $customHomeId)->exists()) {
+                if ($customHomeActive) {
+                    throw new NotifyException(trans('errors.page_custom_home_deletion'), $entity->getUrl());
+                }
+                $removeCustomHome = true;
+            }
+        }
+
+        if ($removeCustomHome) {
+            setting()->remove('app-homepage');
+        }
     }
 
     /**
      * 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
@@ -120,45 +168,59 @@ class TrashCan
 
         $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++;
-            }
+        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);
+        $page->allRevisions()->delete();
 
         // Delete Attached Files
-        $attachmentService = app(AttachmentService::class);
+        $attachmentService = app()->make(AttachmentService::class);
         foreach ($page->attachments as $attachment) {
             $attachmentService->deleteFile($attachment);
         }
 
+        // Remove book template usages
+        $this->queries->books->start()
+            ->where('default_template_id', '=', $page->id)
+            ->update(['default_template_id' => null]);
+
+        // Remove chapter template usages
+        $this->queries->chapters->start()
+            ->where('default_template_id', '=', $page->id)
+            ->update(['default_template_id' => null]);
+
         $page->forceDelete();
+
         return 1;
     }
 
@@ -168,12 +230,12 @@ class TrashCan
      */
     public function getTrashedCounts(): array
     {
-        $provider = app(EntityProvider::class);
         $counts = [];
 
-        /** @var Entity $instance */
-        foreach ($provider->all() as $key => $instance) {
-            $counts[$key] = $instance->newQuery()->onlyTrashed()->count();
+        foreach ((new EntityProvider())->all() as $key => $instance) {
+            /** @var Builder<Entity> $query */
+            $query = $instance->newQuery();
+            $counts[$key] = $query->onlyTrashed()->count();
         }
 
         return $counts;
@@ -181,6 +243,7 @@ class TrashCan
 
     /**
      * Destroy all items that have pending deletions.
+     *
      * @throws Exception
      */
     public function empty(): int
@@ -190,11 +253,13 @@ class TrashCan
         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
@@ -207,28 +272,33 @@ class TrashCan
             $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 ($deletion->deletable instanceof Entity) {
+            $parent = $deletion->deletable->getParent();
+            if ($parent && $parent->trashed()) {
+                $shouldRestore = false;
+            }
         }
 
-        if ($shouldRestore) {
+        if ($deletion->deletable instanceof Entity && $shouldRestore) {
             $restoreCount = $this->restoreEntity($deletion->deletable);
         }
 
         $deletion->delete();
+
         return $restoreCount;
     }
 
@@ -236,6 +306,7 @@ class TrashCan
      * 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
@@ -274,11 +345,11 @@ class TrashCan
             $count++;
         };
 
-        if ($entity->isA('chapter') || $entity->isA('book')) {
+        if ($entity instanceof Chapter || $entity instanceof Book) {
             $entity->pages()->withTrashed()->withCount('deletions')->get()->each($restoreAction);
         }
 
-        if ($entity->isA('book')) {
+        if ($entity instanceof Book) {
             $entity->chapters()->withTrashed()->withCount('deletions')->get()->each($restoreAction);
         }
 
@@ -287,21 +358,26 @@ class TrashCan
 
     /**
      * Destroy the given entity.
+     * Returns the number of total entities destroyed in the operation.
+     *
+     * @throws Exception
      */
-    protected function destroyEntity(Entity $entity): int
+    public 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);
-        }
+        $result = (new DatabaseTransaction(function () use ($entity) {
+            if ($entity instanceof Page) {
+                return $this->destroyPage($entity);
+            } else if ($entity instanceof Chapter) {
+                return $this->destroyChapter($entity);
+            } else if ($entity instanceof Book) {
+                return $this->destroyBook($entity);
+            } else if ($entity instanceof Bookshelf) {
+                return $this->destroyShelf($entity);
+            }
+            return null;
+        }))->run();
+
+        return $result ?? 0;
     }
 
     /**
@@ -317,10 +393,14 @@ class TrashCan
         $entity->jointPermissions()->delete();
         $entity->searchTerms()->delete();
         $entity->deletions()->delete();
+        $entity->favourites()->delete();
+        $entity->watches()->delete();
+        $entity->referencesTo()->delete();
+        $entity->referencesFrom()->delete();
 
-        if ($entity instanceof HasCoverImage && $entity->cover) {
+        if ($entity instanceof HasCoverImage && $entity->cover()->exists()) {
             $imageService = app()->make(ImageService::class);
-            $imageService->destroy($entity->cover);
+            $imageService->destroy($entity->cover()->first());
         }
     }
 }