/**
* 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);
}
/**
/**
* PermissionService constructor.
- * @param JointPermission $jointPermission
- * @param EntityPermission $entityPermission
- * @param Role $role
- * @param Connection $db
- * @param EntityProvider $entityProvider
*/
public function __construct(
JointPermission $jointPermission,
});
// 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);
});
*/
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']);
}]);
}
--- /dev/null
+<?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;
+ }
+
+}
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Relations\MorphMany;
+use Illuminate\Database\Eloquent\SoftDeletes;
/**
* Class Entity
*/
class Entity extends Ownable
{
+ use SoftDeletes;
/**
* @var string - Name of property where the main text content is found
/**
* 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'
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;
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])) {
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();
}
/**
$entity->comments()->delete();
$entity->jointPermissions()->delete();
$entity->searchTerms()->delete();
+ $entity->deleteRecords()->delete();
if ($entity instanceof HasCoverImage && $entity->cover) {
$imageService = app()->make(ImageService::class);
/**
* 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);
}
}
public function destroy(Bookshelf $shelf)
{
$trashCan = new TrashCan();
- $trashCan->destroyShelf($shelf);
+ $trashCan->softDestroyShelf($shelf);
}
}
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
/**
* ChapterRepo constructor.
- * @param $baseRepo
*/
public function __construct(BaseRepo $baseRepo)
{
public function destroy(Chapter $chapter)
{
$trashCan = new TrashCan();
- $trashCan->destroyChapter($chapter);
+ $trashCan->softDestroyChapter($chapter);
}
/**
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;
/**
* Destroy a page from the system.
- * @throws NotifyException
+ * @throws Exception
*/
public function destroy(Page $page)
{
$trashCan = new TrashCan();
- $trashCan->destroyPage($page);
+ $trashCan->softDestroyPage($page);
}
/**
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);
+ });
}
}
$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();
--- /dev/null
+<?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();
+ });
+ }
+}
--- /dev/null
+<?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');
+ }
+}