]> BookStack Code Mirror - bookstack/blob - app/Entities/Tools/TrashCan.php
Code cleanup, bug squashing
[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
155         // Delete Attached Files
156         $attachmentService = app(AttachmentService::class);
157         foreach ($page->attachments as $attachment) {
158             $attachmentService->deleteFile($attachment);
159         }
160
161         $page->forceDelete();
162         return 1;
163     }
164
165     /**
166      * Get the total counts of those that have been trashed
167      * but not yet fully deleted (In recycle bin).
168      */
169     public function getTrashedCounts(): array
170     {
171         $counts = [];
172
173         /** @var Entity $instance */
174         foreach ((new EntityProvider)->all() as $key => $instance) {
175             $counts[$key] = $instance->newQuery()->onlyTrashed()->count();
176         }
177
178         return $counts;
179     }
180
181     /**
182      * Destroy all items that have pending deletions.
183      * @throws Exception
184      */
185     public function empty(): int
186     {
187         $deletions = Deletion::all();
188         $deleteCount = 0;
189         foreach ($deletions as $deletion) {
190             $deleteCount += $this->destroyFromDeletion($deletion);
191         }
192         return $deleteCount;
193     }
194
195     /**
196      * Destroy an element from the given deletion model.
197      * @throws Exception
198      */
199     public function destroyFromDeletion(Deletion $deletion): int
200     {
201         // We directly load the deletable element here just to ensure it still
202         // exists in the event it has already been destroyed during this request.
203         $entity = $deletion->deletable()->first();
204         $count = 0;
205         if ($entity) {
206             $count = $this->destroyEntity($deletion->deletable);
207         }
208         $deletion->delete();
209         return $count;
210     }
211
212     /**
213      * Restore the content within the given deletion.
214      * @throws Exception
215      */
216     public function restoreFromDeletion(Deletion $deletion): int
217     {
218         $shouldRestore = true;
219         $restoreCount = 0;
220         $parent = $deletion->deletable->getParent();
221
222         if ($parent && $parent->trashed()) {
223             $shouldRestore = false;
224         }
225
226         if ($shouldRestore) {
227             $restoreCount = $this->restoreEntity($deletion->deletable);
228         }
229
230         $deletion->delete();
231         return $restoreCount;
232     }
233
234     /**
235      * Automatically clear old content from the recycle bin
236      * depending on the configured lifetime.
237      * Returns the total number of deleted elements.
238      * @throws Exception
239      */
240     public function autoClearOld(): int
241     {
242         $lifetime = intval(config('app.recycle_bin_lifetime'));
243         if ($lifetime < 0) {
244             return 0;
245         }
246
247         $clearBeforeDate = Carbon::now()->addSeconds(10)->subDays($lifetime);
248         $deleteCount = 0;
249
250         $deletionsToRemove = Deletion::query()->where('created_at', '<', $clearBeforeDate)->get();
251         foreach ($deletionsToRemove as $deletion) {
252             $deleteCount += $this->destroyFromDeletion($deletion);
253         }
254
255         return $deleteCount;
256     }
257
258     /**
259      * Restore an entity so it is essentially un-deleted.
260      * Deletions on restored child elements will be removed during this restoration.
261      */
262     protected function restoreEntity(Entity $entity): int
263     {
264         $count = 1;
265         $entity->restore();
266
267         $restoreAction = function ($entity) use (&$count) {
268             if ($entity->deletions_count > 0) {
269                 $entity->deletions()->delete();
270             }
271
272             $entity->restore();
273             $count++;
274         };
275
276         if ($entity->isA('chapter') || $entity->isA('book')) {
277             $entity->pages()->withTrashed()->withCount('deletions')->get()->each($restoreAction);
278         }
279
280         if ($entity->isA('book')) {
281             $entity->chapters()->withTrashed()->withCount('deletions')->get()->each($restoreAction);
282         }
283
284         return $count;
285     }
286
287     /**
288      * Destroy the given entity.
289      */
290     protected function destroyEntity(Entity $entity): int
291     {
292         if ($entity->isA('page')) {
293             return $this->destroyPage($entity);
294         }
295         if ($entity->isA('chapter')) {
296             return $this->destroyChapter($entity);
297         }
298         if ($entity->isA('book')) {
299             return $this->destroyBook($entity);
300         }
301         if ($entity->isA('shelf')) {
302             return $this->destroyShelf($entity);
303         }
304     }
305
306     /**
307      * Update entity relations to remove or update outstanding connections.
308      */
309     protected function destroyCommonRelations(Entity $entity)
310     {
311         Activity::removeEntity($entity);
312         $entity->views()->delete();
313         $entity->permissions()->delete();
314         $entity->tags()->delete();
315         $entity->comments()->delete();
316         $entity->jointPermissions()->delete();
317         $entity->searchTerms()->delete();
318         $entity->deletions()->delete();
319
320         if ($entity instanceof HasCoverImage && $entity->cover) {
321             $imageService = app()->make(ImageService::class);
322             $imageService->destroy($entity->cover);
323         }
324     }
325 }