diff --git a/app/Entities/Controllers/ChapterController.php b/app/Entities/Controllers/ChapterController.php index 00616888a..4b0a525eb 100644 --- a/app/Entities/Controllers/ChapterController.php +++ b/app/Entities/Controllers/ChapterController.php @@ -5,6 +5,8 @@ namespace BookStack\Entities\Controllers; use BookStack\Activity\Models\View; use BookStack\Activity\Tools\UserEntityWatchOptions; use BookStack\Entities\Models\Book; +use BookStack\Entities\Queries\ChapterQueries; +use BookStack\Entities\Queries\EntityQueries; use BookStack\Entities\Repos\ChapterRepo; use BookStack\Entities\Tools\BookContents; use BookStack\Entities\Tools\Cloner; @@ -24,7 +26,9 @@ class ChapterController extends Controller { public function __construct( protected ChapterRepo $chapterRepo, - protected ReferenceFetcher $referenceFetcher + protected ChapterQueries $queries, + protected EntityQueries $entityQueries, + protected ReferenceFetcher $referenceFetcher, ) { } @@ -33,12 +37,15 @@ class ChapterController extends Controller */ public function create(string $bookSlug) { - $book = Book::visible()->where('slug', '=', $bookSlug)->firstOrFail(); + $book = $this->entityQueries->books->findVisibleBySlug($bookSlug); $this->checkOwnablePermission('chapter-create', $book); $this->setPageTitle(trans('entities.chapters_create')); - return view('chapters.create', ['book' => $book, 'current' => $book]); + return view('chapters.create', [ + 'book' => $book, + 'current' => $book, + ]); } /** @@ -55,7 +62,7 @@ class ChapterController extends Controller 'default_template_id' => ['nullable', 'integer'], ]); - $book = Book::visible()->where('slug', '=', $bookSlug)->firstOrFail(); + $book = $this->entityQueries->books->findVisibleBySlug($bookSlug); $this->checkOwnablePermission('chapter-create', $book); $chapter = $this->chapterRepo->create($validated, $book); @@ -68,7 +75,7 @@ class ChapterController extends Controller */ public function show(string $bookSlug, string $chapterSlug) { - $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug); + $chapter = $this->queries->findVisibleBySlugs($bookSlug, $chapterSlug); $this->checkOwnablePermission('chapter-view', $chapter); $sidebarTree = (new BookContents($chapter->book))->getTree(); @@ -96,7 +103,7 @@ class ChapterController extends Controller */ public function edit(string $bookSlug, string $chapterSlug) { - $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug); + $chapter = $this->queries->findVisibleBySlugs($bookSlug, $chapterSlug); $this->checkOwnablePermission('chapter-update', $chapter); $this->setPageTitle(trans('entities.chapters_edit_named', ['chapterName' => $chapter->getShortName()])); @@ -118,7 +125,7 @@ class ChapterController extends Controller 'default_template_id' => ['nullable', 'integer'], ]); - $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug); + $chapter = $this->queries->findVisibleBySlugs($bookSlug, $chapterSlug); $this->checkOwnablePermission('chapter-update', $chapter); $this->chapterRepo->update($chapter, $validated); @@ -133,7 +140,7 @@ class ChapterController extends Controller */ public function showDelete(string $bookSlug, string $chapterSlug) { - $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug); + $chapter = $this->queries->findVisibleBySlugs($bookSlug, $chapterSlug); $this->checkOwnablePermission('chapter-delete', $chapter); $this->setPageTitle(trans('entities.chapters_delete_named', ['chapterName' => $chapter->getShortName()])); @@ -149,7 +156,7 @@ class ChapterController extends Controller */ public function destroy(string $bookSlug, string $chapterSlug) { - $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug); + $chapter = $this->queries->findVisibleBySlugs($bookSlug, $chapterSlug); $this->checkOwnablePermission('chapter-delete', $chapter); $this->chapterRepo->destroy($chapter); @@ -164,7 +171,7 @@ class ChapterController extends Controller */ public function showMove(string $bookSlug, string $chapterSlug) { - $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug); + $chapter = $this->queries->findVisibleBySlugs($bookSlug, $chapterSlug); $this->setPageTitle(trans('entities.chapters_move_named', ['chapterName' => $chapter->getShortName()])); $this->checkOwnablePermission('chapter-update', $chapter); $this->checkOwnablePermission('chapter-delete', $chapter); @@ -182,7 +189,7 @@ class ChapterController extends Controller */ public function move(Request $request, string $bookSlug, string $chapterSlug) { - $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug); + $chapter = $this->queries->findVisibleBySlugs($bookSlug, $chapterSlug); $this->checkOwnablePermission('chapter-update', $chapter); $this->checkOwnablePermission('chapter-delete', $chapter); @@ -211,7 +218,7 @@ class ChapterController extends Controller */ public function showCopy(string $bookSlug, string $chapterSlug) { - $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug); + $chapter = $this->queries->findVisibleBySlugs($bookSlug, $chapterSlug); $this->checkOwnablePermission('chapter-view', $chapter); session()->flashInput(['name' => $chapter->name]); @@ -230,13 +237,13 @@ class ChapterController extends Controller */ public function copy(Request $request, Cloner $cloner, string $bookSlug, string $chapterSlug) { - $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug); + $chapter = $this->queries->findVisibleBySlugs($bookSlug, $chapterSlug); $this->checkOwnablePermission('chapter-view', $chapter); $entitySelection = $request->get('entity_selection') ?: null; - $newParentBook = $entitySelection ? $this->chapterRepo->findParentByIdentifier($entitySelection) : $chapter->getParent(); + $newParentBook = $entitySelection ? $this->entityQueries->findVisibleByStringIdentifier($entitySelection) : $chapter->getParent(); - if (is_null($newParentBook)) { + if (!$newParentBook instanceof Book) { $this->showErrorNotification(trans('errors.selected_book_not_found')); return redirect($chapter->getUrl('/copy')); @@ -256,7 +263,7 @@ class ChapterController extends Controller */ public function convertToBook(HierarchyTransformer $transformer, string $bookSlug, string $chapterSlug) { - $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug); + $chapter = $this->queries->findVisibleBySlugs($bookSlug, $chapterSlug); $this->checkOwnablePermission('chapter-update', $chapter); $this->checkOwnablePermission('chapter-delete', $chapter); $this->checkPermission('book-create-all'); diff --git a/app/Entities/Controllers/ChapterExportController.php b/app/Entities/Controllers/ChapterExportController.php index b67ec9b37..0e071fc82 100644 --- a/app/Entities/Controllers/ChapterExportController.php +++ b/app/Entities/Controllers/ChapterExportController.php @@ -2,7 +2,7 @@ namespace BookStack\Entities\Controllers; -use BookStack\Entities\Repos\ChapterRepo; +use BookStack\Entities\Queries\ChapterQueries; use BookStack\Entities\Tools\ExportFormatter; use BookStack\Exceptions\NotFoundException; use BookStack\Http\Controller; @@ -10,16 +10,10 @@ use Throwable; class ChapterExportController extends Controller { - protected $chapterRepo; - protected $exportFormatter; - - /** - * ChapterExportController constructor. - */ - public function __construct(ChapterRepo $chapterRepo, ExportFormatter $exportFormatter) - { - $this->chapterRepo = $chapterRepo; - $this->exportFormatter = $exportFormatter; + public function __construct( + protected ChapterQueries $queries, + protected ExportFormatter $exportFormatter, + ) { $this->middleware('can:content-export'); } @@ -31,7 +25,7 @@ class ChapterExportController extends Controller */ public function pdf(string $bookSlug, string $chapterSlug) { - $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug); + $chapter = $this->queries->findVisibleBySlugs($bookSlug, $chapterSlug); $pdfContent = $this->exportFormatter->chapterToPdf($chapter); return $this->download()->directly($pdfContent, $chapterSlug . '.pdf'); @@ -45,7 +39,7 @@ class ChapterExportController extends Controller */ public function html(string $bookSlug, string $chapterSlug) { - $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug); + $chapter = $this->queries->findVisibleBySlugs($bookSlug, $chapterSlug); $containedHtml = $this->exportFormatter->chapterToContainedHtml($chapter); return $this->download()->directly($containedHtml, $chapterSlug . '.html'); @@ -58,7 +52,7 @@ class ChapterExportController extends Controller */ public function plainText(string $bookSlug, string $chapterSlug) { - $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug); + $chapter = $this->queries->findVisibleBySlugs($bookSlug, $chapterSlug); $chapterText = $this->exportFormatter->chapterToPlainText($chapter); return $this->download()->directly($chapterText, $chapterSlug . '.txt'); @@ -71,7 +65,7 @@ class ChapterExportController extends Controller */ public function markdown(string $bookSlug, string $chapterSlug) { - $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug); + $chapter = $this->queries->findVisibleBySlugs($bookSlug, $chapterSlug); $chapterText = $this->exportFormatter->chapterToMarkdown($chapter); return $this->download()->directly($chapterText, $chapterSlug . '.md'); diff --git a/app/Entities/Controllers/PageController.php b/app/Entities/Controllers/PageController.php index eaad3c0b7..f2cd729c6 100644 --- a/app/Entities/Controllers/PageController.php +++ b/app/Entities/Controllers/PageController.php @@ -8,6 +8,8 @@ use BookStack\Activity\Tools\UserEntityWatchOptions; use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Chapter; use BookStack\Entities\Models\Page; +use BookStack\Entities\Queries\EntityQueries; +use BookStack\Entities\Queries\PageQueries; use BookStack\Entities\Repos\PageRepo; use BookStack\Entities\Tools\BookContents; use BookStack\Entities\Tools\Cloner; @@ -29,6 +31,8 @@ class PageController extends Controller { public function __construct( protected PageRepo $pageRepo, + protected PageQueries $pageQueries, + protected EntityQueries $entityQueries, protected ReferenceFetcher $referenceFetcher ) { } @@ -435,9 +439,9 @@ class PageController extends Controller $this->checkOwnablePermission('page-view', $page); $entitySelection = $request->get('entity_selection') ?: null; - $newParent = $entitySelection ? $this->pageRepo->findParentByIdentifier($entitySelection) : $page->getParent(); + $newParent = $entitySelection ? $this->entityQueries->findVisibleByStringIdentifier($entitySelection) : $page->getParent(); - if (is_null($newParent)) { + if (!$newParent instanceof Book && !$newParent instanceof Chapter) { $this->showErrorNotification(trans('errors.selected_book_chapter_not_found')); return redirect($page->getUrl('/copy')); diff --git a/app/Entities/Models/Chapter.php b/app/Entities/Models/Chapter.php index d3a710111..422c82442 100644 --- a/app/Entities/Models/Chapter.php +++ b/app/Entities/Models/Chapter.php @@ -11,7 +11,6 @@ use Illuminate\Support\Collection; * Class Chapter. * * @property Collection $pages - * @property string $description * @property ?int $default_template_id * @property ?Page $defaultTemplate */ diff --git a/app/Entities/Queries/BookQueries.php b/app/Entities/Queries/BookQueries.php index 6de28f0c2..5e85523cf 100644 --- a/app/Entities/Queries/BookQueries.php +++ b/app/Entities/Queries/BookQueries.php @@ -6,13 +6,18 @@ use BookStack\Entities\Models\Book; use BookStack\Exceptions\NotFoundException; use Illuminate\Database\Eloquent\Builder; -class BookQueries +class BookQueries implements ProvidesEntityQueries { public function start(): Builder { return Book::query(); } + public function findVisibleById(int $id): ?Book + { + return $this->start()->scopes('visible')->find($id); + } + public function findVisibleBySlug(string $slug): Book { /** @var ?Book $book */ diff --git a/app/Entities/Queries/BookshelfQueries.php b/app/Entities/Queries/BookshelfQueries.php index 7edff45b9..52e123087 100644 --- a/app/Entities/Queries/BookshelfQueries.php +++ b/app/Entities/Queries/BookshelfQueries.php @@ -6,13 +6,18 @@ use BookStack\Entities\Models\Bookshelf; use BookStack\Exceptions\NotFoundException; use Illuminate\Database\Eloquent\Builder; -class BookshelfQueries +class BookshelfQueries implements ProvidesEntityQueries { public function start(): Builder { return Bookshelf::query(); } + public function findVisibleById(int $id): ?Bookshelf + { + return $this->start()->scopes('visible')->find($id); + } + public function findVisibleBySlug(string $slug): Bookshelf { /** @var ?Bookshelf $shelf */ diff --git a/app/Entities/Queries/ChapterQueries.php b/app/Entities/Queries/ChapterQueries.php new file mode 100644 index 000000000..dcfc4aad3 --- /dev/null +++ b/app/Entities/Queries/ChapterQueries.php @@ -0,0 +1,53 @@ +start()->scopes('visible')->find($id); + } + + public function findVisibleBySlugs(string $bookSlug, string $chapterSlug): Chapter + { + /** @var ?Chapter $chapter */ + $chapter = $this->start()->with('book') + ->whereHas('book', function (Builder $query) use ($bookSlug) { + $query->where('slug', '=', $bookSlug); + }) + ->where('slug', '=', $chapterSlug) + ->first(); + + if ($chapter === null) { + throw new NotFoundException(trans('errors.chapter_not_found')); + } + + return $chapter; + } + + public function visibleForList(): Builder + { + return $this->start() + ->select(array_merge(static::$listAttributes, ['book_slug' => function ($builder) { + $builder->select('slug') + ->from('books') + ->whereColumn('books.id', '=', 'chapters.book_id'); + }])); + } +} diff --git a/app/Entities/Queries/EntityQueries.php b/app/Entities/Queries/EntityQueries.php index b1973b0ea..39a21c913 100644 --- a/app/Entities/Queries/EntityQueries.php +++ b/app/Entities/Queries/EntityQueries.php @@ -2,12 +2,42 @@ namespace BookStack\Entities\Queries; +use BookStack\Entities\Models\Entity; + class EntityQueries { public function __construct( public BookshelfQueries $shelves, public BookQueries $books, + public ChapterQueries $chapters, public PageQueries $pages, ) { } + + /** + * Find an entity via an identifier string in the format: + * {type}:{id} + * Example: (book:5). + */ + public function findVisibleByStringIdentifier(string $identifier): ?Entity + { + $explodedId = explode(':', $identifier); + $entityType = $explodedId[0]; + $entityId = intval($explodedId[1]); + + /** @var ?ProvidesEntityQueries $queries */ + $queries = match ($entityType) { + 'page' => $this->pages, + 'chapter' => $this->chapters, + 'book' => $this->books, + 'bookshelf' => $this->shelves, + default => null, + }; + + if (is_null($queries)) { + return null; + } + + return $queries->findVisibleById($entityId); + } } diff --git a/app/Entities/Queries/PageQueries.php b/app/Entities/Queries/PageQueries.php index b9c64f63c..f1991626f 100644 --- a/app/Entities/Queries/PageQueries.php +++ b/app/Entities/Queries/PageQueries.php @@ -5,13 +5,18 @@ namespace BookStack\Entities\Queries; use BookStack\Entities\Models\Page; use Illuminate\Database\Eloquent\Builder; -class PageQueries +class PageQueries implements ProvidesEntityQueries { public function start(): Builder { return Page::query(); } + public function findVisibleById(int $id): ?Page + { + return $this->start()->scopes('visible')->find($id); + } + public function visibleForList(): Builder { return $this->start() diff --git a/app/Entities/Queries/ProvidesEntityQueries.php b/app/Entities/Queries/ProvidesEntityQueries.php new file mode 100644 index 000000000..5c37b02e4 --- /dev/null +++ b/app/Entities/Queries/ProvidesEntityQueries.php @@ -0,0 +1,12 @@ +whereSlugs($bookSlug, $chapterSlug)->first(); - - if ($chapter === null) { - throw new NotFoundException(trans('errors.chapter_not_found')); - } - - return $chapter; - } - /** * Create a new chapter in the system. */ @@ -91,8 +75,8 @@ class ChapterRepo */ public function move(Chapter $chapter, string $parentIdentifier): Book { - $parent = $this->findParentByIdentifier($parentIdentifier); - if (is_null($parent)) { + $parent = $this->entityQueries->findVisibleByStringIdentifier($parentIdentifier); + if (!$parent instanceof Book) { throw new MoveOperationException('Book to move chapter into not found'); } @@ -106,24 +90,4 @@ class ChapterRepo return $parent; } - - /** - * Find a page parent entity via an identifier string in the format: - * {type}:{id} - * Example: (book:5). - * - * @throws MoveOperationException - */ - public function findParentByIdentifier(string $identifier): ?Book - { - $stringExploded = explode(':', $identifier); - $entityType = $stringExploded[0]; - $entityId = intval($stringExploded[1]); - - if ($entityType !== 'book') { - throw new MoveOperationException('Chapters can only be in books'); - } - - return Book::visible()->where('id', '=', $entityId)->first(); - } } diff --git a/app/Entities/Repos/PageRepo.php b/app/Entities/Repos/PageRepo.php index 85237a752..929de528d 100644 --- a/app/Entities/Repos/PageRepo.php +++ b/app/Entities/Repos/PageRepo.php @@ -8,6 +8,7 @@ use BookStack\Entities\Models\Chapter; use BookStack\Entities\Models\Entity; use BookStack\Entities\Models\Page; use BookStack\Entities\Models\PageRevision; +use BookStack\Entities\Queries\EntityQueries; use BookStack\Entities\Tools\BookContents; use BookStack\Entities\Tools\PageContent; use BookStack\Entities\Tools\PageEditorData; @@ -26,6 +27,7 @@ class PageRepo public function __construct( protected BaseRepo $baseRepo, protected RevisionRepo $revisionRepo, + protected EntityQueries $entityQueries, protected ReferenceStore $referenceStore, protected ReferenceUpdater $referenceUpdater ) { @@ -324,8 +326,8 @@ class PageRepo */ public function move(Page $page, string $parentIdentifier): Entity { - $parent = $this->findParentByIdentifier($parentIdentifier); - if (is_null($parent)) { + $parent = $this->entityQueries->findVisibleByStringIdentifier($parentIdentifier); + if (!$parent instanceof Chapter && !$parent instanceof Book) { throw new MoveOperationException('Book or chapter to move page into not found'); } @@ -343,28 +345,6 @@ class PageRepo return $parent; } - /** - * Find a page parent entity via an identifier string in the format: - * {type}:{id} - * Example: (book:5). - * - * @throws MoveOperationException - */ - public function findParentByIdentifier(string $identifier): ?Entity - { - $stringExploded = explode(':', $identifier); - $entityType = $stringExploded[0]; - $entityId = intval($stringExploded[1]); - - if ($entityType !== 'book' && $entityType !== 'chapter') { - throw new MoveOperationException('Pages can only be in books or chapters'); - } - - $parentClass = $entityType === 'book' ? Book::class : Chapter::class; - - return $parentClass::visible()->where('id', '=', $entityId)->first(); - } - /** * Get a new priority for a page. */