X-Git-Url: http://source.bookstackapp.com/bookstack/blobdiff_plain/d8c45f574605ef27d662cfa850d06b61e81aedb6..refs/pull/5405/head:/app/Entities/Tools/BookContents.php diff --git a/app/Entities/Tools/BookContents.php b/app/Entities/Tools/BookContents.php index ff018eda9..7fa2134b7 100644 --- a/app/Entities/Tools/BookContents.php +++ b/app/Entities/Tools/BookContents.php @@ -7,33 +7,30 @@ use BookStack\Entities\Models\BookChild; use BookStack\Entities\Models\Chapter; use BookStack\Entities\Models\Entity; use BookStack\Entities\Models\Page; +use BookStack\Entities\Queries\EntityQueries; use Illuminate\Support\Collection; class BookContents { - /** - * @var Book - */ - protected $book; + protected EntityQueries $queries; - /** - * BookContents constructor. - */ - public function __construct(Book $book) - { - $this->book = $book; + public function __construct( + protected Book $book, + ) { + $this->queries = app()->make(EntityQueries::class); } /** - * Get the current priority of the last item - * at the top-level of the 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) + $maxPage = $this->book->pages() ->where('draft', '=', false) - ->where('chapter_id', '=', 0)->max('priority'); - $maxChapter = Chapter::visible()->where('book_id', '=', $this->book->id) + ->where('chapter_id', '=', 0) + ->max('priority'); + + $maxChapter = $this->book->chapters() ->max('priority'); return max($maxChapter, $maxPage, 1); @@ -45,7 +42,7 @@ class BookContents public function getTree(bool $showDrafts = false, bool $renderPages = false): Collection { $pages = $this->getPages($showDrafts, $renderPages); - $chapters = Chapter::visible()->where('book_id', '=', $this->book->id)->get(); + $chapters = $this->book->chapters()->scopes('visible')->get(); $all = collect()->concat($pages)->concat($chapters); $chapterMap = $chapters->keyBy('id'); $lonePages = collect(); @@ -94,15 +91,17 @@ class BookContents */ protected function getPages(bool $showDrafts = false, bool $getPageContent = false): Collection { - $query = Page::visible() - ->select($getPageContent ? Page::$contentAttributes : Page::$listAttributes) - ->where('book_id', '=', $this->book->id); + if ($getPageContent) { + $query = $this->queries->pages->visibleWithContents(); + } else { + $query = $this->queries->pages->visibleForList(); + } if (!$showDrafts) { $query->where('draft', '=', false); } - return $query->get(); + return $query->where('book_id', '=', $this->book->id)->get(); } /** @@ -116,14 +115,24 @@ class BookContents // Load models into map $modelMap = $this->loadModelsFromSortMap($sortMap); + // Sort our changes from our map to be chapters first + // Since they need to be process to ensure book alignment for child page changes. + $sortMapItems = $sortMap->all(); + usort($sortMapItems, function (BookSortMapItem $itemA, BookSortMapItem $itemB) { + $aScore = $itemA->type === 'page' ? 2 : 1; + $bScore = $itemB->type === 'page' ? 2 : 1; + + return $aScore - $bScore; + }); + // Perform the sort - foreach ($sortMap->all() as $item) { + foreach ($sortMapItems as $item) { $this->applySortUpdates($item, $modelMap); } /** @var Book[] $booksInvolved */ $booksInvolved = array_values(array_filter($modelMap, function (string $key) { - return strpos($key, 'book:') === 0; + return str_starts_with($key, 'book:'); }, ARRAY_FILTER_USE_KEY)); // Update permissions of books involved @@ -158,37 +167,28 @@ class BookContents return; } - $currentParentKey = 'book:' . $model->book_id; + $currentParentKey = 'book:' . $model->book_id; if ($model instanceof Page && $model->chapter_id) { - $currentParentKey = 'chapter:' . $model->chapter_id; + $currentParentKey = 'chapter:' . $model->chapter_id; } - $currentParent = $modelMap[$currentParentKey]; + $currentParent = $modelMap[$currentParentKey] ?? null; /** @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) { + if (!$this->isSortChangePermissible($sortMapItem, $model, $currentParent, $newBook, $newChapter)) { return; } // Action the required changes if ($bookChanged) { - $model->changeBook($sortMapItem->parentBookId); + $model->changeBook($newBook->id); } - if ($chapterChanged) { - $model->chapter_id = $sortMapItem->parentChapterId ?? 0; + if ($model instanceof Page && $chapterChanged) { + $model->chapter_id = $newChapter->id ?? 0; } if ($priorityChanged) { @@ -200,8 +200,72 @@ class BookContents } } + /** + * Check if the current user has permissions to apply the given sorting change. + * Is quite complex since items can gain a different parent change. Acts as a: + * - Update of old parent element (Change of content/order). + * - Update of sorted/moved element. + * - Deletion of element (Relative to parent upon move). + * - Creation of element within parent (Upon move to new parent). + */ + protected function isSortChangePermissible(BookSortMapItem $sortMapItem, BookChild $model, ?Entity $currentParent, ?Entity $newBook, ?Entity $newChapter): bool + { + // Stop if we can't see the current parent or new book. + if (!$currentParent || !$newBook) { + return false; + } + + $hasNewParent = $newBook->id !== $model->book_id || ($model instanceof Page && $model->chapter_id !== ($sortMapItem->parentChapterId ?? 0)); + if ($model instanceof Chapter) { + $hasPermission = userCan('book-update', $currentParent) + && userCan('book-update', $newBook) + && userCan('chapter-update', $model) + && (!$hasNewParent || userCan('chapter-create', $newBook)) + && (!$hasNewParent || userCan('chapter-delete', $model)); + + if (!$hasPermission) { + return false; + } + } + + if ($model instanceof Page) { + $parentPermission = ($currentParent instanceof Chapter) ? 'chapter-update' : 'book-update'; + $hasCurrentParentPermission = userCan($parentPermission, $currentParent); + + // This needs to check if there was an intended chapter location in the original sort map + // rather than inferring from the $newChapter since that variable may be null + // due to other reasons (Visibility). + $newParent = $sortMapItem->parentChapterId ? $newChapter : $newBook; + if (!$newParent) { + return false; + } + + $hasPageEditPermission = userCan('page-update', $model); + $newParentInRightLocation = ($newParent instanceof Book || ($newParent instanceof Chapter && $newParent->book_id === $newBook->id)); + $newParentPermission = ($newParent instanceof Chapter) ? 'chapter-update' : 'book-update'; + $hasNewParentPermission = userCan($newParentPermission, $newParent); + + $hasDeletePermissionIfMoving = (!$hasNewParent || userCan('page-delete', $model)); + $hasCreatePermissionIfMoving = (!$hasNewParent || userCan('page-create', $newParent)); + + $hasPermission = $hasCurrentParentPermission + && $newParentInRightLocation + && $hasNewParentPermission + && $hasPageEditPermission + && $hasDeletePermissionIfMoving + && $hasCreatePermissionIfMoving; + + if (!$hasPermission) { + return false; + } + } + + return true; + } + /** * Load models from the database into the given sort map. + * * @return array */ protected function loadModelsFromSortMap(BookSortMap $sortMap): array @@ -209,8 +273,8 @@ class BookContents $modelMap = []; $ids = [ 'chapter' => [], - 'page' => [], - 'book' => [], + 'page' => [], + 'book' => [], ]; foreach ($sortMap->all() as $sortMapItem) { @@ -221,7 +285,7 @@ class BookContents } } - $pages = Page::visible()->whereIn('id', array_unique($ids['page']))->get(Page::$listAttributes); + $pages = $this->queries->pages->visibleForList()->whereIn('id', array_unique($ids['page']))->get(); /** @var Page $page */ foreach ($pages as $page) { $modelMap['page:' . $page->id] = $page; @@ -231,14 +295,14 @@ class BookContents } } - $chapters = Chapter::visible()->whereIn('id', array_unique($ids['chapter']))->get(); + $chapters = $this->queries->chapters->visibleForList()->whereIn('id', array_unique($ids['chapter']))->get(); /** @var Chapter $chapter */ foreach ($chapters as $chapter) { $modelMap['chapter:' . $chapter->id] = $chapter; $ids['book'][] = $chapter->book_id; } - $books = Book::visible()->whereIn('id', array_unique($ids['book']))->get(); + $books = $this->queries->books->visibleForList()->whereIn('id', array_unique($ids['book']))->get(); /** @var Book $book */ foreach ($books as $book) { $modelMap['book:' . $book->id] = $book;