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