]> BookStack Code Mirror - bookstack/blob - app/Entities/Managers/TrashCan.php
Added per-item recycle-bin delete and restore
[bookstack] / app / Entities / Managers / TrashCan.php
1 <?php namespace BookStack\Entities\Managers;
2
3 use BookStack\Entities\Book;
4 use BookStack\Entities\Bookshelf;
5 use BookStack\Entities\Chapter;
6 use BookStack\Entities\Deletion;
7 use BookStack\Entities\Entity;
8 use BookStack\Entities\EntityProvider;
9 use BookStack\Entities\HasCoverImage;
10 use BookStack\Entities\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
17 class TrashCan
18 {
19
20     /**
21      * Send a shelf to the recycle bin.
22      */
23     public function softDestroyShelf(Bookshelf $shelf)
24     {
25         Deletion::createForEntity($shelf);
26         $shelf->delete();
27     }
28
29     /**
30      * Send a book to the recycle bin.
31      * @throws Exception
32      */
33     public function softDestroyBook(Book $book)
34     {
35         Deletion::createForEntity($book);
36
37         foreach ($book->pages as $page) {
38             $this->softDestroyPage($page, false);
39         }
40
41         foreach ($book->chapters as $chapter) {
42             $this->softDestroyChapter($chapter, false);
43         }
44
45         $book->delete();
46     }
47
48     /**
49      * Send a chapter to the recycle bin.
50      * @throws Exception
51      */
52     public function softDestroyChapter(Chapter $chapter, bool $recordDelete = true)
53     {
54         if ($recordDelete) {
55             Deletion::createForEntity($chapter);
56         }
57
58         if (count($chapter->pages) > 0) {
59             foreach ($chapter->pages as $page) {
60                 $this->softDestroyPage($page, false);
61             }
62         }
63
64         $chapter->delete();
65     }
66
67     /**
68      * Send a page to the recycle bin.
69      * @throws Exception
70      */
71     public function softDestroyPage(Page $page, bool $recordDelete = true)
72     {
73         if ($recordDelete) {
74             Deletion::createForEntity($page);
75         }
76
77         // Check if set as custom homepage & remove setting if not used or throw error if active
78         $customHome = setting('app-homepage', '0:');
79         if (intval($page->id) === intval(explode(':', $customHome)[0])) {
80             if (setting('app-homepage-type') === 'page') {
81                 throw new NotifyException(trans('errors.page_custom_home_deletion'), $page->getUrl());
82             }
83             setting()->remove('app-homepage');
84         }
85
86         $page->delete();
87     }
88
89     /**
90      * Remove a bookshelf from the system.
91      * @throws Exception
92      */
93     public function destroyShelf(Bookshelf $shelf): int
94     {
95         $this->destroyCommonRelations($shelf);
96         $shelf->forceDelete();
97         return 1;
98     }
99
100     /**
101      * Remove a book from the system.
102      * Destroys any child chapters and pages.
103      * @throws Exception
104      */
105     public function destroyBook(Book $book): int
106     {
107         $count = 0;
108         $pages = $book->pages()->withTrashed()->get();
109         foreach ($pages as $page) {
110             $this->destroyPage($page);
111             $count++;
112         }
113
114         $chapters = $book->chapters()->withTrashed()->get();
115         foreach ($chapters as $chapter) {
116             $this->destroyChapter($chapter);
117             $count++;
118         }
119
120         $this->destroyCommonRelations($book);
121         $book->forceDelete();
122         return $count + 1;
123     }
124
125     /**
126      * Remove a chapter from the system.
127      * Destroys all pages within.
128      * @throws Exception
129      */
130     public function destroyChapter(Chapter $chapter): int
131     {
132         $count = 0;
133         $pages = $chapter->pages()->withTrashed()->get();
134         if (count($pages)) {
135             foreach ($pages as $page) {
136                 $this->destroyPage($page);
137                 $count++;
138             }
139         }
140
141         $this->destroyCommonRelations($chapter);
142         $chapter->forceDelete();
143         return $count + 1;
144     }
145
146     /**
147      * Remove a page from the system.
148      * @throws Exception
149      */
150     public function destroyPage(Page $page): int
151     {
152         $this->destroyCommonRelations($page);
153
154         // Delete Attached Files
155         $attachmentService = app(AttachmentService::class);
156         foreach ($page->attachments as $attachment) {
157             $attachmentService->deleteFile($attachment);
158         }
159
160         $page->forceDelete();
161         return 1;
162     }
163
164     /**
165      * Get the total counts of those that have been trashed
166      * but not yet fully deleted (In recycle bin).
167      */
168     public function getTrashedCounts(): array
169     {
170         $provider = app(EntityProvider::class);
171         $counts = [];
172
173         /** @var Entity $instance */
174         foreach ($provider->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 destroyFromAllDeletions(): 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      * Restore an entity so it is essentially un-deleted.
236      * Deletions on restored child elements will be removed during this restoration.
237      */
238     protected function restoreEntity(Entity $entity): int
239     {
240         $count = 1;
241         $entity->restore();
242
243         if ($entity->isA('chapter') || $entity->isA('book')) {
244             foreach ($entity->pages()->withTrashed()->withCount('deletions')->get() as $page) {
245                 if ($page->deletions_count > 0) {
246                     $page->deletions()->delete();
247                 }
248
249                 $page->restore();
250                 $count++;
251             }
252         }
253
254         if ($entity->isA('book')) {
255             foreach ($entity->chapters()->withTrashed()->withCount('deletions')->get() as $chapter) {
256                 if ($chapter->deletions_count === 0) {
257                     $chapter->deletions()->delete();
258                 }
259
260                 $chapter->restore();
261                 $count++;
262             }
263         }
264
265         return $count;
266     }
267
268     /**
269      * Destroy the given entity.
270      */
271     protected function destroyEntity(Entity $entity): int
272     {
273         if ($entity->isA('page')) {
274             return $this->destroyPage($entity);
275         }
276         if ($entity->isA('chapter')) {
277             return $this->destroyChapter($entity);
278         }
279         if ($entity->isA('book')) {
280             return $this->destroyBook($entity);
281         }
282         if ($entity->isA('shelf')) {
283             return $this->destroyShelf($entity);
284         }
285     }
286
287     /**
288      * Update entity relations to remove or update outstanding connections.
289      */
290     protected function destroyCommonRelations(Entity $entity)
291     {
292         Activity::removeEntity($entity);
293         $entity->views()->delete();
294         $entity->permissions()->delete();
295         $entity->tags()->delete();
296         $entity->comments()->delete();
297         $entity->jointPermissions()->delete();
298         $entity->searchTerms()->delete();
299         $entity->deletions()->delete();
300
301         if ($entity instanceof HasCoverImage && $entity->cover) {
302             $imageService = app()->make(ImageService::class);
303             $imageService->destroy($entity->cover);
304         }
305     }
306 }