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