]> BookStack Code Mirror - bookstack/commitdiff
Started implementation of recycle bin functionality
authorDan Brown <redacted>
Sun, 27 Sep 2020 22:24:33 +0000 (23:24 +0100)
committerDan Brown <redacted>
Sun, 27 Sep 2020 22:24:33 +0000 (23:24 +0100)
13 files changed:
app/Actions/ViewService.php
app/Auth/Permissions/PermissionService.php
app/Entities/DeleteRecord.php [new file with mode: 0644]
app/Entities/Entity.php
app/Entities/Managers/TrashCan.php
app/Entities/Repos/BookRepo.php
app/Entities/Repos/BookshelfRepo.php
app/Entities/Repos/ChapterRepo.php
app/Entities/Repos/PageRepo.php
app/Entities/SearchService.php
app/Http/Controllers/HomeController.php
database/migrations/2020_09_27_210059_add_entity_soft_deletes.php [new file with mode: 0644]
database/migrations/2020_09_27_210528_create_delete_records_table.php [new file with mode: 0644]

index 324bfaa4ef9fb14c722148529c4bb895692a12e8..aa75abb72d3e0b8fb006d8600654bd2029c7d04f 100644 (file)
@@ -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);
     }
 
     /**
index 97cc1ca241e84209f235136550378f3cb8f43f81..2609779bfa3e79805bd1c2839c3020add1caad29 100644 (file)
@@ -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 (file)
index 0000000..84b37f5
--- /dev/null
@@ -0,0 +1,41 @@
+<?php namespace BookStack\Entities;
+
+use BookStack\Auth\User;
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
+use Illuminate\Database\Eloquent\Relations\MorphTo;
+
+class DeleteRecord extends Model
+{
+
+    /**
+     * Get the related deletable record.
+     */
+    public function deletable(): MorphTo
+    {
+        return $this->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;
+    }
+
+}
index cc7df46d4f653e201f9ba17c72a09c0b45046577..d1a8664e472d1fc5e0fa1d86ef3990c8efd2a7ce 100644 (file)
@@ -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'
index 1a32294fc7500dec7102cd08d5063e706df65932..9a21f5e2c13d9abd15ab8b2d9fbc1c410010d1c1 100644 (file)
@@ -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,32 +82,73 @@ class TrashCan
             setting()->remove('app-homepage');
         }
 
-        $this->destroyCommonRelations($page);
+        $page->delete();
+    }
 
-        // Delete Attached Files
-        $attachmentService = app(AttachmentService::class);
-        foreach ($page->attachments as $attachment) {
-            $attachmentService->deleteFile($attachment);
+    /**
+     * 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);
         }
 
-        $page->delete();
+        $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)
     {
-        if (count($chapter->pages) > 0) {
-            foreach ($chapter->pages as $page) {
-                $page->chapter_id = 0;
-                $page->save();
+        $pages = $chapter->pages()->withTrashed()->get();
+        if (count($pages)) {
+            foreach ($pages as $page) {
+                $this->destroyPage($page);
             }
         }
 
         $this->destroyCommonRelations($chapter);
-        $chapter->delete();
+        $chapter->forceDelete();
+    }
+
+    /**
+     * Remove a page from the system.
+     * @throws Exception
+     */
+    public function destroyPage(Page $page)
+    {
+        $this->destroyCommonRelations($page);
+
+        // Delete Attached Files
+        $attachmentService = app(AttachmentService::class);
+        foreach ($page->attachments as $attachment) {
+            $attachmentService->deleteFile($attachment);
+        }
+
+        $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);
index 70db0fa65750bde4266c97040939d7a0b55c098a..bb1895b36a3b62a0948322efc8803ef0efb3cff2 100644 (file)
@@ -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);
     }
 }
index ba687c6f6e754f3a49959ad932294620cb3e74c8..eb1536da75afa8fa5fbec5121ad31b1a5e33bcad 100644 (file)
@@ -174,6 +174,6 @@ class BookshelfRepo
     public function destroy(Bookshelf $shelf)
     {
         $trashCan = new TrashCan();
-        $trashCan->destroyShelf($shelf);
+        $trashCan->softDestroyShelf($shelf);
     }
 }
index c6f3a2d2f0fc093c37b6e541081c20c977468c75..c1573f5db85ff48c3775783b7d27ed74da73620b 100644 (file)
@@ -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);
     }
 
     /**
index e5f13463c388f781dd215338afb4af37cefaa7c5..87839192b0623183a8109e03c9d775b46560ac2b 100644 (file)
@@ -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);
     }
 
     /**
index 11b731cd0153591e2cfd7b6b71f88504f088cd92..7da8192cc783665b024bdc9517c60ab28599df2b 100644 (file)
@@ -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);
+                });
         }
     }
 
index 60d2664d03a81107b9427f1258a8a82664551c90..3d68b8bcdb35410e96309305460a6d0f67149149 100644 (file)
@@ -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 (file)
index 0000000..d2b63e8
--- /dev/null
@@ -0,0 +1,50 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+class AddEntitySoftDeletes extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::table('bookshelves', function(Blueprint  $table) {
+            $table->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 (file)
index 0000000..cdb18ce
--- /dev/null
@@ -0,0 +1,38 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+class CreateDeleteRecordsTable extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::create('delete_records', function (Blueprint $table) {
+            $table->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');
+    }
+}