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