3 namespace BookStack\Entities\Tools;
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;
20 * BookContents constructor.
22 public function __construct(Book $book)
28 * Get the current priority of the last item
29 * at the top-level of the book.
31 public function getLastPriority(): int
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)
39 return max($maxChapter, $maxPage, 1);
43 * Get the contents as a sorted collection tree.
45 public function getTree(bool $showDrafts = false, bool $renderPages = false): Collection
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();
53 $pages->groupBy('chapter_id')->each(function ($pages, $chapter_id) use ($chapterMap, &$lonePages) {
54 $chapter = $chapterMap->get($chapter_id);
56 $chapter->setAttribute('visible_pages', collect($pages)->sortBy($this->bookChildSortFunc()));
58 $lonePages = $lonePages->concat($pages);
62 $chapters->whereNull('visible_pages')->each(function (Chapter $chapter) {
63 $chapter->setAttribute('visible_pages', collect([]));
66 $all->each(function (Entity $entity) use ($renderPages) {
67 $entity->setRelation('book', $this->book);
69 if ($renderPages && $entity instanceof Page) {
70 $entity->html = (new PageContent($entity))->render();
74 return collect($chapters)->concat($lonePages)->sortBy($this->bookChildSortFunc());
78 * Function for providing a sorting score for an entity in relation to the
79 * other items within the book.
81 protected function bookChildSortFunc(): callable
83 return function (Entity $entity) {
84 if (isset($entity['draft']) && $entity['draft']) {
88 return $entity['priority'] ?? 0;
93 * Get the visible pages within this book.
95 protected function getPages(bool $showDrafts = false, bool $getPageContent = false): Collection
97 $query = Page::visible()
98 ->select($getPageContent ? Page::$contentAttributes : Page::$listAttributes)
99 ->where('book_id', '=', $this->book->id);
102 $query->where('draft', '=', false);
105 return $query->get();
109 * Sort the books content using the given sort map.
110 * Returns a list of books that were involved in the operation.
114 public function sortUsingMap(BookSortMap $sortMap): array
116 // Load models into map
117 $modelMap = $this->loadModelsFromSortMap($sortMap);
120 foreach ($sortMap->all() as $item) {
121 $this->applySortUpdates($item, $modelMap);
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));
129 // Update permissions of books involved
130 foreach ($booksInvolved as $book) {
131 $book->rebuildPermissions();
134 return $booksInvolved;
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.
142 * @param array<string, Entity> $modelMap
144 protected function applySortUpdates(BookSortMapItem $sortMapItem, array $modelMap): void
146 /** @var BookChild $model */
147 $model = $modelMap[$sortMapItem->type . ':' . $sortMapItem->id] ?? null;
152 $priorityChanged = $model->priority !== $sortMapItem->sort;
153 $bookChanged = $model->book_id !== $sortMapItem->parentBookId;
154 $chapterChanged = ($model instanceof Page) && $model->chapter_id !== $sortMapItem->parentChapterId;
156 // Stop if there's no change
157 if (!$priorityChanged && !$bookChanged && !$chapterChanged) {
161 $currentParentKey = 'book:' . $model->book_id;
162 if ($model instanceof Page && $model->chapter_id) {
163 $currentParentKey = 'chapter:' . $model->chapter_id;
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;
172 // Check permissions of our changes to be made
173 if (!$currentParent || !$newBook) {
175 } else if (!userCan('chapter-update', $currentParent) && !userCan('book-update', $currentParent)) {
177 } else if ($bookChanged && !$newChapter && !userCan('book-update', $newBook)) {
179 } else if ($newChapter && !userCan('chapter-update', $newChapter)) {
181 } else if (($chapterChanged || $bookChanged) && $newChapter && $newBook->id !== $newChapter->book_id) {
185 // Action the required changes
187 $model->changeBook($sortMapItem->parentBookId);
190 if ($chapterChanged) {
191 $model->chapter_id = $sortMapItem->parentChapterId ?? 0;
194 if ($priorityChanged) {
195 $model->priority = $sortMapItem->sort;
198 if ($chapterChanged || $priorityChanged) {
204 * Load models from the database into the given sort map.
205 * @return array<string, Entity>
207 protected function loadModelsFromSortMap(BookSortMap $sortMap): array
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;
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;
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;
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;