]> BookStack Code Mirror - bookstack/blob - app/Entities/Tools/BookContents.php
Minor capitalisation fix for Estonian
[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 BookStack\Exceptions\SortOperationException;
11 use Illuminate\Support\Collection;
12
13 class BookContents
14 {
15     /**
16      * @var Book
17      */
18     protected $book;
19
20     /**
21      * BookContents constructor.
22      */
23     public function __construct(Book $book)
24     {
25         $this->book = $book;
26     }
27
28     /**
29      * Get the current priority of the last item
30      * at the top-level of the book.
31      */
32     public function getLastPriority(): int
33     {
34         $maxPage = Page::visible()->where('book_id', '=', $this->book->id)
35             ->where('draft', '=', false)
36             ->where('chapter_id', '=', 0)->max('priority');
37         $maxChapter = Chapter::visible()->where('book_id', '=', $this->book->id)
38             ->max('priority');
39
40         return max($maxChapter, $maxPage, 1);
41     }
42
43     /**
44      * Get the contents as a sorted collection tree.
45      */
46     public function getTree(bool $showDrafts = false, bool $renderPages = false): Collection
47     {
48         $pages = $this->getPages($showDrafts, $renderPages);
49         $chapters = Chapter::visible()->where('book_id', '=', $this->book->id)->get();
50         $all = collect()->concat($pages)->concat($chapters);
51         $chapterMap = $chapters->keyBy('id');
52         $lonePages = collect();
53
54         $pages->groupBy('chapter_id')->each(function ($pages, $chapter_id) use ($chapterMap, &$lonePages) {
55             $chapter = $chapterMap->get($chapter_id);
56             if ($chapter) {
57                 $chapter->setAttribute('visible_pages', collect($pages)->sortBy($this->bookChildSortFunc()));
58             } else {
59                 $lonePages = $lonePages->concat($pages);
60             }
61         });
62
63         $chapters->whereNull('visible_pages')->each(function (Chapter $chapter) {
64             $chapter->setAttribute('visible_pages', collect([]));
65         });
66
67         $all->each(function (Entity $entity) use ($renderPages) {
68             $entity->setRelation('book', $this->book);
69
70             if ($renderPages && $entity->isA('page')) {
71                 $entity->html = (new PageContent($entity))->render();
72             }
73         });
74
75         return collect($chapters)->concat($lonePages)->sortBy($this->bookChildSortFunc());
76     }
77
78     /**
79      * Function for providing a sorting score for an entity in relation to the
80      * other items within the book.
81      */
82     protected function bookChildSortFunc(): callable
83     {
84         return function (Entity $entity) {
85             if (isset($entity['draft']) && $entity['draft']) {
86                 return -100;
87             }
88
89             return $entity['priority'] ?? 0;
90         };
91     }
92
93     /**
94      * Get the visible pages within this book.
95      */
96     protected function getPages(bool $showDrafts = false, bool $getPageContent = false): Collection
97     {
98         $query = Page::visible()
99             ->select($getPageContent ? Page::$contentAttributes : Page::$listAttributes)
100             ->where('book_id', '=', $this->book->id);
101
102         if (!$showDrafts) {
103             $query->where('draft', '=', false);
104         }
105
106         return $query->get();
107     }
108
109     /**
110      * Sort the books content using the given map.
111      * The map is a single-dimension collection of objects in the following format:
112      *   {
113      *     +"id": "294" (ID of item)
114      *     +"sort": 1 (Sort order index)
115      *     +"parentChapter": false (ID of parent chapter, as string, or false)
116      *     +"type": "page" (Entity type of item)
117      *     +"book": "1" (Id of book to place item in)
118      *   }.
119      *
120      * Returns a list of books that were involved in the operation.
121      *
122      * @throws SortOperationException
123      */
124     public function sortUsingMap(Collection $sortMap): Collection
125     {
126         // Load models into map
127         $this->loadModelsIntoSortMap($sortMap);
128         $booksInvolved = $this->getBooksInvolvedInSort($sortMap);
129
130         // Perform the sort
131         $sortMap->each(function ($mapItem) {
132             $this->applySortUpdates($mapItem);
133         });
134
135         // Update permissions and activity.
136         $booksInvolved->each(function (Book $book) {
137             $book->rebuildPermissions();
138         });
139
140         return $booksInvolved;
141     }
142
143     /**
144      * Using the given sort map item, detect changes for the related model
145      * and update it if required.
146      */
147     protected function applySortUpdates(\stdClass $sortMapItem)
148     {
149         /** @var BookChild $model */
150         $model = $sortMapItem->model;
151
152         $priorityChanged = intval($model->priority) !== intval($sortMapItem->sort);
153         $bookChanged = intval($model->book_id) !== intval($sortMapItem->book);
154         $chapterChanged = ($sortMapItem->type === 'page') && intval($model->chapter_id) !== $sortMapItem->parentChapter;
155
156         if ($bookChanged) {
157             $model->changeBook($sortMapItem->book);
158         }
159
160         if ($chapterChanged) {
161             $model->chapter_id = intval($sortMapItem->parentChapter);
162             $model->save();
163         }
164
165         if ($priorityChanged) {
166             $model->priority = intval($sortMapItem->sort);
167             $model->save();
168         }
169     }
170
171     /**
172      * Load models from the database into the given sort map.
173      */
174     protected function loadModelsIntoSortMap(Collection $sortMap): void
175     {
176         $keyMap = $sortMap->keyBy(function (\stdClass $sortMapItem) {
177             return  $sortMapItem->type . ':' . $sortMapItem->id;
178         });
179         $pageIds = $sortMap->where('type', '=', 'page')->pluck('id');
180         $chapterIds = $sortMap->where('type', '=', 'chapter')->pluck('id');
181
182         $pages = Page::visible()->whereIn('id', $pageIds)->get();
183         $chapters = Chapter::visible()->whereIn('id', $chapterIds)->get();
184
185         foreach ($pages as $page) {
186             $sortItem = $keyMap->get('page:' . $page->id);
187             $sortItem->model = $page;
188         }
189
190         foreach ($chapters as $chapter) {
191             $sortItem = $keyMap->get('chapter:' . $chapter->id);
192             $sortItem->model = $chapter;
193         }
194     }
195
196     /**
197      * Get the books involved in a sort.
198      * The given sort map should have its models loaded first.
199      *
200      * @throws SortOperationException
201      */
202     protected function getBooksInvolvedInSort(Collection $sortMap): Collection
203     {
204         $bookIdsInvolved = collect([$this->book->id]);
205         $bookIdsInvolved = $bookIdsInvolved->concat($sortMap->pluck('book'));
206         $bookIdsInvolved = $bookIdsInvolved->concat($sortMap->pluck('model.book_id'));
207         $bookIdsInvolved = $bookIdsInvolved->unique()->toArray();
208
209         $books = Book::hasPermission('update')->whereIn('id', $bookIdsInvolved)->get();
210
211         if (count($books) !== count($bookIdsInvolved)) {
212             throw new SortOperationException('Could not find all books requested in sort operation');
213         }
214
215         return $books;
216     }
217 }