| 
									
										
										
										
											2019-10-05 19:55:01 +08:00
										 |  |  | <?php namespace BookStack\Entities\Managers; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | use BookStack\Entities\Book; | 
					
						
							|  |  |  | use BookStack\Entities\BookChild; | 
					
						
							|  |  |  | use BookStack\Entities\Chapter; | 
					
						
							|  |  |  | use BookStack\Entities\Entity; | 
					
						
							|  |  |  | use BookStack\Entities\Page; | 
					
						
							|  |  |  | use BookStack\Exceptions\SortOperationException; | 
					
						
							|  |  |  | use Illuminate\Support\Collection; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class BookContents | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     /** | 
					
						
							|  |  |  |      * @var Book | 
					
						
							|  |  |  |      */ | 
					
						
							|  |  |  |     protected $book; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     /** | 
					
						
							|  |  |  |      * BookContents constructor. | 
					
						
							|  |  |  |      * @param $book | 
					
						
							|  |  |  |      */ | 
					
						
							|  |  |  |     public function __construct(Book $book) | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         $this->book = $book; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     /** | 
					
						
							|  |  |  |      * Get the current priority of the last item | 
					
						
							|  |  |  |      * at the top-level of the book. | 
					
						
							|  |  |  |      */ | 
					
						
							|  |  |  |     public function getLastPriority(): int | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         $maxPage = Page::visible()->where('book_id', '=', $this->book->id) | 
					
						
							|  |  |  |             ->where('draft', '=', false) | 
					
						
							|  |  |  |             ->where('chapter_id', '=', 0)->max('priority'); | 
					
						
							|  |  |  |         $maxChapter = Chapter::visible()->where('book_id', '=', $this->book->id) | 
					
						
							|  |  |  |             ->max('priority'); | 
					
						
							|  |  |  |         return max($maxChapter, $maxPage, 1); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     /** | 
					
						
							|  |  |  |      * Get the contents as a sorted collection tree. | 
					
						
							|  |  |  |      */ | 
					
						
							|  |  |  |     public function getTree(bool $showDrafts = false, bool $renderPages = false): Collection | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         $pages = $this->getPages($showDrafts); | 
					
						
							|  |  |  |         $chapters = Chapter::visible()->where('book_id', '=', $this->book->id)->get(); | 
					
						
							|  |  |  |         $all = collect()->concat($pages)->concat($chapters); | 
					
						
							|  |  |  |         $chapterMap = $chapters->keyBy('id'); | 
					
						
							|  |  |  |         $lonePages = collect(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         $pages->groupBy('chapter_id')->each(function ($pages, $chapter_id) use ($chapterMap, &$lonePages) { | 
					
						
							|  |  |  |             $chapter = $chapterMap->get($chapter_id); | 
					
						
							|  |  |  |             if ($chapter) { | 
					
						
							|  |  |  |                 $chapter->setAttribute('pages', collect($pages)->sortBy($this->bookChildSortFunc())); | 
					
						
							|  |  |  |             } else { | 
					
						
							|  |  |  |                 $lonePages = $lonePages->concat($pages); | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |         }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-15 05:13:52 +08:00
										 |  |  |         $all->each(function (Entity $entity) use ($renderPages) { | 
					
						
							| 
									
										
										
										
											2019-10-05 19:55:01 +08:00
										 |  |  |             $entity->setRelation('book', $this->book); | 
					
						
							| 
									
										
										
										
											2020-08-15 05:13:52 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-09-27 00:00:17 +08:00
										 |  |  |             if ($renderPages && $entity->isA('page')) { | 
					
						
							| 
									
										
										
										
											2020-08-15 05:13:52 +08:00
										 |  |  |                 $entity->html = (new PageContent($entity))->render(); | 
					
						
							|  |  |  |             } | 
					
						
							| 
									
										
										
										
											2019-10-05 19:55:01 +08:00
										 |  |  |         }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return collect($chapters)->concat($lonePages)->sortBy($this->bookChildSortFunc()); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     /** | 
					
						
							|  |  |  |      * Function for providing a sorting score for an entity in relation to the | 
					
						
							|  |  |  |      * other items within the book. | 
					
						
							|  |  |  |      */ | 
					
						
							|  |  |  |     protected function bookChildSortFunc(): callable | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         return function (Entity $entity) { | 
					
						
							|  |  |  |             if (isset($entity['draft']) && $entity['draft']) { | 
					
						
							|  |  |  |                 return -100; | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |             return $entity['priority'] ?? 0; | 
					
						
							|  |  |  |         }; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     /** | 
					
						
							|  |  |  |      * Get the visible pages within this book. | 
					
						
							|  |  |  |      */ | 
					
						
							|  |  |  |     protected function getPages(bool $showDrafts = false): Collection | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         $query = Page::visible()->where('book_id', '=', $this->book->id); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if (!$showDrafts) { | 
					
						
							|  |  |  |             $query->where('draft', '=', false); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return $query->get(); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     /** | 
					
						
							|  |  |  |      * Sort the books content using the given map. | 
					
						
							|  |  |  |      * The map is a single-dimension collection of objects in the following format: | 
					
						
							|  |  |  |      *   { | 
					
						
							|  |  |  |      *     +"id": "294" (ID of item) | 
					
						
							|  |  |  |      *     +"sort": 1 (Sort order index) | 
					
						
							|  |  |  |      *     +"parentChapter": false (ID of parent chapter, as string, or false) | 
					
						
							|  |  |  |      *     +"type": "page" (Entity type of item) | 
					
						
							|  |  |  |      *     +"book": "1" (Id of book to place item in) | 
					
						
							|  |  |  |      *   } | 
					
						
							|  |  |  |      * | 
					
						
							|  |  |  |      * Returns a list of books that were involved in the operation. | 
					
						
							|  |  |  |      * @throws SortOperationException | 
					
						
							|  |  |  |      */ | 
					
						
							|  |  |  |     public function sortUsingMap(Collection $sortMap): Collection | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         // Load models into map
 | 
					
						
							|  |  |  |         $this->loadModelsIntoSortMap($sortMap); | 
					
						
							|  |  |  |         $booksInvolved = $this->getBooksInvolvedInSort($sortMap); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         // Perform the sort
 | 
					
						
							|  |  |  |         $sortMap->each(function ($mapItem) { | 
					
						
							|  |  |  |             $this->applySortUpdates($mapItem); | 
					
						
							|  |  |  |         }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         // Update permissions and activity.
 | 
					
						
							|  |  |  |         $booksInvolved->each(function (Book $book) { | 
					
						
							|  |  |  |             $book->rebuildPermissions(); | 
					
						
							|  |  |  |         }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return $booksInvolved; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     /** | 
					
						
							|  |  |  |      * Using the given sort map item, detect changes for the related model | 
					
						
							|  |  |  |      * and update it if required. | 
					
						
							|  |  |  |      */ | 
					
						
							|  |  |  |     protected function applySortUpdates(\stdClass $sortMapItem) | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         /** @var BookChild $model */ | 
					
						
							|  |  |  |         $model = $sortMapItem->model; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         $priorityChanged = intval($model->priority) !== intval($sortMapItem->sort); | 
					
						
							|  |  |  |         $bookChanged = intval($model->book_id) !== intval($sortMapItem->book); | 
					
						
							|  |  |  |         $chapterChanged = ($sortMapItem->type === 'page') && intval($model->chapter_id) !== $sortMapItem->parentChapter; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if ($bookChanged) { | 
					
						
							|  |  |  |             $model->changeBook($sortMapItem->book); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if ($chapterChanged) { | 
					
						
							|  |  |  |             $model->chapter_id = intval($sortMapItem->parentChapter); | 
					
						
							|  |  |  |             $model->save(); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if ($priorityChanged) { | 
					
						
							|  |  |  |             $model->priority = intval($sortMapItem->sort); | 
					
						
							|  |  |  |             $model->save(); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     /** | 
					
						
							|  |  |  |      * Load models from the database into the given sort map. | 
					
						
							|  |  |  |      */ | 
					
						
							|  |  |  |     protected function loadModelsIntoSortMap(Collection $sortMap): void | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         $keyMap = $sortMap->keyBy(function (\stdClass $sortMapItem) { | 
					
						
							|  |  |  |             return  $sortMapItem->type . ':' . $sortMapItem->id; | 
					
						
							|  |  |  |         }); | 
					
						
							|  |  |  |         $pageIds = $sortMap->where('type', '=', 'page')->pluck('id'); | 
					
						
							|  |  |  |         $chapterIds = $sortMap->where('type', '=', 'chapter')->pluck('id'); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         $pages = Page::visible()->whereIn('id', $pageIds)->get(); | 
					
						
							|  |  |  |         $chapters = Chapter::visible()->whereIn('id', $chapterIds)->get(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         foreach ($pages as $page) { | 
					
						
							|  |  |  |             $sortItem = $keyMap->get('page:' . $page->id); | 
					
						
							|  |  |  |             $sortItem->model = $page; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         foreach ($chapters as $chapter) { | 
					
						
							|  |  |  |             $sortItem = $keyMap->get('chapter:' . $chapter->id); | 
					
						
							|  |  |  |             $sortItem->model = $chapter; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     /** | 
					
						
							|  |  |  |      * Get the books involved in a sort. | 
					
						
							|  |  |  |      * The given sort map should have its models loaded first. | 
					
						
							|  |  |  |      * @throws SortOperationException | 
					
						
							|  |  |  |      */ | 
					
						
							|  |  |  |     protected function getBooksInvolvedInSort(Collection $sortMap): Collection | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         $bookIdsInvolved = collect([$this->book->id]); | 
					
						
							|  |  |  |         $bookIdsInvolved = $bookIdsInvolved->concat($sortMap->pluck('book')); | 
					
						
							|  |  |  |         $bookIdsInvolved = $bookIdsInvolved->concat($sortMap->pluck('model.book_id')); | 
					
						
							|  |  |  |         $bookIdsInvolved = $bookIdsInvolved->unique()->toArray(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         $books = Book::hasPermission('update')->whereIn('id', $bookIdsInvolved)->get(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if (count($books) !== count($bookIdsInvolved)) { | 
					
						
							|  |  |  |             throw new SortOperationException("Could not find all books requested in sort operation"); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return $books; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | } |