]> BookStack Code Mirror - bookstack/blob - app/Entities/Tools/TrashCan.php
Updated translator & dependency attribution before release v25.05.1
[bookstack] / app / Entities / Tools / TrashCan.php
1 <?php
2
3 namespace BookStack\Entities\Tools;
4
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 Exception;
19 use Illuminate\Database\Eloquent\Builder;
20 use Illuminate\Support\Carbon;
21
22 class TrashCan
23 {
24     public function __construct(
25         protected EntityQueries $queries,
26     ) {
27     }
28
29     /**
30      * Send a shelf to the recycle bin.
31      *
32      * @throws NotifyException
33      */
34     public function softDestroyShelf(Bookshelf $shelf)
35     {
36         $this->ensureDeletable($shelf);
37         Deletion::createForEntity($shelf);
38         $shelf->delete();
39     }
40
41     /**
42      * Send a book to the recycle bin.
43      *
44      * @throws Exception
45      */
46     public function softDestroyBook(Book $book)
47     {
48         $this->ensureDeletable($book);
49         Deletion::createForEntity($book);
50
51         foreach ($book->pages as $page) {
52             $this->softDestroyPage($page, false);
53         }
54
55         foreach ($book->chapters as $chapter) {
56             $this->softDestroyChapter($chapter, false);
57         }
58
59         $book->delete();
60     }
61
62     /**
63      * Send a chapter to the recycle bin.
64      *
65      * @throws Exception
66      */
67     public function softDestroyChapter(Chapter $chapter, bool $recordDelete = true)
68     {
69         if ($recordDelete) {
70             $this->ensureDeletable($chapter);
71             Deletion::createForEntity($chapter);
72         }
73
74         if (count($chapter->pages) > 0) {
75             foreach ($chapter->pages as $page) {
76                 $this->softDestroyPage($page, false);
77             }
78         }
79
80         $chapter->delete();
81     }
82
83     /**
84      * Send a page to the recycle bin.
85      *
86      * @throws Exception
87      */
88     public function softDestroyPage(Page $page, bool $recordDelete = true)
89     {
90         if ($recordDelete) {
91             $this->ensureDeletable($page);
92             Deletion::createForEntity($page);
93         }
94
95         $page->delete();
96     }
97
98     /**
99      * Ensure the given entity is deletable.
100      * Is not for permissions, but logical conditions within the application.
101      * Will throw if not deletable.
102      *
103      * @throws NotifyException
104      */
105     protected function ensureDeletable(Entity $entity): void
106     {
107         $customHomeId = intval(explode(':', setting('app-homepage', '0:'))[0]);
108         $customHomeActive = setting('app-homepage-type') === 'page';
109         $removeCustomHome = false;
110
111         // Check custom homepage usage for pages
112         if ($entity instanceof Page && $entity->id === $customHomeId) {
113             if ($customHomeActive) {
114                 throw new NotifyException(trans('errors.page_custom_home_deletion'), $entity->getUrl());
115             }
116             $removeCustomHome = true;
117         }
118
119         // Check custom homepage usage within chapters or books
120         if ($entity instanceof Chapter || $entity instanceof Book) {
121             if ($entity->pages()->where('id', '=', $customHomeId)->exists()) {
122                 if ($customHomeActive) {
123                     throw new NotifyException(trans('errors.page_custom_home_deletion'), $entity->getUrl());
124                 }
125                 $removeCustomHome = true;
126             }
127         }
128
129         if ($removeCustomHome) {
130             setting()->remove('app-homepage');
131         }
132     }
133
134     /**
135      * Remove a bookshelf from the system.
136      *
137      * @throws Exception
138      */
139     protected function destroyShelf(Bookshelf $shelf): int
140     {
141         $this->destroyCommonRelations($shelf);
142         $shelf->forceDelete();
143
144         return 1;
145     }
146
147     /**
148      * Remove a book from the system.
149      * Destroys any child chapters and pages.
150      *
151      * @throws Exception
152      */
153     protected function destroyBook(Book $book): int
154     {
155         $count = 0;
156         $pages = $book->pages()->withTrashed()->get();
157         foreach ($pages as $page) {
158             $this->destroyPage($page);
159             $count++;
160         }
161
162         $chapters = $book->chapters()->withTrashed()->get();
163         foreach ($chapters as $chapter) {
164             $this->destroyChapter($chapter);
165             $count++;
166         }
167
168         $this->destroyCommonRelations($book);
169         $book->forceDelete();
170
171         return $count + 1;
172     }
173
174     /**
175      * Remove a chapter from the system.
176      * Destroys all pages within.
177      *
178      * @throws Exception
179      */
180     protected function destroyChapter(Chapter $chapter): int
181     {
182         $count = 0;
183         $pages = $chapter->pages()->withTrashed()->get();
184         foreach ($pages as $page) {
185             $this->destroyPage($page);
186             $count++;
187         }
188
189         $this->destroyCommonRelations($chapter);
190         $chapter->forceDelete();
191
192         return $count + 1;
193     }
194
195     /**
196      * Remove a page from the system.
197      *
198      * @throws Exception
199      */
200     protected function destroyPage(Page $page): int
201     {
202         $this->destroyCommonRelations($page);
203         $page->allRevisions()->delete();
204
205         // Delete Attached Files
206         $attachmentService = app()->make(AttachmentService::class);
207         foreach ($page->attachments as $attachment) {
208             $attachmentService->deleteFile($attachment);
209         }
210
211         // Remove book template usages
212         $this->queries->books->start()
213             ->where('default_template_id', '=', $page->id)
214             ->update(['default_template_id' => null]);
215
216         // Remove chapter template usages
217         $this->queries->chapters->start()
218             ->where('default_template_id', '=', $page->id)
219             ->update(['default_template_id' => null]);
220
221         $page->forceDelete();
222
223         return 1;
224     }
225
226     /**
227      * Get the total counts of those that have been trashed
228      * but not yet fully deleted (In recycle bin).
229      */
230     public function getTrashedCounts(): array
231     {
232         $counts = [];
233
234         foreach ((new EntityProvider())->all() as $key => $instance) {
235             /** @var Builder<Entity> $query */
236             $query = $instance->newQuery();
237             $counts[$key] = $query->onlyTrashed()->count();
238         }
239
240         return $counts;
241     }
242
243     /**
244      * Destroy all items that have pending deletions.
245      *
246      * @throws Exception
247      */
248     public function empty(): int
249     {
250         $deletions = Deletion::all();
251         $deleteCount = 0;
252         foreach ($deletions as $deletion) {
253             $deleteCount += $this->destroyFromDeletion($deletion);
254         }
255
256         return $deleteCount;
257     }
258
259     /**
260      * Destroy an element from the given deletion model.
261      *
262      * @throws Exception
263      */
264     public function destroyFromDeletion(Deletion $deletion): int
265     {
266         // We directly load the deletable element here just to ensure it still
267         // exists in the event it has already been destroyed during this request.
268         $entity = $deletion->deletable()->first();
269         $count = 0;
270         if ($entity) {
271             $count = $this->destroyEntity($deletion->deletable);
272         }
273         $deletion->delete();
274
275         return $count;
276     }
277
278     /**
279      * Restore the content within the given deletion.
280      *
281      * @throws Exception
282      */
283     public function restoreFromDeletion(Deletion $deletion): int
284     {
285         $shouldRestore = true;
286         $restoreCount = 0;
287
288         if ($deletion->deletable instanceof Entity) {
289             $parent = $deletion->deletable->getParent();
290             if ($parent && $parent->trashed()) {
291                 $shouldRestore = false;
292             }
293         }
294
295         if ($deletion->deletable instanceof Entity && $shouldRestore) {
296             $restoreCount = $this->restoreEntity($deletion->deletable);
297         }
298
299         $deletion->delete();
300
301         return $restoreCount;
302     }
303
304     /**
305      * Automatically clear old content from the recycle bin
306      * depending on the configured lifetime.
307      * Returns the total number of deleted elements.
308      *
309      * @throws Exception
310      */
311     public function autoClearOld(): int
312     {
313         $lifetime = intval(config('app.recycle_bin_lifetime'));
314         if ($lifetime < 0) {
315             return 0;
316         }
317
318         $clearBeforeDate = Carbon::now()->addSeconds(10)->subDays($lifetime);
319         $deleteCount = 0;
320
321         $deletionsToRemove = Deletion::query()->where('created_at', '<', $clearBeforeDate)->get();
322         foreach ($deletionsToRemove as $deletion) {
323             $deleteCount += $this->destroyFromDeletion($deletion);
324         }
325
326         return $deleteCount;
327     }
328
329     /**
330      * Restore an entity so it is essentially un-deleted.
331      * Deletions on restored child elements will be removed during this restoration.
332      */
333     protected function restoreEntity(Entity $entity): int
334     {
335         $count = 1;
336         $entity->restore();
337
338         $restoreAction = function ($entity) use (&$count) {
339             if ($entity->deletions_count > 0) {
340                 $entity->deletions()->delete();
341             }
342
343             $entity->restore();
344             $count++;
345         };
346
347         if ($entity instanceof Chapter || $entity instanceof Book) {
348             $entity->pages()->withTrashed()->withCount('deletions')->get()->each($restoreAction);
349         }
350
351         if ($entity instanceof Book) {
352             $entity->chapters()->withTrashed()->withCount('deletions')->get()->each($restoreAction);
353         }
354
355         return $count;
356     }
357
358     /**
359      * Destroy the given entity.
360      *
361      * @throws Exception
362      */
363     public function destroyEntity(Entity $entity): int
364     {
365         if ($entity instanceof Page) {
366             return $this->destroyPage($entity);
367         }
368         if ($entity instanceof Chapter) {
369             return $this->destroyChapter($entity);
370         }
371         if ($entity instanceof Book) {
372             return $this->destroyBook($entity);
373         }
374         if ($entity instanceof Bookshelf) {
375             return $this->destroyShelf($entity);
376         }
377
378         return 0;
379     }
380
381     /**
382      * Update entity relations to remove or update outstanding connections.
383      */
384     protected function destroyCommonRelations(Entity $entity)
385     {
386         Activity::removeEntity($entity);
387         $entity->views()->delete();
388         $entity->permissions()->delete();
389         $entity->tags()->delete();
390         $entity->comments()->delete();
391         $entity->jointPermissions()->delete();
392         $entity->searchTerms()->delete();
393         $entity->deletions()->delete();
394         $entity->favourites()->delete();
395         $entity->watches()->delete();
396         $entity->referencesTo()->delete();
397         $entity->referencesFrom()->delete();
398
399         if ($entity instanceof HasCoverImage && $entity->cover()->exists()) {
400             $imageService = app()->make(ImageService::class);
401             $imageService->destroy($entity->cover()->first());
402         }
403     }
404 }
Morty Proxy This is a proxified and sanitized view of the page, visit original site.