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\EntityContainerData;
10 use BookStack\Entities\Models\HasCoverInterface;
11 use BookStack\Entities\Models\Deletion;
12 use BookStack\Entities\Models\Entity;
13 use BookStack\Entities\Models\Page;
14 use BookStack\Entities\Queries\EntityQueries;
15 use BookStack\Exceptions\NotifyException;
16 use BookStack\Facades\Activity;
17 use BookStack\Uploads\AttachmentService;
18 use BookStack\Uploads\ImageService;
19 use BookStack\Util\DatabaseTransaction;
21 use Illuminate\Database\Eloquent\Builder;
22 use Illuminate\Support\Carbon;
26 public function __construct(
27 protected EntityQueries $queries,
32 * Send a shelf to the recycle bin.
34 * @throws NotifyException
36 public function softDestroyShelf(Bookshelf $shelf)
38 $this->ensureDeletable($shelf);
39 Deletion::createForEntity($shelf);
44 * Send a book to the recycle bin.
48 public function softDestroyBook(Book $book)
50 $this->ensureDeletable($book);
51 Deletion::createForEntity($book);
53 foreach ($book->pages as $page) {
54 $this->softDestroyPage($page, false);
57 foreach ($book->chapters as $chapter) {
58 $this->softDestroyChapter($chapter, false);
65 * Send a chapter to the recycle bin.
69 public function softDestroyChapter(Chapter $chapter, bool $recordDelete = true)
72 $this->ensureDeletable($chapter);
73 Deletion::createForEntity($chapter);
76 if (count($chapter->pages) > 0) {
77 foreach ($chapter->pages as $page) {
78 $this->softDestroyPage($page, false);
86 * Send a page to the recycle bin.
90 public function softDestroyPage(Page $page, bool $recordDelete = true)
93 $this->ensureDeletable($page);
94 Deletion::createForEntity($page);
101 * Ensure the given entity is deletable.
102 * Is not for permissions, but logical conditions within the application.
103 * Will throw if not deletable.
105 * @throws NotifyException
107 protected function ensureDeletable(Entity $entity): void
109 $customHomeId = intval(explode(':', setting('app-homepage', '0:'))[0]);
110 $customHomeActive = setting('app-homepage-type') === 'page';
111 $removeCustomHome = false;
113 // Check custom homepage usage for pages
114 if ($entity instanceof Page && $entity->id === $customHomeId) {
115 if ($customHomeActive) {
116 throw new NotifyException(trans('errors.page_custom_home_deletion'), $entity->getUrl());
118 $removeCustomHome = true;
121 // Check custom homepage usage within chapters or books
122 if ($entity instanceof Chapter || $entity instanceof Book) {
123 if ($entity->pages()->where('id', '=', $customHomeId)->exists()) {
124 if ($customHomeActive) {
125 throw new NotifyException(trans('errors.page_custom_home_deletion'), $entity->getUrl());
127 $removeCustomHome = true;
131 if ($removeCustomHome) {
132 setting()->remove('app-homepage');
137 * Remove a bookshelf from the system.
141 protected function destroyShelf(Bookshelf $shelf): int
143 $this->destroyCommonRelations($shelf);
144 $shelf->books()->detach();
145 $shelf->forceDelete();
151 * Remove a book from the system.
152 * Destroys any child chapters and pages.
156 protected function destroyBook(Book $book): int
159 $pages = $book->pages()->withTrashed()->get();
160 foreach ($pages as $page) {
161 $this->destroyPage($page);
165 $chapters = $book->chapters()->withTrashed()->get();
166 foreach ($chapters as $chapter) {
167 $this->destroyChapter($chapter);
171 $this->destroyCommonRelations($book);
172 $book->shelves()->detach();
173 $book->forceDelete();
179 * Remove a chapter from the system.
180 * Destroys all pages within.
184 protected function destroyChapter(Chapter $chapter): int
187 $pages = $chapter->pages()->withTrashed()->get();
188 foreach ($pages as $page) {
189 $this->destroyPage($page);
193 $this->destroyCommonRelations($chapter);
194 $chapter->forceDelete();
200 * Remove a page from the system.
204 protected function destroyPage(Page $page): int
206 $this->destroyCommonRelations($page);
207 $page->allRevisions()->delete();
209 // Delete Attached Files
210 $attachmentService = app()->make(AttachmentService::class);
211 foreach ($page->attachments as $attachment) {
212 $attachmentService->deleteFile($attachment);
215 // Remove use as a template
216 EntityContainerData::query()
217 ->where('default_template_id', '=', $page->id)
218 ->update(['default_template_id' => null]);
220 // TODO - Handle related images (uploaded_to for gallery/drawings).
221 // Should maybe reset to null
222 // But does that present visibility/permission issues if they used to retain their old
224 // If so, might be better to leave them as-is like before, but ensure the maintenance
225 // cleanup command/action can find these "orphaned" images and delete them.
226 // But that would leave potential attachment to new pages on increment reset scenarios.
227 // Need to review permission scenarios for null field values relative to storage options.
229 $page->forceDelete();
235 * Get the total counts of those that have been trashed
236 * but not yet fully deleted (In recycle bin).
238 public function getTrashedCounts(): array
242 foreach ((new EntityProvider())->all() as $key => $instance) {
243 /** @var Builder<Entity> $query */
244 $query = $instance->newQuery();
245 $counts[$key] = $query->onlyTrashed()->count();
252 * Destroy all items that have pending deletions.
256 public function empty(): int
258 $deletions = Deletion::all();
260 foreach ($deletions as $deletion) {
261 $deleteCount += $this->destroyFromDeletion($deletion);
268 * Destroy an element from the given deletion model.
272 public function destroyFromDeletion(Deletion $deletion): int
274 // We directly load the deletable element here just to ensure it still
275 // exists in the event it has already been destroyed during this request.
276 $entity = $deletion->deletable()->first();
279 $count = $this->destroyEntity($deletion->deletable);
287 * Restore the content within the given deletion.
291 public function restoreFromDeletion(Deletion $deletion): int
293 $shouldRestore = true;
296 if ($deletion->deletable instanceof Entity) {
297 $parent = $deletion->deletable->getParent();
298 if ($parent && $parent->trashed()) {
299 $shouldRestore = false;
303 if ($deletion->deletable instanceof Entity && $shouldRestore) {
304 $restoreCount = $this->restoreEntity($deletion->deletable);
309 return $restoreCount;
313 * Automatically clear old content from the recycle bin
314 * depending on the configured lifetime.
315 * Returns the total number of deleted elements.
319 public function autoClearOld(): int
321 $lifetime = intval(config('app.recycle_bin_lifetime'));
326 $clearBeforeDate = Carbon::now()->addSeconds(10)->subDays($lifetime);
329 $deletionsToRemove = Deletion::query()->where('created_at', '<', $clearBeforeDate)->get();
330 foreach ($deletionsToRemove as $deletion) {
331 $deleteCount += $this->destroyFromDeletion($deletion);
338 * Restore an entity so it is essentially un-deleted.
339 * Deletions on restored child elements will be removed during this restoration.
341 protected function restoreEntity(Entity $entity): int
346 $restoreAction = function ($entity) use (&$count) {
347 if ($entity->deletions_count > 0) {
348 $entity->deletions()->delete();
355 if ($entity instanceof Chapter || $entity instanceof Book) {
356 $entity->pages()->withTrashed()->withCount('deletions')->get()->each($restoreAction);
359 if ($entity instanceof Book) {
360 $entity->chapters()->withTrashed()->withCount('deletions')->get()->each($restoreAction);
367 * Destroy the given entity.
368 * Returns the number of total entities destroyed in the operation.
372 public function destroyEntity(Entity $entity): int
374 $result = (new DatabaseTransaction(function () use ($entity) {
375 if ($entity instanceof Page) {
376 return $this->destroyPage($entity);
377 } else if ($entity instanceof Chapter) {
378 return $this->destroyChapter($entity);
379 } else if ($entity instanceof Book) {
380 return $this->destroyBook($entity);
381 } else if ($entity instanceof Bookshelf) {
382 return $this->destroyShelf($entity);
391 * Update entity relations to remove or update outstanding connections.
393 protected function destroyCommonRelations(Entity $entity)
395 Activity::removeEntity($entity);
396 $entity->views()->delete();
397 $entity->permissions()->delete();
398 $entity->tags()->delete();
399 $entity->comments()->delete();
400 $entity->jointPermissions()->delete();
401 $entity->searchTerms()->delete();
402 $entity->deletions()->delete();
403 $entity->favourites()->delete();
404 $entity->watches()->delete();
405 $entity->referencesTo()->delete();
406 $entity->referencesFrom()->delete();
408 if ($entity instanceof HasCoverInterface && $entity->coverInfo()->exists()) {
409 $imageService = app()->make(ImageService::class);
410 $imageService->destroy($entity->coverInfo()->getImage());
413 $entity->relatedData()->delete();