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