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