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\Entities\Queries\EntityQueries;
14 use BookStack\Exceptions\NotifyException;
15 use BookStack\Facades\Activity;
16 use BookStack\Uploads\AttachmentService;
17 use BookStack\Uploads\ImageService;
18 use BookStack\Util\DatabaseTransaction;
20 use Illuminate\Database\Eloquent\Builder;
21 use Illuminate\Support\Carbon;
25 public function __construct(
26 protected EntityQueries $queries,
31 * Send a shelf to the recycle bin.
33 * @throws NotifyException
35 public function softDestroyShelf(Bookshelf $shelf)
37 $this->ensureDeletable($shelf);
38 Deletion::createForEntity($shelf);
43 * Send a book to the recycle bin.
47 public function softDestroyBook(Book $book)
49 $this->ensureDeletable($book);
50 Deletion::createForEntity($book);
52 foreach ($book->pages as $page) {
53 $this->softDestroyPage($page, false);
56 foreach ($book->chapters as $chapter) {
57 $this->softDestroyChapter($chapter, false);
64 * Send a chapter to the recycle bin.
68 public function softDestroyChapter(Chapter $chapter, bool $recordDelete = true)
71 $this->ensureDeletable($chapter);
72 Deletion::createForEntity($chapter);
75 if (count($chapter->pages) > 0) {
76 foreach ($chapter->pages as $page) {
77 $this->softDestroyPage($page, false);
85 * Send a page to the recycle bin.
89 public function softDestroyPage(Page $page, bool $recordDelete = true)
92 $this->ensureDeletable($page);
93 Deletion::createForEntity($page);
100 * Ensure the given entity is deletable.
101 * Is not for permissions, but logical conditions within the application.
102 * Will throw if not deletable.
104 * @throws NotifyException
106 protected function ensureDeletable(Entity $entity): void
108 $customHomeId = intval(explode(':', setting('app-homepage', '0:'))[0]);
109 $customHomeActive = setting('app-homepage-type') === 'page';
110 $removeCustomHome = false;
112 // Check custom homepage usage for pages
113 if ($entity instanceof Page && $entity->id === $customHomeId) {
114 if ($customHomeActive) {
115 throw new NotifyException(trans('errors.page_custom_home_deletion'), $entity->getUrl());
117 $removeCustomHome = true;
120 // Check custom homepage usage within chapters or books
121 if ($entity instanceof Chapter || $entity instanceof Book) {
122 if ($entity->pages()->where('id', '=', $customHomeId)->exists()) {
123 if ($customHomeActive) {
124 throw new NotifyException(trans('errors.page_custom_home_deletion'), $entity->getUrl());
126 $removeCustomHome = true;
130 if ($removeCustomHome) {
131 setting()->remove('app-homepage');
136 * Remove a bookshelf from the system.
140 protected function destroyShelf(Bookshelf $shelf): int
142 $this->destroyCommonRelations($shelf);
143 $shelf->forceDelete();
149 * Remove a book from the system.
150 * Destroys any child chapters and pages.
154 protected function destroyBook(Book $book): int
157 $pages = $book->pages()->withTrashed()->get();
158 foreach ($pages as $page) {
159 $this->destroyPage($page);
163 $chapters = $book->chapters()->withTrashed()->get();
164 foreach ($chapters as $chapter) {
165 $this->destroyChapter($chapter);
169 $this->destroyCommonRelations($book);
170 $book->forceDelete();
176 * Remove a chapter from the system.
177 * Destroys all pages within.
181 protected function destroyChapter(Chapter $chapter): int
184 $pages = $chapter->pages()->withTrashed()->get();
185 foreach ($pages as $page) {
186 $this->destroyPage($page);
190 $this->destroyCommonRelations($chapter);
191 $chapter->forceDelete();
197 * Remove a page from the system.
201 protected function destroyPage(Page $page): int
203 $this->destroyCommonRelations($page);
204 $page->allRevisions()->delete();
206 // Delete Attached Files
207 $attachmentService = app()->make(AttachmentService::class);
208 foreach ($page->attachments as $attachment) {
209 $attachmentService->deleteFile($attachment);
212 // Remove book template usages
213 $this->queries->books->start()
214 ->where('default_template_id', '=', $page->id)
215 ->update(['default_template_id' => null]);
217 // Remove chapter template usages
218 $this->queries->chapters->start()
219 ->where('default_template_id', '=', $page->id)
220 ->update(['default_template_id' => null]);
222 $page->forceDelete();
228 * Get the total counts of those that have been trashed
229 * but not yet fully deleted (In recycle bin).
231 public function getTrashedCounts(): array
235 foreach ((new EntityProvider())->all() as $key => $instance) {
236 /** @var Builder<Entity> $query */
237 $query = $instance->newQuery();
238 $counts[$key] = $query->onlyTrashed()->count();
245 * Destroy all items that have pending deletions.
249 public function empty(): int
251 $deletions = Deletion::all();
253 foreach ($deletions as $deletion) {
254 $deleteCount += $this->destroyFromDeletion($deletion);
261 * Destroy an element from the given deletion model.
265 public function destroyFromDeletion(Deletion $deletion): int
267 // We directly load the deletable element here just to ensure it still
268 // exists in the event it has already been destroyed during this request.
269 $entity = $deletion->deletable()->first();
272 $count = $this->destroyEntity($deletion->deletable);
280 * Restore the content within the given deletion.
284 public function restoreFromDeletion(Deletion $deletion): int
286 $shouldRestore = true;
289 if ($deletion->deletable instanceof Entity) {
290 $parent = $deletion->deletable->getParent();
291 if ($parent && $parent->trashed()) {
292 $shouldRestore = false;
296 if ($deletion->deletable instanceof Entity && $shouldRestore) {
297 $restoreCount = $this->restoreEntity($deletion->deletable);
302 return $restoreCount;
306 * Automatically clear old content from the recycle bin
307 * depending on the configured lifetime.
308 * Returns the total number of deleted elements.
312 public function autoClearOld(): int
314 $lifetime = intval(config('app.recycle_bin_lifetime'));
319 $clearBeforeDate = Carbon::now()->addSeconds(10)->subDays($lifetime);
322 $deletionsToRemove = Deletion::query()->where('created_at', '<', $clearBeforeDate)->get();
323 foreach ($deletionsToRemove as $deletion) {
324 $deleteCount += $this->destroyFromDeletion($deletion);
331 * Restore an entity so it is essentially un-deleted.
332 * Deletions on restored child elements will be removed during this restoration.
334 protected function restoreEntity(Entity $entity): int
339 $restoreAction = function ($entity) use (&$count) {
340 if ($entity->deletions_count > 0) {
341 $entity->deletions()->delete();
348 if ($entity instanceof Chapter || $entity instanceof Book) {
349 $entity->pages()->withTrashed()->withCount('deletions')->get()->each($restoreAction);
352 if ($entity instanceof Book) {
353 $entity->chapters()->withTrashed()->withCount('deletions')->get()->each($restoreAction);
360 * Destroy the given entity.
361 * Returns the number of total entities destroyed in the operation.
365 public function destroyEntity(Entity $entity): int
367 $result = (new DatabaseTransaction(function () use ($entity) {
368 if ($entity instanceof Page) {
369 return $this->destroyPage($entity);
370 } else if ($entity instanceof Chapter) {
371 return $this->destroyChapter($entity);
372 } else if ($entity instanceof Book) {
373 return $this->destroyBook($entity);
374 } else if ($entity instanceof Bookshelf) {
375 return $this->destroyShelf($entity);
384 * Update entity relations to remove or update outstanding connections.
386 protected function destroyCommonRelations(Entity $entity)
388 Activity::removeEntity($entity);
389 $entity->views()->delete();
390 $entity->permissions()->delete();
391 $entity->tags()->delete();
392 $entity->comments()->delete();
393 $entity->jointPermissions()->delete();
394 $entity->searchTerms()->delete();
395 $entity->deletions()->delete();
396 $entity->favourites()->delete();
397 $entity->watches()->delete();
398 $entity->referencesTo()->delete();
399 $entity->referencesFrom()->delete();
401 if ($entity instanceof HasCoverImage && $entity->cover()->exists()) {
402 $imageService = app()->make(ImageService::class);
403 $imageService->destroy($entity->cover()->first());