3 namespace BookStack\Entities\Tools;
5 use BookStack\Entities\EntityProvider;
6 use BookStack\Entities\Models\Book;
7 use BookStack\Entities\Models\Bookshelf;
8 use BookStack\Entities\Models\Chapter;
9 use BookStack\Entities\Models\Deletion;
10 use BookStack\Entities\Models\Entity;
11 use BookStack\Entities\Models\HasCoverImage;
12 use BookStack\Entities\Models\Page;
13 use BookStack\Exceptions\NotifyException;
14 use BookStack\Facades\Activity;
15 use BookStack\Uploads\AttachmentService;
16 use BookStack\Uploads\ImageService;
18 use Illuminate\Database\Eloquent\Builder;
19 use Illuminate\Support\Carbon;
24 * Send a shelf to the recycle bin.
25 * @throws NotifyException
27 public function softDestroyShelf(Bookshelf $shelf)
29 $this->ensureDeletable($shelf);
30 Deletion::createForEntity($shelf);
35 * Send a book to the recycle bin.
39 public function softDestroyBook(Book $book)
41 $this->ensureDeletable($book);
42 Deletion::createForEntity($book);
44 foreach ($book->pages as $page) {
45 $this->softDestroyPage($page, false);
48 foreach ($book->chapters as $chapter) {
49 $this->softDestroyChapter($chapter, false);
56 * Send a chapter to the recycle bin.
60 public function softDestroyChapter(Chapter $chapter, bool $recordDelete = true)
63 $this->ensureDeletable($chapter);
64 Deletion::createForEntity($chapter);
67 if (count($chapter->pages) > 0) {
68 foreach ($chapter->pages as $page) {
69 $this->softDestroyPage($page, false);
77 * Send a page to the recycle bin.
81 public function softDestroyPage(Page $page, bool $recordDelete = true)
84 $this->ensureDeletable($page);
85 Deletion::createForEntity($page);
92 * Ensure the given entity is deletable.
93 * Is not for permissions, but logical conditions within the application.
94 * Will throw if not deletable.
96 * @throws NotifyException
98 protected function ensureDeletable(Entity $entity): void
100 $customHomeId = intval(explode(':', setting('app-homepage', '0:'))[0]);
101 $customHomeActive = setting('app-homepage-type') === 'page';
102 $removeCustomHome = false;
104 // Check custom homepage usage for pages
105 if ($entity instanceof Page && $entity->id === $customHomeId) {
106 if ($customHomeActive) {
107 throw new NotifyException(trans('errors.page_custom_home_deletion'), $entity->getUrl());
109 $removeCustomHome = true;
112 // Check custom homepage usage within chapters or books
113 if ($entity instanceof Chapter || $entity instanceof Book) {
114 if ($entity->pages()->where('id', '=', $customHomeId)->exists()) {
115 if ($customHomeActive) {
116 throw new NotifyException(trans('errors.page_custom_home_deletion'), $entity->getUrl());
118 $removeCustomHome = true;
122 if ($removeCustomHome) {
123 setting()->remove('app-homepage');
128 * Remove a bookshelf from the system.
132 protected function destroyShelf(Bookshelf $shelf): int
134 $this->destroyCommonRelations($shelf);
135 $shelf->forceDelete();
141 * Remove a book from the system.
142 * Destroys any child chapters and pages.
146 protected function destroyBook(Book $book): int
149 $pages = $book->pages()->withTrashed()->get();
150 foreach ($pages as $page) {
151 $this->destroyPage($page);
155 $chapters = $book->chapters()->withTrashed()->get();
156 foreach ($chapters as $chapter) {
157 $this->destroyChapter($chapter);
161 $this->destroyCommonRelations($book);
162 $book->forceDelete();
168 * Remove a chapter from the system.
169 * Destroys all pages within.
173 protected function destroyChapter(Chapter $chapter): int
176 $pages = $chapter->pages()->withTrashed()->get();
177 foreach ($pages as $page) {
178 $this->destroyPage($page);
182 $this->destroyCommonRelations($chapter);
183 $chapter->forceDelete();
189 * Remove a page from the system.
193 protected function destroyPage(Page $page): int
195 $this->destroyCommonRelations($page);
196 $page->allRevisions()->delete();
198 // Delete Attached Files
199 $attachmentService = app(AttachmentService::class);
200 foreach ($page->attachments as $attachment) {
201 $attachmentService->deleteFile($attachment);
204 $page->forceDelete();
210 * Get the total counts of those that have been trashed
211 * but not yet fully deleted (In recycle bin).
213 public function getTrashedCounts(): array
217 foreach ((new EntityProvider())->all() as $key => $instance) {
218 /** @var Builder<Entity> $query */
219 $query = $instance->newQuery();
220 $counts[$key] = $query->onlyTrashed()->count();
227 * Destroy all items that have pending deletions.
231 public function empty(): int
233 $deletions = Deletion::all();
235 foreach ($deletions as $deletion) {
236 $deleteCount += $this->destroyFromDeletion($deletion);
243 * Destroy an element from the given deletion model.
247 public function destroyFromDeletion(Deletion $deletion): int
249 // We directly load the deletable element here just to ensure it still
250 // exists in the event it has already been destroyed during this request.
251 $entity = $deletion->deletable()->first();
254 $count = $this->destroyEntity($deletion->deletable);
262 * Restore the content within the given deletion.
266 public function restoreFromDeletion(Deletion $deletion): int
268 $shouldRestore = true;
271 if ($deletion->deletable instanceof Entity) {
272 $parent = $deletion->deletable->getParent();
273 if ($parent && $parent->trashed()) {
274 $shouldRestore = false;
278 if ($deletion->deletable instanceof Entity && $shouldRestore) {
279 $restoreCount = $this->restoreEntity($deletion->deletable);
284 return $restoreCount;
288 * Automatically clear old content from the recycle bin
289 * depending on the configured lifetime.
290 * Returns the total number of deleted elements.
294 public function autoClearOld(): int
296 $lifetime = intval(config('app.recycle_bin_lifetime'));
301 $clearBeforeDate = Carbon::now()->addSeconds(10)->subDays($lifetime);
304 $deletionsToRemove = Deletion::query()->where('created_at', '<', $clearBeforeDate)->get();
305 foreach ($deletionsToRemove as $deletion) {
306 $deleteCount += $this->destroyFromDeletion($deletion);
313 * Restore an entity so it is essentially un-deleted.
314 * Deletions on restored child elements will be removed during this restoration.
316 protected function restoreEntity(Entity $entity): int
321 $restoreAction = function ($entity) use (&$count) {
322 if ($entity->deletions_count > 0) {
323 $entity->deletions()->delete();
330 if ($entity instanceof Chapter || $entity instanceof Book) {
331 $entity->pages()->withTrashed()->withCount('deletions')->get()->each($restoreAction);
334 if ($entity instanceof Book) {
335 $entity->chapters()->withTrashed()->withCount('deletions')->get()->each($restoreAction);
342 * Destroy the given entity.
346 protected function destroyEntity(Entity $entity): int
348 if ($entity instanceof Page) {
349 return $this->destroyPage($entity);
351 if ($entity instanceof Chapter) {
352 return $this->destroyChapter($entity);
354 if ($entity instanceof Book) {
355 return $this->destroyBook($entity);
357 if ($entity instanceof Bookshelf) {
358 return $this->destroyShelf($entity);
365 * Update entity relations to remove or update outstanding connections.
367 protected function destroyCommonRelations(Entity $entity)
369 Activity::removeEntity($entity);
370 $entity->views()->delete();
371 $entity->permissions()->delete();
372 $entity->tags()->delete();
373 $entity->comments()->delete();
374 $entity->jointPermissions()->delete();
375 $entity->searchTerms()->delete();
376 $entity->deletions()->delete();
377 $entity->favourites()->delete();
379 if ($entity instanceof HasCoverImage && $entity->cover()->exists()) {
380 $imageService = app()->make(ImageService::class);
381 $imageService->destroy($entity->cover()->first());