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