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