2021-06-26 23:23:15 +08:00
|
|
|
<?php
|
|
|
|
|
|
|
|
namespace BookStack\Entities\Tools;
|
2019-10-05 19:55:01 +08:00
|
|
|
|
2020-11-22 08:17:45 +08:00
|
|
|
use BookStack\Entities\Models\Book;
|
|
|
|
use BookStack\Entities\Models\BookChild;
|
|
|
|
use BookStack\Entities\Models\Chapter;
|
|
|
|
use BookStack\Entities\Models\Entity;
|
|
|
|
use BookStack\Entities\Models\Page;
|
2019-10-05 19:55:01 +08:00
|
|
|
use Illuminate\Support\Collection;
|
|
|
|
|
|
|
|
class BookContents
|
|
|
|
{
|
|
|
|
/**
|
|
|
|
* @var Book
|
|
|
|
*/
|
|
|
|
protected $book;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* BookContents constructor.
|
|
|
|
*/
|
|
|
|
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');
|
2021-06-26 23:23:15 +08:00
|
|
|
|
2019-10-05 19:55:01 +08:00
|
|
|
return max($maxChapter, $maxPage, 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the contents as a sorted collection tree.
|
|
|
|
*/
|
|
|
|
public function getTree(bool $showDrafts = false, bool $renderPages = false): Collection
|
|
|
|
{
|
2021-08-22 20:15:58 +08:00
|
|
|
$pages = $this->getPages($showDrafts, $renderPages);
|
2019-10-05 19:55:01 +08:00
|
|
|
$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) {
|
2020-12-18 01:31:18 +08:00
|
|
|
$chapter->setAttribute('visible_pages', collect($pages)->sortBy($this->bookChildSortFunc()));
|
2019-10-05 19:55:01 +08:00
|
|
|
} else {
|
|
|
|
$lonePages = $lonePages->concat($pages);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2020-12-18 01:31:18 +08:00
|
|
|
$chapters->whereNull('visible_pages')->each(function (Chapter $chapter) {
|
|
|
|
$chapter->setAttribute('visible_pages', collect([]));
|
|
|
|
});
|
|
|
|
|
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
|
|
|
|
2021-11-20 22:03:56 +08:00
|
|
|
if ($renderPages && $entity instanceof 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;
|
|
|
|
}
|
2021-06-26 23:23:15 +08:00
|
|
|
|
2019-10-05 19:55:01 +08:00
|
|
|
return $entity['priority'] ?? 0;
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the visible pages within this book.
|
|
|
|
*/
|
2021-08-22 02:58:19 +08:00
|
|
|
protected function getPages(bool $showDrafts = false, bool $getPageContent = false): Collection
|
2019-10-05 19:55:01 +08:00
|
|
|
{
|
2021-08-22 02:58:19 +08:00
|
|
|
$query = Page::visible()
|
|
|
|
->select($getPageContent ? Page::$contentAttributes : Page::$listAttributes)
|
|
|
|
->where('book_id', '=', $this->book->id);
|
2019-10-05 19:55:01 +08:00
|
|
|
|
|
|
|
if (!$showDrafts) {
|
|
|
|
$query->where('draft', '=', false);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $query->get();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2022-01-05 01:31:57 +08:00
|
|
|
* Sort the books content using the given sort map.
|
2019-10-05 19:55:01 +08:00
|
|
|
* Returns a list of books that were involved in the operation.
|
2021-06-26 23:23:15 +08:00
|
|
|
*
|
2022-01-05 05:09:34 +08:00
|
|
|
* @returns Book[]
|
2019-10-05 19:55:01 +08:00
|
|
|
*/
|
2022-01-05 05:09:34 +08:00
|
|
|
public function sortUsingMap(BookSortMap $sortMap): array
|
2019-10-05 19:55:01 +08:00
|
|
|
{
|
|
|
|
// Load models into map
|
2022-01-05 05:09:34 +08:00
|
|
|
$modelMap = $this->loadModelsFromSortMap($sortMap);
|
2019-10-05 19:55:01 +08:00
|
|
|
|
|
|
|
// Perform the sort
|
2022-01-05 01:31:57 +08:00
|
|
|
foreach ($sortMap->all() as $item) {
|
2022-01-05 05:09:34 +08:00
|
|
|
$this->applySortUpdates($item, $modelMap);
|
2022-01-05 01:31:57 +08:00
|
|
|
}
|
2019-10-05 19:55:01 +08:00
|
|
|
|
2022-01-05 05:09:34 +08:00
|
|
|
/** @var Book[] $booksInvolved */
|
|
|
|
$booksInvolved = array_values(array_filter($modelMap, function (string $key) {
|
|
|
|
return strpos($key, 'book:') === 0;
|
|
|
|
}, ARRAY_FILTER_USE_KEY));
|
|
|
|
|
|
|
|
// Update permissions of books involved
|
|
|
|
foreach ($booksInvolved as $book) {
|
2019-10-05 19:55:01 +08:00
|
|
|
$book->rebuildPermissions();
|
2022-01-05 05:09:34 +08:00
|
|
|
}
|
2019-10-05 19:55:01 +08:00
|
|
|
|
|
|
|
return $booksInvolved;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Using the given sort map item, detect changes for the related model
|
2022-01-05 05:09:34 +08:00
|
|
|
* and update it if required. Changes where permissions are lacking will
|
|
|
|
* be skipped and not throw an error.
|
|
|
|
*
|
|
|
|
* @param array<string, Entity> $modelMap
|
2019-10-05 19:55:01 +08:00
|
|
|
*/
|
2022-01-05 05:09:34 +08:00
|
|
|
protected function applySortUpdates(BookSortMapItem $sortMapItem, array $modelMap): void
|
2019-10-05 19:55:01 +08:00
|
|
|
{
|
2022-01-05 05:09:34 +08:00
|
|
|
/** @var BookChild $model */
|
|
|
|
$model = $modelMap[$sortMapItem->type . ':' . $sortMapItem->id] ?? null;
|
2022-01-05 01:31:57 +08:00
|
|
|
if (!$model) {
|
|
|
|
return;
|
|
|
|
}
|
2019-10-05 19:55:01 +08:00
|
|
|
|
2022-01-05 01:31:57 +08:00
|
|
|
$priorityChanged = $model->priority !== $sortMapItem->sort;
|
|
|
|
$bookChanged = $model->book_id !== $sortMapItem->parentBookId;
|
|
|
|
$chapterChanged = ($model instanceof Page) && $model->chapter_id !== $sortMapItem->parentChapterId;
|
2019-10-05 19:55:01 +08:00
|
|
|
|
2022-01-05 05:09:34 +08:00
|
|
|
// Stop if there's no change
|
|
|
|
if (!$priorityChanged && !$bookChanged && !$chapterChanged) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
$currentParentKey = 'book:' . $model->book_id;
|
|
|
|
if ($model instanceof Page && $model->chapter_id) {
|
|
|
|
$currentParentKey = 'chapter:' . $model->chapter_id;
|
|
|
|
}
|
|
|
|
|
|
|
|
$currentParent = $modelMap[$currentParentKey];
|
|
|
|
/** @var Book $newBook */
|
|
|
|
$newBook = $modelMap['book:' . $sortMapItem->parentBookId] ?? null;
|
|
|
|
/** @var ?Chapter $newChapter */
|
|
|
|
$newChapter = $sortMapItem->parentChapterId ? ($modelMap['chapter:' . $sortMapItem->parentChapterId] ?? null) : null;
|
|
|
|
|
|
|
|
// Check permissions of our changes to be made
|
|
|
|
if (!$currentParent || !$newBook) {
|
|
|
|
return;
|
|
|
|
} else if (!userCan('chapter-update', $currentParent) && !userCan('book-update', $currentParent)) {
|
|
|
|
return;
|
|
|
|
} else if ($bookChanged && !$newChapter && !userCan('book-update', $newBook)) {
|
|
|
|
return;
|
|
|
|
} else if ($newChapter && !userCan('chapter-update', $newChapter)) {
|
|
|
|
return;
|
|
|
|
} else if (($chapterChanged || $bookChanged) && $newChapter && $newBook->id !== $newChapter->book_id) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Action the required changes
|
2019-10-05 19:55:01 +08:00
|
|
|
if ($bookChanged) {
|
2022-01-05 01:31:57 +08:00
|
|
|
$model->changeBook($sortMapItem->parentBookId);
|
2019-10-05 19:55:01 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
if ($chapterChanged) {
|
2022-01-05 05:09:34 +08:00
|
|
|
$model->chapter_id = $sortMapItem->parentChapterId ?? 0;
|
2019-10-05 19:55:01 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
if ($priorityChanged) {
|
2022-01-05 01:31:57 +08:00
|
|
|
$model->priority = $sortMapItem->sort;
|
2022-01-05 05:09:34 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
if ($chapterChanged || $priorityChanged) {
|
2019-10-05 19:55:01 +08:00
|
|
|
$model->save();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Load models from the database into the given sort map.
|
2022-01-05 05:09:34 +08:00
|
|
|
* @return array<string, Entity>
|
2019-10-05 19:55:01 +08:00
|
|
|
*/
|
2022-01-05 05:09:34 +08:00
|
|
|
protected function loadModelsFromSortMap(BookSortMap $sortMap): array
|
2019-10-05 19:55:01 +08:00
|
|
|
{
|
2022-01-05 05:09:34 +08:00
|
|
|
$modelMap = [];
|
|
|
|
$ids = [
|
|
|
|
'chapter' => [],
|
|
|
|
'page' => [],
|
|
|
|
'book' => [],
|
|
|
|
];
|
|
|
|
|
|
|
|
foreach ($sortMap->all() as $sortMapItem) {
|
|
|
|
$ids[$sortMapItem->type][] = $sortMapItem->id;
|
|
|
|
$ids['book'][] = $sortMapItem->parentBookId;
|
|
|
|
if ($sortMapItem->parentChapterId) {
|
|
|
|
$ids['chapter'][] = $sortMapItem->parentChapterId;
|
|
|
|
}
|
|
|
|
}
|
2019-10-05 19:55:01 +08:00
|
|
|
|
2022-01-05 05:09:34 +08:00
|
|
|
$pages = Page::visible()->whereIn('id', array_unique($ids['page']))->get(Page::$listAttributes);
|
|
|
|
/** @var Page $page */
|
2019-10-05 19:55:01 +08:00
|
|
|
foreach ($pages as $page) {
|
2022-01-05 05:09:34 +08:00
|
|
|
$modelMap['page:' . $page->id] = $page;
|
|
|
|
$ids['book'][] = $page->book_id;
|
|
|
|
if ($page->chapter_id) {
|
|
|
|
$ids['chapter'][] = $page->chapter_id;
|
|
|
|
}
|
2019-10-05 19:55:01 +08:00
|
|
|
}
|
|
|
|
|
2022-01-05 05:09:34 +08:00
|
|
|
$chapters = Chapter::visible()->whereIn('id', array_unique($ids['chapter']))->get();
|
|
|
|
/** @var Chapter $chapter */
|
2019-10-05 19:55:01 +08:00
|
|
|
foreach ($chapters as $chapter) {
|
2022-01-05 05:09:34 +08:00
|
|
|
$modelMap['chapter:' . $chapter->id] = $chapter;
|
|
|
|
$ids['book'][] = $chapter->book_id;
|
2019-10-05 19:55:01 +08:00
|
|
|
}
|
|
|
|
|
2022-01-05 05:09:34 +08:00
|
|
|
$books = Book::visible()->whereIn('id', array_unique($ids['book']))->get();
|
|
|
|
/** @var Book $book */
|
|
|
|
foreach ($books as $book) {
|
|
|
|
$modelMap['book:' . $book->id] = $book;
|
2019-10-05 19:55:01 +08:00
|
|
|
}
|
|
|
|
|
2022-01-05 05:09:34 +08:00
|
|
|
return $modelMap;
|
2019-10-05 19:55:01 +08:00
|
|
|
}
|
|
|
|
}
|