]> BookStack Code Mirror - bookstack/blob - app/Entities/Repos/EntityRepo.php
Use joint_permissions to determine is a user has an available page or chapter to...
[bookstack] / app / Entities / Repos / EntityRepo.php
1 <?php namespace BookStack\Entities\Repos;
2
3 use BookStack\Actions\TagRepo;
4 use BookStack\Actions\ViewService;
5 use BookStack\Auth\Permissions\PermissionService;
6 use BookStack\Auth\User;
7 use BookStack\Entities\Book;
8 use BookStack\Entities\Bookshelf;
9 use BookStack\Entities\Chapter;
10 use BookStack\Entities\Entity;
11 use BookStack\Entities\EntityProvider;
12 use BookStack\Entities\Page;
13 use BookStack\Entities\SearchService;
14 use BookStack\Exceptions\NotFoundException;
15 use BookStack\Exceptions\NotifyException;
16 use BookStack\Uploads\AttachmentService;
17 use DOMDocument;
18 use Illuminate\Http\Request;
19 use Illuminate\Support\Collection;
20
21 class EntityRepo
22 {
23
24     /**
25      * @var EntityProvider
26      */
27     protected $entityProvider;
28
29     /**
30      * @var PermissionService
31      */
32     protected $permissionService;
33
34     /**
35      * @var ViewService
36      */
37     protected $viewService;
38
39     /**
40      * @var TagRepo
41      */
42     protected $tagRepo;
43
44     /**
45      * @var SearchService
46      */
47     protected $searchService;
48
49     /**
50      * EntityRepo constructor.
51      * @param EntityProvider $entityProvider
52      * @param ViewService $viewService
53      * @param PermissionService $permissionService
54      * @param TagRepo $tagRepo
55      * @param SearchService $searchService
56      */
57     public function __construct(
58         EntityProvider $entityProvider,
59         ViewService $viewService,
60         PermissionService $permissionService,
61         TagRepo $tagRepo,
62         SearchService $searchService
63     ) {
64         $this->entityProvider = $entityProvider;
65         $this->viewService = $viewService;
66         $this->permissionService = $permissionService;
67         $this->tagRepo = $tagRepo;
68         $this->searchService = $searchService;
69     }
70
71     /**
72      * Base query for searching entities via permission system
73      * @param string $type
74      * @param bool $allowDrafts
75      * @param string $permission
76      * @return \Illuminate\Database\Query\Builder
77      */
78     protected function entityQuery($type, $allowDrafts = false, $permission = 'view')
79     {
80         $q = $this->permissionService->enforceEntityRestrictions($type, $this->entityProvider->get($type), $permission);
81         if (strtolower($type) === 'page' && !$allowDrafts) {
82             $q = $q->where('draft', '=', false);
83         }
84         return $q;
85     }
86
87     /**
88      * Check if an entity with the given id exists.
89      * @param $type
90      * @param $id
91      * @return bool
92      */
93     public function exists($type, $id)
94     {
95         return $this->entityQuery($type)->where('id', '=', $id)->exists();
96     }
97
98     /**
99      * Get an entity by ID
100      * @param string $type
101      * @param integer $id
102      * @param bool $allowDrafts
103      * @param bool $ignorePermissions
104      * @return \BookStack\Entities\Entity
105      */
106     public function getById($type, $id, $allowDrafts = false, $ignorePermissions = false)
107     {
108         $query = $this->entityQuery($type, $allowDrafts);
109
110         if ($ignorePermissions) {
111             $query = $this->entityProvider->get($type)->newQuery();
112         }
113
114         return $query->find($id);
115     }
116
117     /**
118      * @param string $type
119      * @param []int $ids
120      * @param bool $allowDrafts
121      * @param bool $ignorePermissions
122      * @return \Illuminate\Database\Eloquent\Builder[]|\Illuminate\Database\Eloquent\Collection|Collection
123      */
124     public function getManyById($type, $ids, $allowDrafts = false, $ignorePermissions = false)
125     {
126         $query = $this->entityQuery($type, $allowDrafts);
127
128         if ($ignorePermissions) {
129             $query = $this->entityProvider->get($type)->newQuery();
130         }
131
132         return $query->whereIn('id', $ids)->get();
133     }
134
135     /**
136      * Get an entity by its url slug.
137      * @param string $type
138      * @param string $slug
139      * @param string|bool $bookSlug
140      * @return \BookStack\Entities\Entity
141      * @throws NotFoundException
142      */
143     public function getBySlug($type, $slug, $bookSlug = false)
144     {
145         $q = $this->entityQuery($type)->where('slug', '=', $slug);
146
147         if (strtolower($type) === 'chapter' || strtolower($type) === 'page') {
148             $q = $q->where('book_id', '=', function ($query) use ($bookSlug) {
149                 $query->select('id')
150                     ->from($this->entityProvider->book->getTable())
151                     ->where('slug', '=', $bookSlug)->limit(1);
152             });
153         }
154         $entity = $q->first();
155         if ($entity === null) {
156             throw new NotFoundException(trans('errors.' . strtolower($type) . '_not_found'));
157         }
158         return $entity;
159     }
160
161
162     /**
163      * Get all entities of a type with the given permission, limited by count unless count is false.
164      * @param string $type
165      * @param integer|bool $count
166      * @param string $permission
167      * @return Collection
168      */
169     public function getAll($type, $count = 20, $permission = 'view')
170     {
171         $q = $this->entityQuery($type, false, $permission)->orderBy('name', 'asc');
172         if ($count !== false) {
173             $q = $q->take($count);
174         }
175         return $q->get();
176     }
177
178     /**
179      * Get all entities in a paginated format
180      * @param $type
181      * @param int $count
182      * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
183      */
184     public function getAllPaginated($type, $count = 10)
185     {
186         return $this->entityQuery($type)->orderBy('name', 'asc')->paginate($count);
187     }
188
189     /**
190      * Get the most recently created entities of the given type.
191      * @param string $type
192      * @param int $count
193      * @param int $page
194      * @param bool|callable $additionalQuery
195      * @return Collection
196      */
197     public function getRecentlyCreated($type, $count = 20, $page = 0, $additionalQuery = false)
198     {
199         $query = $this->permissionService->enforceEntityRestrictions($type, $this->entityProvider->get($type))
200             ->orderBy('created_at', 'desc');
201         if (strtolower($type) === 'page') {
202             $query = $query->where('draft', '=', false);
203         }
204         if ($additionalQuery !== false && is_callable($additionalQuery)) {
205             $additionalQuery($query);
206         }
207         return $query->skip($page * $count)->take($count)->get();
208     }
209
210     /**
211      * Get the most recently updated entities of the given type.
212      * @param string $type
213      * @param int $count
214      * @param int $page
215      * @param bool|callable $additionalQuery
216      * @return Collection
217      */
218     public function getRecentlyUpdated($type, $count = 20, $page = 0, $additionalQuery = false)
219     {
220         $query = $this->permissionService->enforceEntityRestrictions($type, $this->entityProvider->get($type))
221             ->orderBy('updated_at', 'desc');
222         if (strtolower($type) === 'page') {
223             $query = $query->where('draft', '=', false);
224         }
225         if ($additionalQuery !== false && is_callable($additionalQuery)) {
226             $additionalQuery($query);
227         }
228         return $query->skip($page * $count)->take($count)->get();
229     }
230
231     /**
232      * Get the most recently viewed entities.
233      * @param string|bool $type
234      * @param int $count
235      * @param int $page
236      * @return mixed
237      */
238     public function getRecentlyViewed($type, $count = 10, $page = 0)
239     {
240         $filter = is_bool($type) ? false : $this->entityProvider->get($type);
241         return $this->viewService->getUserRecentlyViewed($count, $page, $filter);
242     }
243
244     /**
245      * Get the latest pages added to the system with pagination.
246      * @param string $type
247      * @param int $count
248      * @return mixed
249      */
250     public function getRecentlyCreatedPaginated($type, $count = 20)
251     {
252         return $this->entityQuery($type)->orderBy('created_at', 'desc')->paginate($count);
253     }
254
255     /**
256      * Get the latest pages added to the system with pagination.
257      * @param string $type
258      * @param int $count
259      * @return mixed
260      */
261     public function getRecentlyUpdatedPaginated($type, $count = 20)
262     {
263         return $this->entityQuery($type)->orderBy('updated_at', 'desc')->paginate($count);
264     }
265
266     /**
267      * Get the most popular entities base on all views.
268      * @param string|bool $type
269      * @param int $count
270      * @param int $page
271      * @return mixed
272      */
273     public function getPopular($type, $count = 10, $page = 0)
274     {
275         $filter = is_bool($type) ? false : $this->entityProvider->get($type);
276         return $this->viewService->getPopular($count, $page, $filter);
277     }
278
279     /**
280      * Get draft pages owned by the current user.
281      * @param int $count
282      * @param int $page
283      * @return Collection
284      */
285     public function getUserDraftPages($count = 20, $page = 0)
286     {
287         return $this->entityProvider->page->where('draft', '=', true)
288             ->where('created_by', '=', user()->id)
289             ->orderBy('updated_at', 'desc')
290             ->skip($count * $page)->take($count)->get();
291     }
292
293     /**
294      * Get the number of entities the given user has created.
295      * @param string $type
296      * @param User $user
297      * @return int
298      */
299     public function getUserTotalCreated(string $type, User $user)
300     {
301         return $this->entityProvider->get($type)
302             ->where('created_by', '=', $user->id)->count();
303     }
304
305     /**
306      * Get the child items for a chapter sorted by priority but
307      * with draft items floated to the top.
308      * @param \BookStack\Entities\Bookshelf $bookshelf
309      * @return \Illuminate\Database\Eloquent\Collection|static[]
310      */
311     public function getBookshelfChildren(Bookshelf $bookshelf)
312     {
313         return $this->permissionService->enforceEntityRestrictions('book', $bookshelf->books())->get();
314     }
315
316     /**
317      * Get all child objects of a book.
318      * Returns a sorted collection of Pages and Chapters.
319      * Loads the book slug onto child elements to prevent access database access for getting the slug.
320      * @param \BookStack\Entities\Book $book
321      * @param bool $filterDrafts
322      * @param bool $renderPages
323      * @return mixed
324      */
325     public function getBookChildren(Book $book, $filterDrafts = false, $renderPages = false)
326     {
327         $q = $this->permissionService->bookChildrenQuery($book->id, $filterDrafts, $renderPages)->get();
328         $entities = [];
329         $parents = [];
330         $tree = [];
331
332         foreach ($q as $index => $rawEntity) {
333             if ($rawEntity->entity_type ===  $this->entityProvider->page->getMorphClass()) {
334                 $entities[$index] = $this->entityProvider->page->newFromBuilder($rawEntity);
335                 if ($renderPages) {
336                     $entities[$index]->html = $rawEntity->html;
337                     $entities[$index]->html = $this->renderPage($entities[$index]);
338                 };
339             } else if ($rawEntity->entity_type === $this->entityProvider->chapter->getMorphClass()) {
340                 $entities[$index] = $this->entityProvider->chapter->newFromBuilder($rawEntity);
341                 $key = $entities[$index]->entity_type . ':' . $entities[$index]->id;
342                 $parents[$key] = $entities[$index];
343                 $parents[$key]->setAttribute('pages', collect());
344             }
345             if ($entities[$index]->chapter_id === 0 || $entities[$index]->chapter_id === '0') {
346                 $tree[] = $entities[$index];
347             }
348             $entities[$index]->book = $book;
349         }
350
351         foreach ($entities as $entity) {
352             if ($entity->chapter_id === 0 || $entity->chapter_id === '0') {
353                 continue;
354             }
355             $parentKey = $this->entityProvider->chapter->getMorphClass() . ':' . $entity->chapter_id;
356             if (!isset($parents[$parentKey])) {
357                 $tree[] = $entity;
358                 continue;
359             }
360             $chapter = $parents[$parentKey];
361             $chapter->pages->push($entity);
362         }
363
364         return collect($tree);
365     }
366
367     /**
368      * Get the child items for a chapter sorted by priority but
369      * with draft items floated to the top.
370      * @param \BookStack\Entities\Chapter $chapter
371      * @return \Illuminate\Database\Eloquent\Collection|static[]
372      */
373     public function getChapterChildren(Chapter $chapter)
374     {
375         return $this->permissionService->enforceEntityRestrictions('page', $chapter->pages())
376             ->orderBy('draft', 'DESC')->orderBy('priority', 'ASC')->get();
377     }
378
379
380     /**
381      * Get the next sequential priority for a new child element in the given book.
382      * @param \BookStack\Entities\Book $book
383      * @return int
384      */
385     public function getNewBookPriority(Book $book)
386     {
387         $lastElem = $this->getBookChildren($book)->pop();
388         return $lastElem ? $lastElem->priority + 1 : 0;
389     }
390
391     /**
392      * Get a new priority for a new page to be added to the given chapter.
393      * @param \BookStack\Entities\Chapter $chapter
394      * @return int
395      */
396     public function getNewChapterPriority(Chapter $chapter)
397     {
398         $lastPage = $chapter->pages('DESC')->first();
399         return $lastPage !== null ? $lastPage->priority + 1 : 0;
400     }
401
402     /**
403      * Find a suitable slug for an entity.
404      * @param string $type
405      * @param string $name
406      * @param bool|integer $currentId
407      * @param bool|integer $bookId Only pass if type is not a book
408      * @return string
409      */
410     public function findSuitableSlug($type, $name, $currentId = false, $bookId = false)
411     {
412         $slug = $this->nameToSlug($name);
413         while ($this->slugExists($type, $slug, $currentId, $bookId)) {
414             $slug .= '-' . substr(md5(rand(1, 500)), 0, 3);
415         }
416         return $slug;
417     }
418
419     /**
420      * Check if a slug already exists in the database.
421      * @param string $type
422      * @param string $slug
423      * @param bool|integer $currentId
424      * @param bool|integer $bookId
425      * @return bool
426      */
427     protected function slugExists($type, $slug, $currentId = false, $bookId = false)
428     {
429         $query = $this->entityProvider->get($type)->where('slug', '=', $slug);
430         if (strtolower($type) === 'page' || strtolower($type) === 'chapter') {
431             $query = $query->where('book_id', '=', $bookId);
432         }
433         if ($currentId) {
434             $query = $query->where('id', '!=', $currentId);
435         }
436         return $query->count() > 0;
437     }
438
439     /**
440      * Updates entity restrictions from a request
441      * @param Request $request
442      * @param \BookStack\Entities\Entity $entity
443      * @throws \Throwable
444      */
445     public function updateEntityPermissionsFromRequest(Request $request, Entity $entity)
446     {
447         $entity->restricted = $request->get('restricted', '') === 'true';
448         $entity->permissions()->delete();
449
450         if ($request->filled('restrictions')) {
451             foreach ($request->get('restrictions') as $roleId => $restrictions) {
452                 foreach ($restrictions as $action => $value) {
453                     $entity->permissions()->create([
454                         'role_id' => $roleId,
455                         'action'  => strtolower($action)
456                     ]);
457                 }
458             }
459         }
460
461         $entity->save();
462         $this->permissionService->buildJointPermissionsForEntity($entity);
463     }
464
465
466
467     /**
468      * Create a new entity from request input.
469      * Used for books and chapters.
470      * @param string $type
471      * @param array $input
472      * @param bool|Book $book
473      * @return \BookStack\Entities\Entity
474      */
475     public function createFromInput($type, $input = [], $book = false)
476     {
477         $isChapter = strtolower($type) === 'chapter';
478         $entityModel = $this->entityProvider->get($type)->newInstance($input);
479         $entityModel->slug = $this->findSuitableSlug($type, $entityModel->name, false, $isChapter ? $book->id : false);
480         $entityModel->created_by = user()->id;
481         $entityModel->updated_by = user()->id;
482         $isChapter ? $book->chapters()->save($entityModel) : $entityModel->save();
483
484         if (isset($input['tags'])) {
485             $this->tagRepo->saveTagsToEntity($entityModel, $input['tags']);
486         }
487
488         $this->permissionService->buildJointPermissionsForEntity($entityModel);
489         $this->searchService->indexEntity($entityModel);
490         return $entityModel;
491     }
492
493     /**
494      * Update entity details from request input.
495      * Used for books and chapters
496      * @param string $type
497      * @param \BookStack\Entities\Entity $entityModel
498      * @param array $input
499      * @return \BookStack\Entities\Entity
500      */
501     public function updateFromInput($type, Entity $entityModel, $input = [])
502     {
503         if ($entityModel->name !== $input['name']) {
504             $entityModel->slug = $this->findSuitableSlug($type, $input['name'], $entityModel->id);
505         }
506         $entityModel->fill($input);
507         $entityModel->updated_by = user()->id;
508         $entityModel->save();
509
510         if (isset($input['tags'])) {
511             $this->tagRepo->saveTagsToEntity($entityModel, $input['tags']);
512         }
513
514         $this->permissionService->buildJointPermissionsForEntity($entityModel);
515         $this->searchService->indexEntity($entityModel);
516         return $entityModel;
517     }
518
519     /**
520      * Sync the books assigned to a shelf from a comma-separated list
521      * of book IDs.
522      * @param \BookStack\Entities\Bookshelf $shelf
523      * @param string $books
524      */
525     public function updateShelfBooks(Bookshelf $shelf, string $books)
526     {
527         $ids = explode(',', $books);
528
529         // Check books exist and match ordering
530         $bookIds = $this->entityQuery('book')->whereIn('id', $ids)->get(['id'])->pluck('id');
531         $syncData = [];
532         foreach ($ids as $index => $id) {
533             if ($bookIds->contains($id)) {
534                 $syncData[$id] = ['order' => $index];
535             }
536         }
537
538         $shelf->books()->sync($syncData);
539     }
540
541     /**
542      * Change the book that an entity belongs to.
543      * @param string $type
544      * @param integer $newBookId
545      * @param Entity $entity
546      * @param bool $rebuildPermissions
547      * @return \BookStack\Entities\Entity
548      */
549     public function changeBook($type, $newBookId, Entity $entity, $rebuildPermissions = false)
550     {
551         $entity->book_id = $newBookId;
552         // Update related activity
553         foreach ($entity->activity as $activity) {
554             $activity->book_id = $newBookId;
555             $activity->save();
556         }
557         $entity->slug = $this->findSuitableSlug($type, $entity->name, $entity->id, $newBookId);
558         $entity->save();
559
560         // Update all child pages if a chapter
561         if (strtolower($type) === 'chapter') {
562             foreach ($entity->pages as $page) {
563                 $this->changeBook('page', $newBookId, $page, false);
564             }
565         }
566
567         // Update permissions if applicable
568         if ($rebuildPermissions) {
569             $entity->load('book');
570             $this->permissionService->buildJointPermissionsForEntity($entity->book);
571         }
572
573         return $entity;
574     }
575
576     /**
577      * Alias method to update the book jointPermissions in the PermissionService.
578      * @param Book $book
579      */
580     public function buildJointPermissionsForBook(Book $book)
581     {
582         $this->permissionService->buildJointPermissionsForEntity($book);
583     }
584
585     /**
586      * Format a name as a url slug.
587      * @param $name
588      * @return string
589      */
590     protected function nameToSlug($name)
591     {
592         $slug = preg_replace('/[\+\/\\\?\@\}\{\.\,\=\[\]\#\&\!\*\'\;\:\$\%]/', '', mb_strtolower($name));
593         $slug = preg_replace('/\s{2,}/', ' ', $slug);
594         $slug = str_replace(' ', '-', $slug);
595         if ($slug === "") {
596             $slug = substr(md5(rand(1, 500)), 0, 5);
597         }
598         return $slug;
599     }
600
601     /**
602      * Render the page for viewing, Parsing and performing features such as page transclusion.
603      * @param Page $page
604      * @param bool $ignorePermissions
605      * @return mixed|string
606      */
607     public function renderPage(Page $page, $ignorePermissions = false)
608     {
609         $content = $page->html;
610         if (!config('app.allow_content_scripts')) {
611             $content = $this->escapeScripts($content);
612         }
613
614         $matches = [];
615         preg_match_all("/{{@\s?([0-9].*?)}}/", $content, $matches);
616         if (count($matches[0]) === 0) {
617             return $content;
618         }
619
620         $topLevelTags = ['table', 'ul', 'ol'];
621         foreach ($matches[1] as $index => $includeId) {
622             $splitInclude = explode('#', $includeId, 2);
623             $pageId = intval($splitInclude[0]);
624             if (is_nan($pageId)) {
625                 continue;
626             }
627
628             $matchedPage = $this->getById('page', $pageId, false, $ignorePermissions);
629             if ($matchedPage === null) {
630                 $content = str_replace($matches[0][$index], '', $content);
631                 continue;
632             }
633
634             if (count($splitInclude) === 1) {
635                 $content = str_replace($matches[0][$index], $matchedPage->html, $content);
636                 continue;
637             }
638
639             $doc = new DOMDocument();
640             $doc->loadHTML(mb_convert_encoding('<body>'.$matchedPage->html.'</body>', 'HTML-ENTITIES', 'UTF-8'));
641             $matchingElem = $doc->getElementById($splitInclude[1]);
642             if ($matchingElem === null) {
643                 $content = str_replace($matches[0][$index], '', $content);
644                 continue;
645             }
646             $innerContent = '';
647             $isTopLevel = in_array(strtolower($matchingElem->nodeName), $topLevelTags);
648             if ($isTopLevel) {
649                 $innerContent .= $doc->saveHTML($matchingElem);
650             } else {
651                 foreach ($matchingElem->childNodes as $childNode) {
652                     $innerContent .= $doc->saveHTML($childNode);
653                 }
654             }
655             $content = str_replace($matches[0][$index], trim($innerContent), $content);
656         }
657
658         return $content;
659     }
660
661     /**
662      * Escape script tags within HTML content.
663      * @param string $html
664      * @return mixed
665      */
666     protected function escapeScripts(string $html)
667     {
668         $scriptSearchRegex = '/<script.*?>.*?<\/script>/ms';
669         $matches = [];
670         preg_match_all($scriptSearchRegex, $html, $matches);
671         if (count($matches) === 0) {
672             return $html;
673         }
674
675         foreach ($matches[0] as $match) {
676             $html = str_replace($match, htmlentities($match), $html);
677         }
678         return $html;
679     }
680
681     /**
682      * Search for image usage within page content.
683      * @param $imageString
684      * @return mixed
685      */
686     public function searchForImage($imageString)
687     {
688         $pages = $this->entityQuery('page')->where('html', 'like', '%' . $imageString . '%')->get();
689         foreach ($pages as $page) {
690             $page->url = $page->getUrl();
691             $page->html = '';
692             $page->text = '';
693         }
694         return count($pages) > 0 ? $pages : false;
695     }
696
697     /**
698      * Destroy a bookshelf instance
699      * @param \BookStack\Entities\Bookshelf $shelf
700      * @throws \Throwable
701      */
702     public function destroyBookshelf(Bookshelf $shelf)
703     {
704         $this->destroyEntityCommonRelations($shelf);
705         $shelf->delete();
706     }
707
708     /**
709      * Destroy the provided book and all its child entities.
710      * @param \BookStack\Entities\Book $book
711      * @throws NotifyException
712      * @throws \Throwable
713      */
714     public function destroyBook(Book $book)
715     {
716         foreach ($book->pages as $page) {
717             $this->destroyPage($page);
718         }
719         foreach ($book->chapters as $chapter) {
720             $this->destroyChapter($chapter);
721         }
722         $this->destroyEntityCommonRelations($book);
723         $book->delete();
724     }
725
726     /**
727      * Destroy a chapter and its relations.
728      * @param \BookStack\Entities\Chapter $chapter
729      * @throws \Throwable
730      */
731     public function destroyChapter(Chapter $chapter)
732     {
733         if (count($chapter->pages) > 0) {
734             foreach ($chapter->pages as $page) {
735                 $page->chapter_id = 0;
736                 $page->save();
737             }
738         }
739         $this->destroyEntityCommonRelations($chapter);
740         $chapter->delete();
741     }
742
743     /**
744      * Destroy a given page along with its dependencies.
745      * @param Page $page
746      * @throws NotifyException
747      * @throws \Throwable
748      */
749     public function destroyPage(Page $page)
750     {
751         // Check if set as custom homepage
752         $customHome = setting('app-homepage', '0:');
753         if (intval($page->id) === intval(explode(':', $customHome)[0])) {
754             throw new NotifyException(trans('errors.page_custom_home_deletion'), $page->getUrl());
755         }
756
757         $this->destroyEntityCommonRelations($page);
758
759         // Delete Attached Files
760         $attachmentService = app(AttachmentService::class);
761         foreach ($page->attachments as $attachment) {
762             $attachmentService->deleteFile($attachment);
763         }
764
765         $page->delete();
766     }
767
768     /**
769      * Destroy or handle the common relations connected to an entity.
770      * @param \BookStack\Entities\Entity $entity
771      * @throws \Throwable
772      */
773     protected function destroyEntityCommonRelations(Entity $entity)
774     {
775         \Activity::removeEntity($entity);
776         $entity->views()->delete();
777         $entity->permissions()->delete();
778         $entity->tags()->delete();
779         $entity->comments()->delete();
780         $this->permissionService->deleteJointPermissionsForEntity($entity);
781         $this->searchService->deleteEntityTerms($entity);
782     }
783
784     /**
785      * Copy the permissions of a bookshelf to all child books.
786      * Returns the number of books that had permissions updated.
787      * @param \BookStack\Entities\Bookshelf $bookshelf
788      * @return int
789      * @throws \Throwable
790      */
791     public function copyBookshelfPermissions(Bookshelf $bookshelf)
792     {
793         $shelfPermissions = $bookshelf->permissions()->get(['role_id', 'action'])->toArray();
794         $shelfBooks = $bookshelf->books()->get();
795         $updatedBookCount = 0;
796
797         foreach ($shelfBooks as $book) {
798             if (!userCan('restrictions-manage', $book)) {
799                 continue;
800             }
801             $book->permissions()->delete();
802             $book->restricted = $bookshelf->restricted;
803             $book->permissions()->createMany($shelfPermissions);
804             $book->save();
805             $this->permissionService->buildJointPermissionsForEntity($book);
806             $updatedBookCount++;
807         }
808
809         return $updatedBookCount;
810     }
811 }