]> BookStack Code Mirror - bookstack/blob - app/Entities/Tools/BookContents.php
Changed model loading and permission checking on book sort
[bookstack] / app / Entities / Tools / BookContents.php
1 <?php
2
3 namespace BookStack\Entities\Tools;
4
5 use BookStack\Entities\Models\Book;
6 use BookStack\Entities\Models\BookChild;
7 use BookStack\Entities\Models\Chapter;
8 use BookStack\Entities\Models\Entity;
9 use BookStack\Entities\Models\Page;
10 use Illuminate\Support\Collection;
11
12 class BookContents
13 {
14     /**
15      * @var Book
16      */
17     protected $book;
18
19     /**
20      * BookContents constructor.
21      */
22     public function __construct(Book $book)
23     {
24         $this->book = $book;
25     }
26
27     /**
28      * Get the current priority of the last item
29      * at the top-level of the book.
30      */
31     public function getLastPriority(): int
32     {
33         $maxPage = Page::visible()->where('book_id', '=', $this->book->id)
34             ->where('draft', '=', false)
35             ->where('chapter_id', '=', 0)->max('priority');
36         $maxChapter = Chapter::visible()->where('book_id', '=', $this->book->id)
37             ->max('priority');
38
39         return max($maxChapter, $maxPage, 1);
40     }
41
42     /**
43      * Get the contents as a sorted collection tree.
44      */
45     public function getTree(bool $showDrafts = false, bool $renderPages = false): Collection
46     {
47         $pages = $this->getPages($showDrafts, $renderPages);
48         $chapters = Chapter::visible()->where('book_id', '=', $this->book->id)->get();
49         $all = collect()->concat($pages)->concat($chapters);
50         $chapterMap = $chapters->keyBy('id');
51         $lonePages = collect();
52
53         $pages->groupBy('chapter_id')->each(function ($pages, $chapter_id) use ($chapterMap, &$lonePages) {
54             $chapter = $chapterMap->get($chapter_id);
55             if ($chapter) {
56                 $chapter->setAttribute('visible_pages', collect($pages)->sortBy($this->bookChildSortFunc()));
57             } else {
58                 $lonePages = $lonePages->concat($pages);
59             }
60         });
61
62         $chapters->whereNull('visible_pages')->each(function (Chapter $chapter) {
63             $chapter->setAttribute('visible_pages', collect([]));
64         });
65
66         $all->each(function (Entity $entity) use ($renderPages) {
67             $entity->setRelation('book', $this->book);
68
69             if ($renderPages && $entity instanceof Page) {
70                 $entity->html = (new PageContent($entity))->render();
71             }
72         });
73
74         return collect($chapters)->concat($lonePages)->sortBy($this->bookChildSortFunc());
75     }
76
77     /**
78      * Function for providing a sorting score for an entity in relation to the
79      * other items within the book.
80      */
81     protected function bookChildSortFunc(): callable
82     {
83         return function (Entity $entity) {
84             if (isset($entity['draft']) && $entity['draft']) {
85                 return -100;
86             }
87
88             return $entity['priority'] ?? 0;
89         };
90     }
91
92     /**
93      * Get the visible pages within this book.
94      */
95     protected function getPages(bool $showDrafts = false, bool $getPageContent = false): Collection
96     {
97         $query = Page::visible()
98             ->select($getPageContent ? Page::$contentAttributes : Page::$listAttributes)
99             ->where('book_id', '=', $this->book->id);
100
101         if (!$showDrafts) {
102             $query->where('draft', '=', false);
103         }
104
105         return $query->get();
106     }
107
108     /**
109      * Sort the books content using the given sort map.
110      * Returns a list of books that were involved in the operation.
111      *
112      * @returns Book[]
113      */
114     public function sortUsingMap(BookSortMap $sortMap): array
115     {
116         // Load models into map
117         $modelMap = $this->loadModelsFromSortMap($sortMap);
118
119         // Perform the sort
120         foreach ($sortMap->all() as $item) {
121             $this->applySortUpdates($item, $modelMap);
122         }
123
124         /** @var Book[] $booksInvolved */
125         $booksInvolved = array_values(array_filter($modelMap, function (string $key) {
126             return strpos($key, 'book:') === 0;
127         }, ARRAY_FILTER_USE_KEY));
128
129         // Update permissions of books involved
130         foreach ($booksInvolved as $book) {
131             $book->rebuildPermissions();
132         }
133
134         return $booksInvolved;
135     }
136
137     /**
138      * Using the given sort map item, detect changes for the related model
139      * and update it if required. Changes where permissions are lacking will
140      * be skipped and not throw an error.
141      *
142      * @param array<string, Entity> $modelMap
143      */
144     protected function applySortUpdates(BookSortMapItem $sortMapItem, array $modelMap): void
145     {
146         /** @var BookChild $model */
147         $model = $modelMap[$sortMapItem->type . ':' . $sortMapItem->id] ?? null;
148         if (!$model) {
149             return;
150         }
151
152         $priorityChanged = $model->priority !== $sortMapItem->sort;
153         $bookChanged = $model->book_id !== $sortMapItem->parentBookId;
154         $chapterChanged = ($model instanceof Page) && $model->chapter_id !== $sortMapItem->parentChapterId;
155
156         // Stop if there's no change
157         if (!$priorityChanged && !$bookChanged && !$chapterChanged) {
158             return;
159         }
160
161         $currentParentKey =  'book:' . $model->book_id;
162         if ($model instanceof Page && $model->chapter_id) {
163              $currentParentKey = 'chapter:' . $model->chapter_id;
164         }
165
166         $currentParent = $modelMap[$currentParentKey];
167         /** @var Book $newBook */
168         $newBook = $modelMap['book:' . $sortMapItem->parentBookId] ?? null;
169         /** @var ?Chapter $newChapter */
170         $newChapter = $sortMapItem->parentChapterId ? ($modelMap['chapter:' . $sortMapItem->parentChapterId] ?? null) : null;
171
172         // Check permissions of our changes to be made
173         if (!$currentParent || !$newBook) {
174             return;
175         } else if (!userCan('chapter-update', $currentParent) && !userCan('book-update', $currentParent)) {
176             return;
177         } else if ($bookChanged && !$newChapter && !userCan('book-update', $newBook)) {
178             return;
179         } else if ($newChapter && !userCan('chapter-update', $newChapter)) {
180             return;
181         } else if (($chapterChanged || $bookChanged) && $newChapter && $newBook->id !== $newChapter->book_id) {
182             return;
183         }
184
185         // Action the required changes
186         if ($bookChanged) {
187             $model->changeBook($sortMapItem->parentBookId);
188         }
189
190         if ($chapterChanged) {
191             $model->chapter_id = $sortMapItem->parentChapterId ?? 0;
192         }
193
194         if ($priorityChanged) {
195             $model->priority = $sortMapItem->sort;
196         }
197
198         if ($chapterChanged || $priorityChanged) {
199             $model->save();
200         }
201     }
202
203     /**
204      * Load models from the database into the given sort map.
205      * @return array<string, Entity>
206      */
207     protected function loadModelsFromSortMap(BookSortMap $sortMap): array
208     {
209         $modelMap = [];
210         $ids = [
211             'chapter' => [],
212             'page' => [],
213             'book' => [],
214         ];
215
216         foreach ($sortMap->all() as $sortMapItem) {
217             $ids[$sortMapItem->type][] = $sortMapItem->id;
218             $ids['book'][] = $sortMapItem->parentBookId;
219             if ($sortMapItem->parentChapterId) {
220                 $ids['chapter'][] = $sortMapItem->parentChapterId;
221             }
222         }
223
224         $pages = Page::visible()->whereIn('id', array_unique($ids['page']))->get(Page::$listAttributes);
225         /** @var Page $page */
226         foreach ($pages as $page) {
227             $modelMap['page:' . $page->id] = $page;
228             $ids['book'][] = $page->book_id;
229             if ($page->chapter_id) {
230                 $ids['chapter'][] = $page->chapter_id;
231             }
232         }
233
234         $chapters = Chapter::visible()->whereIn('id', array_unique($ids['chapter']))->get();
235         /** @var Chapter $chapter */
236         foreach ($chapters as $chapter) {
237             $modelMap['chapter:' . $chapter->id] = $chapter;
238             $ids['book'][] = $chapter->book_id;
239         }
240
241         $books = Book::visible()->whereIn('id', array_unique($ids['book']))->get();
242         /** @var Book $book */
243         foreach ($books as $book) {
244             $modelMap['book:' . $book->id] = $book;
245         }
246
247         return $modelMap;
248     }
249 }