1 <?php namespace BookStack\Entities\Tools;
3 use BookStack\Entities\Models\Book;
4 use BookStack\Entities\Models\BookChild;
5 use BookStack\Entities\Models\Chapter;
6 use BookStack\Entities\Models\Entity;
7 use BookStack\Entities\Models\Page;
8 use BookStack\Exceptions\SortOperationException;
9 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)
38 return max($maxChapter, $maxPage, 1);
42 * Get the contents as a sorted collection tree.
44 public function getTree(bool $showDrafts = false, bool $renderPages = false): Collection
46 $pages = $this->getPages($showDrafts);
47 $chapters = Chapter::visible()->where('book_id', '=', $this->book->id)->get();
48 $all = collect()->concat($pages)->concat($chapters);
49 $chapterMap = $chapters->keyBy('id');
50 $lonePages = collect();
52 $pages->groupBy('chapter_id')->each(function ($pages, $chapter_id) use ($chapterMap, &$lonePages) {
53 $chapter = $chapterMap->get($chapter_id);
55 $chapter->setAttribute('visible_pages', collect($pages)->sortBy($this->bookChildSortFunc()));
57 $lonePages = $lonePages->concat($pages);
61 $chapters->whereNull('visible_pages')->each(function (Chapter $chapter) {
62 $chapter->setAttribute('visible_pages', collect([]));
65 $all->each(function (Entity $entity) use ($renderPages) {
66 $entity->setRelation('book', $this->book);
68 if ($renderPages && $entity->isA('page')) {
69 $entity->html = (new PageContent($entity))->render();
73 return collect($chapters)->concat($lonePages)->sortBy($this->bookChildSortFunc());
77 * Function for providing a sorting score for an entity in relation to the
78 * other items within the book.
80 protected function bookChildSortFunc(): callable
82 return function (Entity $entity) {
83 if (isset($entity['draft']) && $entity['draft']) {
86 return $entity['priority'] ?? 0;
91 * Get the visible pages within this book.
93 protected function getPages(bool $showDrafts = false): Collection
95 $query = Page::visible()->where('book_id', '=', $this->book->id);
98 $query->where('draft', '=', false);
101 return $query->get();
105 * Sort the books content using the given map.
106 * The map is a single-dimension collection of objects in the following format:
108 * +"id": "294" (ID of item)
109 * +"sort": 1 (Sort order index)
110 * +"parentChapter": false (ID of parent chapter, as string, or false)
111 * +"type": "page" (Entity type of item)
112 * +"book": "1" (Id of book to place item in)
115 * Returns a list of books that were involved in the operation.
116 * @throws SortOperationException
118 public function sortUsingMap(Collection $sortMap): Collection
120 // Load models into map
121 $this->loadModelsIntoSortMap($sortMap);
122 $booksInvolved = $this->getBooksInvolvedInSort($sortMap);
125 $sortMap->each(function ($mapItem) {
126 $this->applySortUpdates($mapItem);
129 // Update permissions and activity.
130 $booksInvolved->each(function (Book $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.
141 protected function applySortUpdates(\stdClass $sortMapItem)
143 /** @var BookChild $model */
144 $model = $sortMapItem->model;
146 $priorityChanged = intval($model->priority) !== intval($sortMapItem->sort);
147 $bookChanged = intval($model->book_id) !== intval($sortMapItem->book);
148 $chapterChanged = ($sortMapItem->type === 'page') && intval($model->chapter_id) !== $sortMapItem->parentChapter;
151 $model->changeBook($sortMapItem->book);
154 if ($chapterChanged) {
155 $model->chapter_id = intval($sortMapItem->parentChapter);
159 if ($priorityChanged) {
160 $model->priority = intval($sortMapItem->sort);
166 * Load models from the database into the given sort map.
168 protected function loadModelsIntoSortMap(Collection $sortMap): void
170 $keyMap = $sortMap->keyBy(function (\stdClass $sortMapItem) {
171 return $sortMapItem->type . ':' . $sortMapItem->id;
173 $pageIds = $sortMap->where('type', '=', 'page')->pluck('id');
174 $chapterIds = $sortMap->where('type', '=', 'chapter')->pluck('id');
176 $pages = Page::visible()->whereIn('id', $pageIds)->get();
177 $chapters = Chapter::visible()->whereIn('id', $chapterIds)->get();
179 foreach ($pages as $page) {
180 $sortItem = $keyMap->get('page:' . $page->id);
181 $sortItem->model = $page;
184 foreach ($chapters as $chapter) {
185 $sortItem = $keyMap->get('chapter:' . $chapter->id);
186 $sortItem->model = $chapter;
191 * Get the books involved in a sort.
192 * The given sort map should have its models loaded first.
193 * @throws SortOperationException
195 protected function getBooksInvolvedInSort(Collection $sortMap): Collection
197 $bookIdsInvolved = collect([$this->book->id]);
198 $bookIdsInvolved = $bookIdsInvolved->concat($sortMap->pluck('book'));
199 $bookIdsInvolved = $bookIdsInvolved->concat($sortMap->pluck('model.book_id'));
200 $bookIdsInvolved = $bookIdsInvolved->unique()->toArray();
202 $books = Book::hasPermission('update')->whereIn('id', $bookIdsInvolved)->get();
204 if (count($books) !== count($bookIdsInvolved)) {
205 throw new SortOperationException("Could not find all books requested in sort operation");