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