]> BookStack Code Mirror - bookstack/blob - app/Entities/Tools/BookContents.php
Apply fixes from StyleCI
[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);
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): Collection
97     {
98         $query = Page::visible()->where('book_id', '=', $this->book->id);
99
100         if (!$showDrafts) {
101             $query->where('draft', '=', false);
102         }
103
104         return $query->get();
105     }
106
107     /**
108      * Sort the books content using the given map.
109      * The map is a single-dimension collection of objects in the following format:
110      *   {
111      *     +"id": "294" (ID of item)
112      *     +"sort": 1 (Sort order index)
113      *     +"parentChapter": false (ID of parent chapter, as string, or false)
114      *     +"type": "page" (Entity type of item)
115      *     +"book": "1" (Id of book to place item in)
116      *   }.
117      *
118      * Returns a list of books that were involved in the operation.
119      *
120      * @throws SortOperationException
121      */
122     public function sortUsingMap(Collection $sortMap): Collection
123     {
124         // Load models into map
125         $this->loadModelsIntoSortMap($sortMap);
126         $booksInvolved = $this->getBooksInvolvedInSort($sortMap);
127
128         // Perform the sort
129         $sortMap->each(function ($mapItem) {
130             $this->applySortUpdates($mapItem);
131         });
132
133         // Update permissions and activity.
134         $booksInvolved->each(function (Book $book) {
135             $book->rebuildPermissions();
136         });
137
138         return $booksInvolved;
139     }
140
141     /**
142      * Using the given sort map item, detect changes for the related model
143      * and update it if required.
144      */
145     protected function applySortUpdates(\stdClass $sortMapItem)
146     {
147         /** @var BookChild $model */
148         $model = $sortMapItem->model;
149
150         $priorityChanged = intval($model->priority) !== intval($sortMapItem->sort);
151         $bookChanged = intval($model->book_id) !== intval($sortMapItem->book);
152         $chapterChanged = ($sortMapItem->type === 'page') && intval($model->chapter_id) !== $sortMapItem->parentChapter;
153
154         if ($bookChanged) {
155             $model->changeBook($sortMapItem->book);
156         }
157
158         if ($chapterChanged) {
159             $model->chapter_id = intval($sortMapItem->parentChapter);
160             $model->save();
161         }
162
163         if ($priorityChanged) {
164             $model->priority = intval($sortMapItem->sort);
165             $model->save();
166         }
167     }
168
169     /**
170      * Load models from the database into the given sort map.
171      */
172     protected function loadModelsIntoSortMap(Collection $sortMap): void
173     {
174         $keyMap = $sortMap->keyBy(function (\stdClass $sortMapItem) {
175             return  $sortMapItem->type . ':' . $sortMapItem->id;
176         });
177         $pageIds = $sortMap->where('type', '=', 'page')->pluck('id');
178         $chapterIds = $sortMap->where('type', '=', 'chapter')->pluck('id');
179
180         $pages = Page::visible()->whereIn('id', $pageIds)->get();
181         $chapters = Chapter::visible()->whereIn('id', $chapterIds)->get();
182
183         foreach ($pages as $page) {
184             $sortItem = $keyMap->get('page:' . $page->id);
185             $sortItem->model = $page;
186         }
187
188         foreach ($chapters as $chapter) {
189             $sortItem = $keyMap->get('chapter:' . $chapter->id);
190             $sortItem->model = $chapter;
191         }
192     }
193
194     /**
195      * Get the books involved in a sort.
196      * The given sort map should have its models loaded first.
197      *
198      * @throws SortOperationException
199      */
200     protected function getBooksInvolvedInSort(Collection $sortMap): Collection
201     {
202         $bookIdsInvolved = collect([$this->book->id]);
203         $bookIdsInvolved = $bookIdsInvolved->concat($sortMap->pluck('book'));
204         $bookIdsInvolved = $bookIdsInvolved->concat($sortMap->pluck('model.book_id'));
205         $bookIdsInvolved = $bookIdsInvolved->unique()->toArray();
206
207         $books = Book::hasPermission('update')->whereIn('id', $bookIdsInvolved)->get();
208
209         if (count($books) !== count($bookIdsInvolved)) {
210             throw new SortOperationException('Could not find all books requested in sort operation');
211         }
212
213         return $books;
214     }
215 }