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.
26 * @throws NotifyException
28 public function softDestroyShelf(Bookshelf $shelf)
30 $this->ensureDeletable($shelf);
31 Deletion::createForEntity($shelf);
36 * Send a book to the recycle bin.
40 public function softDestroyBook(Book $book)
42 $this->ensureDeletable($book);
43 Deletion::createForEntity($book);
45 foreach ($book->pages as $page) {
46 $this->softDestroyPage($page, false);
49 foreach ($book->chapters as $chapter) {
50 $this->softDestroyChapter($chapter, false);
57 * Send a chapter to the recycle bin.
61 public function softDestroyChapter(Chapter $chapter, bool $recordDelete = true)
64 $this->ensureDeletable($chapter);
65 Deletion::createForEntity($chapter);
68 if (count($chapter->pages) > 0) {
69 foreach ($chapter->pages as $page) {
70 $this->softDestroyPage($page, false);
78 * Send a page to the recycle bin.
82 public function softDestroyPage(Page $page, bool $recordDelete = true)
85 $this->ensureDeletable($page);
86 Deletion::createForEntity($page);
93 * Ensure the given entity is deletable.
94 * Is not for permissions, but logical conditions within the application.
95 * Will throw if not deletable.
97 * @throws NotifyException
99 protected function ensureDeletable(Entity $entity): void
101 $customHomeId = intval(explode(':', setting('app-homepage', '0:'))[0]);
102 $customHomeActive = setting('app-homepage-type') === 'page';
103 $removeCustomHome = false;
105 // Check custom homepage usage for pages
106 if ($entity instanceof Page && $entity->id === $customHomeId) {
107 if ($customHomeActive) {
108 throw new NotifyException(trans('errors.page_custom_home_deletion'), $entity->getUrl());
110 $removeCustomHome = true;
113 // Check custom homepage usage within chapters or books
114 if ($entity instanceof Chapter || $entity instanceof Book) {
115 if ($entity->pages()->where('id', '=', $customHomeId)->exists()) {
116 if ($customHomeActive) {
117 throw new NotifyException(trans('errors.page_custom_home_deletion'), $entity->getUrl());
119 $removeCustomHome = true;
123 if ($removeCustomHome) {
124 setting()->remove('app-homepage');
129 * Remove a bookshelf from the system.
133 protected function destroyShelf(Bookshelf $shelf): int
135 $this->destroyCommonRelations($shelf);
136 $shelf->forceDelete();
142 * Remove a book from the system.
143 * Destroys any child chapters and pages.
147 protected function destroyBook(Book $book): int
150 $pages = $book->pages()->withTrashed()->get();
151 foreach ($pages as $page) {
152 $this->destroyPage($page);
156 $chapters = $book->chapters()->withTrashed()->get();
157 foreach ($chapters as $chapter) {
158 $this->destroyChapter($chapter);
162 $this->destroyCommonRelations($book);
163 $book->forceDelete();
169 * Remove a chapter from the system.
170 * Destroys all pages within.
174 protected function destroyChapter(Chapter $chapter): int
177 $pages = $chapter->pages()->withTrashed()->get();
178 foreach ($pages as $page) {
179 $this->destroyPage($page);
183 $this->destroyCommonRelations($chapter);
184 $chapter->forceDelete();
190 * Remove a page from the system.
194 protected function destroyPage(Page $page): int
196 $this->destroyCommonRelations($page);
197 $page->allRevisions()->delete();
199 // Delete Attached Files
200 $attachmentService = app()->make(AttachmentService::class);
201 foreach ($page->attachments as $attachment) {
202 $attachmentService->deleteFile($attachment);
205 // Remove book template usages
206 Book::query()->where('default_template_id', '=', $page->id)
207 ->update(['default_template_id' => null]);
209 $page->forceDelete();
211 // Remove chapter template usages
212 Chapter::query()->where('default_template_id', '=', $page->id)
213 ->update(['default_template_id' => null]);
215 $page->forceDelete();
221 * Get the total counts of those that have been trashed
222 * but not yet fully deleted (In recycle bin).
224 public function getTrashedCounts(): array
228 foreach ((new EntityProvider())->all() as $key => $instance) {
229 /** @var Builder<Entity> $query */
230 $query = $instance->newQuery();
231 $counts[$key] = $query->onlyTrashed()->count();
238 * Destroy all items that have pending deletions.
242 public function empty(): int
244 $deletions = Deletion::all();
246 foreach ($deletions as $deletion) {
247 $deleteCount += $this->destroyFromDeletion($deletion);
254 * Destroy an element from the given deletion model.
258 public function destroyFromDeletion(Deletion $deletion): int
260 // We directly load the deletable element here just to ensure it still
261 // exists in the event it has already been destroyed during this request.
262 $entity = $deletion->deletable()->first();
265 $count = $this->destroyEntity($deletion->deletable);
273 * Restore the content within the given deletion.
277 public function restoreFromDeletion(Deletion $deletion): int
279 $shouldRestore = true;
282 if ($deletion->deletable instanceof Entity) {
283 $parent = $deletion->deletable->getParent();
284 if ($parent && $parent->trashed()) {
285 $shouldRestore = false;
289 if ($deletion->deletable instanceof Entity && $shouldRestore) {
290 $restoreCount = $this->restoreEntity($deletion->deletable);
295 return $restoreCount;
299 * Automatically clear old content from the recycle bin
300 * depending on the configured lifetime.
301 * Returns the total number of deleted elements.
305 public function autoClearOld(): int
307 $lifetime = intval(config('app.recycle_bin_lifetime'));
312 $clearBeforeDate = Carbon::now()->addSeconds(10)->subDays($lifetime);
315 $deletionsToRemove = Deletion::query()->where('created_at', '<', $clearBeforeDate)->get();
316 foreach ($deletionsToRemove as $deletion) {
317 $deleteCount += $this->destroyFromDeletion($deletion);
324 * Restore an entity so it is essentially un-deleted.
325 * Deletions on restored child elements will be removed during this restoration.
327 protected function restoreEntity(Entity $entity): int
332 $restoreAction = function ($entity) use (&$count) {
333 if ($entity->deletions_count > 0) {
334 $entity->deletions()->delete();
341 if ($entity instanceof Chapter || $entity instanceof Book) {
342 $entity->pages()->withTrashed()->withCount('deletions')->get()->each($restoreAction);
345 if ($entity instanceof Book) {
346 $entity->chapters()->withTrashed()->withCount('deletions')->get()->each($restoreAction);
353 * Destroy the given entity.
357 public function destroyEntity(Entity $entity): int
359 if ($entity instanceof Page) {
360 return $this->destroyPage($entity);
362 if ($entity instanceof Chapter) {
363 return $this->destroyChapter($entity);
365 if ($entity instanceof Book) {
366 return $this->destroyBook($entity);
368 if ($entity instanceof Bookshelf) {
369 return $this->destroyShelf($entity);
376 * Update entity relations to remove or update outstanding connections.
378 protected function destroyCommonRelations(Entity $entity)
380 Activity::removeEntity($entity);
381 $entity->views()->delete();
382 $entity->permissions()->delete();
383 $entity->tags()->delete();
384 $entity->comments()->delete();
385 $entity->jointPermissions()->delete();
386 $entity->searchTerms()->delete();
387 $entity->deletions()->delete();
388 $entity->favourites()->delete();
389 $entity->watches()->delete();
390 $entity->referencesTo()->delete();
391 $entity->referencesFrom()->delete();
393 if ($entity instanceof HasCoverImage && $entity->cover()->exists()) {
394 $imageService = app()->make(ImageService::class);
395 $imageService->destroy($entity->cover()->first());